Rewrite AuthCore as a native Swift port of smbcloud-auth-sdk

Replace the hand-rolled OAuth/PKCE AuthCore (which spoke the platform oauth/* endpoints) with a faithful native Swift port of the canonical client SDK crate, smbcloud-cli/crates/smbcloud-auth-sdk: - AuthCoreClient: tenant client flow (login/signup/me/logout/remove) plus Apple provider helpers and account recovery (reset/resend); confidential ClientCredentials (app_id/app_secret). - OIDC: public-client Authorization Code + PKCE flow. - ClientCredentials, SmbCloudEnvironment, SmbCloudError/SmbCloudErrorCode, AccountStatus, User, SignupResult, Apple types, shared HTTP layer. - SmbCloudAuth (Apple UI): SmbCloudWebAuth now drives the OIDC flow; SmbCloudCredentialsManager (Keychain) unchanged. Remove the UniFFI bridge entirely: drop the smbcloud_authFFI xcframework, the SmbCloudAuthFFI target/product, and the FFI generation machinery from the Makefile. The package is now pure Swift and builds with `swift build` on Apple and Linux. Docs/AuthCore-SDK-Surface.md pins the surface to the Rust crate; QuickStart/example docs updated to the new API. Note: account-recovery methods are ahead of smbcloud-auth-sdk (the backend has them, the crate doesn't wrap them yet) — upstream for binding parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Seto Elkahfi committed Jun 22, 2026 at 21:53 UTC 87021bd00c968f38838bb7d2426ba97eadbcb9e6
27 files changed +1478 -3332
Docs/AuthCore-SDK-Surface.md new
+257
@@ -0,0 +1,257 @@
1 +# AuthCore target surface — pinned to `smbcloud-auth-sdk`
2 +
3 +**Status:** design spec for correcting `AuthCore`.
4 +
5 +## Principle
6 +
7 +`AuthCore` is a **faithful Swift port of the canonical client SDK core,
8 +`smbcloud-cli/crates/smbcloud-auth-sdk`** — the same crate wrapped by the
9 +`-apple`, `-py`, and `-wasm` bindings. The Rust crate is the **contract
10 +authority**; `smbcloud-model` is the **DTO/type authority**; the Rails
11 +`smbcloud-api` is the ultimate wire truth. AuthCore implements the contract
12 +natively in Swift (no Rust/UniFFI dependency, so it builds cleanly on Linux
13 +server-side and on Apple) and is held in lockstep by a **shared conformance
14 +suite** run against the same fixtures/contract as the other bindings.
15 +
16 +> The previously shipped `AuthCore` (`SmbCloudAuthClient` doing `oauth/authorize`
17 +> PKCE) corresponds to *only* the `oidc.rs` module and was mislabeled as the
18 +> whole SDK — it omitted the primary tenant credential flow. This spec restores
19 +> the full surface.
20 +
21 +Module map (`smbcloud-auth-sdk/src/*` → AuthCore):
22 +
23 +| Rust module | AuthCore surface |
24 +|------------------------|----------------------------------------------------|
25 +| `client_credentials.rs`| `ClientCredentials`, base-URL builder |
26 +| `login.rs` | `AuthCoreClient.login` |
27 +| `signup.rs` | `AuthCoreClient.signup` |
28 +| `me.rs` | `AuthCoreClient.me` |
29 +| `logout.rs` | `AuthCoreClient.logout` |
30 +| `remove.rs` | `AuthCoreClient.remove` |
31 +| `apple.rs` | `AuthCoreClient.buildAppleAuthorizationRequest` / `parseAppleCallbackURL` |
32 +| `oidc.rs` | `OIDC.*` (public-client PKCE) |
33 +
34 +## Shared types
35 +
36 +```swift
37 +/// Mirrors smbcloud_network::environment::Environment.
38 +public enum SmbCloudEnvironment: Sendable {
39 + case dev // http · localhost:8088
40 + case production // https · api.smbcloud.xyz
41 +
42 + public var apiProtocol: String { self == .dev ? "http" : "https" }
43 + public var apiHost: String { self == .dev ? "localhost:8088" : "api.smbcloud.xyz" }
44 +}
45 +
46 +/// Mirrors client_credentials::ClientCredentials. Sent as the `client_id` and
47 +/// `client_secret` QUERY params on every /v1/client/* request.
48 +public struct ClientCredentials: Sendable {
49 + public let appId: String
50 + public let appSecret: String
51 + public init(appId: String, appSecret: String)
52 +}
53 +
54 +/// Mirrors smbcloud_model::error_codes::{ErrorResponse, ErrorCode}.
55 +public struct SmbCloudError: Error, Sendable {
56 + public let code: SmbCloudErrorCode
57 + public let message: String
58 +}
59 +
60 +public enum SmbCloudErrorCode: Int, Sendable {
61 + case unknown = 0, parseError = 1, networkError = 2, inputError = 3,
62 + missingConfig = 4, cancel = 5
63 + case unauthorized = 100, invalidParams = 101
64 + case emailNotFound = 1000, emailNotVerified = 1001, emailConfirmationFailed = 1002,
65 + passwordNotSet = 1003, gitHubEmailNotConnected = 1004, emailAlreadyExist = 1005,
66 + invalidPassword = 1006
67 + case projectNotFound = 2000, unsupportedRunner = 2001
68 +}
69 +```
70 +
71 +## Tenant credential flow (primary)
72 +
73 +```swift
74 +public struct AuthCoreClient: Sendable {
75 + public init(environment: SmbCloudEnvironment, credentials: ClientCredentials)
76 +
77 + // login.rs → POST /v1/client/users/sign_in (body {user:{email,password}})
78 + public func login(username: String, password: String) async throws -> AccountStatus
79 +
80 + // signup.rs → POST /v1/client/users (body {user:{email,password}})
81 + public func signup(email: String, password: String) async throws -> SignupResult
82 +
83 + // me.rs → GET /v1/client/me
84 + public func me(accessToken: String) async throws -> User
85 +
86 + // logout.rs → DELETE /v1/client/users/sign_out
87 + public func logout(accessToken: String) async throws
88 +
89 + // remove.rs → DELETE /v1/client/me
90 + public func remove(accessToken: String) async throws
91 +
92 + // Account recovery — ahead of smbcloud-auth-sdk (backend has these; the
93 + // canonical crate doesn't wrap them yet). Upstream for parity. Each returns
94 + // the server's user-facing message.
95 + public func requestPasswordReset(email: String) async throws -> String // POST users/reset_password
96 + public func completePasswordReset(token:password:passwordConfirmation:) async throws -> String // POST users/reset_password/complete
97 + public func resendConfirmation(email: String) async throws -> String // POST users/resend_confirmation
98 +}
99 +
100 +/// login.rs result, mapped by network::request_login.
101 +public enum AccountStatus: Sendable {
102 + case notFound
103 + case ready(accessToken: String) // full Authorization header value (includes "Bearer ")
104 + case incomplete(status: AccountErrorCode)
105 +}
106 +
107 +/// Mirrors smbcloud_model::account::ErrorCode (distinct from SmbCloudErrorCode).
108 +public enum AccountErrorCode: Int, Sendable {
109 + case emailNotFound = 1000, emailUnverified = 1001, emailConfirmationFailed = 1002,
110 + passwordNotSet = 1003, githubNotLinked = 1004, emailAlreadyExist = 1005,
111 + invalidPassword = 1006, hostedMailAccountUnverified = 1007
112 +}
113 +
114 +/// me.rs returns smbcloud_model::account::User (the SDK decodes only these).
115 +public struct User: Codable, Sendable {
116 + public let id: Int
117 + public let email: String
118 + public let createdAt: Date
119 + public let updatedAt: Date
120 +}
121 +
122 +/// signup.rs returns smbcloud_model::signup::SignupResult.
123 +public struct SignupResult: Codable, Sendable {
124 + public let code: Int?
125 + public let message: String
126 + public let data: AccountData?
127 +}
128 +
129 +public struct AccountData: Codable, Sendable { // account::Data
130 + public let id: Int
131 + public let email: String
132 + public let createdAt: String
133 +}
134 +```
135 +
136 +### `login` → `AccountStatus` mapping (from `network::request_login`)
137 +
138 +| HTTP | Result |
139 +|-----------------------------------|--------|
140 +| `200` + `Authorization` header | `.ready(accessToken: <header value>)` |
141 +| `200`, no header, body `EmailNotVerified` | `.incomplete(.emailUnverified)` |
142 +| `200`, no header, body `PasswordNotSet` | `.incomplete(.passwordNotSet)` |
143 +| `404` | `.notFound` |
144 +| `422` | parse `SmbAuthorization` → `.incomplete(error_code)`, else throw `SmbCloudError(.networkError, message)` |
145 +| transport failure | throw `SmbCloudError(.networkError, …)` |
146 +
147 +## Apple provider flow (tenant)
148 +
149 +```swift
150 +extension AuthCoreClient {
151 + // apple.rs → builds v1/client/oauth/apple/authorize URL (creds in query)
152 + public func buildAppleAuthorizationRequest(
153 + redirectURI: String, state: String? = nil
154 + ) throws -> AppleAuthorizationRequest
155 +
156 + public func parseAppleCallbackURL(
157 + _ callbackURL: String, expectedState: String? = nil
158 + ) throws -> AppleAuthSession
159 +}
160 +
161 +public struct AppleAuthorizationRequest: Sendable {
162 + public let authorizeURL: String
163 + public let redirectURI: String
164 + public let state: String
165 +}
166 +
167 +public struct AppleAuthSession: Sendable {
168 + public let accessToken: String
169 + public let refreshToken: String?
170 + public let email: String?
171 + public let name: String?
172 + public let provider: String // defaults "apple"
173 + public let providerAccountId: String
174 + public let state: String?
175 +}
176 +```
177 +
178 +## OIDC public-client flow (`oidc.rs`)
179 +
180 +Public client: uses `oidcClientId` only — **no `app_secret`** — against the
181 +issuer endpoints `oauth/authorize|token|userinfo`. PKCE: `code_verifier` = two
182 +concatenated UUID-simple strings; `code_challenge` = base64url-no-pad(SHA256);
183 +`code_challenge_method=S256`; `scope="openid profile email"`.
184 +
185 +```swift
186 +public enum OIDC {
187 + public static func buildAuthorizationRequest(
188 + environment: SmbCloudEnvironment, oidcClientId: String, redirectURI: String
189 + ) throws -> AuthorizationRequest
190 +
191 + public static func parseCallbackURL(_ callbackURL: String) throws -> CallbackPayload
192 +
193 + public static func exchangeCode(
194 + environment: SmbCloudEnvironment, oidcClientId: String,
195 + redirectURI: String, code: String, codeVerifier: String
196 + ) async throws -> TokenResponse
197 +
198 + public static func getUserInfo(
199 + environment: SmbCloudEnvironment, accessToken: String, tenantId: String? = nil
200 + ) async throws -> UserInfo
201 +}
202 +
203 +public struct AuthorizationRequest: Sendable {
204 + public let authorizeURL: String
205 + public let redirectURI: String
206 + public let state: String
207 + public let codeVerifier: String
208 +}
209 +public struct CallbackPayload: Sendable { public let code: String; public let state: String }
210 +public struct TokenResponse: Codable, Sendable {
211 + public let accessToken: String
212 + public let tokenType: String
213 + public let expiresIn: Int?
214 + public let refreshToken: String?
215 + public let scope: String?
216 + public let idToken: String?
217 +}
218 +public struct UserInfo: Codable, Sendable {
219 + public let sub: String
220 + public let email: String?
221 + public let emailVerified: Bool?
222 + public let tenantId: UInt64?
223 + public let tenantSlug: String?
224 +}
225 +```
226 +
227 +## Transport notes (match the Rust crate exactly)
228 +
229 +- **Credentials are QUERY params** named `client_id` / `client_secret` on every
230 + `/v1/client/*` URL (built by `base_url_builder`), even for POST/DELETE.
231 +- `login` / `signup` send header `User-agent: <app_id>` and a JSON body.
232 +- `me` sends `Authorization: <accessToken>` (the value is passed **as-is** — it
233 + already includes the `Bearer ` prefix because `.ready` captured the full
234 + header), plus `Accept: application/json` and
235 + `Content-Type: application/x-www-form-urlencoded`.
236 +- `logout` is bespoke: `200` → success, anything else → `SmbCloudError(.unauthorized)`.
237 +- `remove` decodes an empty body (`request::<()>`).
238 +- Use the host HTTP stack (`URLSession` on Apple, `URLSession`/async-http-client
239 + on Linux). No embedded runtime.
240 +
241 +## Layering & client confidentiality
242 +
243 +- `AuthCore` (this surface) is headless and builds on Linux + Apple. The Vapor
244 + `api-server` depends on it directly and is the **only** place that holds
245 + `app_secret` (confidential client).
246 +- `SmbCloudAuth` (Apple-only) keeps the UI/Keychain layer (ASWebAuthenticationSession
247 + for the OIDC/Apple flows) and re-exports `AuthCore`.
248 +- **Never ship `app_secret` in a distributed client** (native app / browser).
249 + Public clients use the OIDC/Apple public flows or route through a BFF
250 + (SwiftStore's `api-server`).
251 +
252 +## Conformance
253 +
254 +Pin AuthCore to the crate with shared contract tests: golden request fixtures
255 +(method, path, query, headers, body) and response→type fixtures covering the
256 +`AccountStatus` mapping and the error model, run in CI exactly as the other
257 +bindings are. Drift fails the build.
Docs/QuickStart-iOS.md
+6 -20
@@ -30,24 +30,15 @@ final class AuthenticationStore: ObservableObject {
30 @Published private(set) var errorMessage: String?
31 @Published private(set) var isLoading = false
32
33 - private let webAuth: SmbCloudWebAuth?
33 + private let webAuth = SmbCloudWebAuth(
34 + environment: .production,
35 + oidcClientId: "YOUR_OIDC_CLIENT_ID",
36 + redirectURL: URL(string: "myapp://auth/callback")!
37 + )
38 private let credentialsManager = SmbCloudCredentialsManager(
39 service: "com.example.myapp.smbcloud-auth"
40 )
41
38 - init() {
39 - do {
40 - self.webAuth = try SmbCloudWebAuth(
41 - domain: "api.smbcloud.xyz",
42 - clientId: "YOUR_PUBLIC_CLIENT_ID",
43 - redirectURL: URL(string: "myapp://auth/callback")!
44 - )
45 - } catch {
46 - self.webAuth = nil
47 - self.errorMessage = error.localizedDescription
48 - }
49 - }
50 -
42 func restoreSession() {
43 do {
44 session = try credentialsManager.currentValidSession()
@@ -57,11 +48,6 @@ final class AuthenticationStore: ObservableObject {
48 }
49
50 func signIn(anchor: @escaping () -> SmbCloudPresentationAnchor) async {
60 - guard let webAuth else {
61 - errorMessage = errorMessage ?? "The smbCloud web auth client could not be created."
62 - return
63 - }
64 -
51 isLoading = true
52 errorMessage = nil
53 defer { isLoading = false }
@@ -82,7 +68,7 @@ final class AuthenticationStore: ObservableObject {
68
69 func signOut() {
70 do {
85 - try webAuth?.clearSession(credentialsManager: credentialsManager)
71 + try webAuth.clearSession(credentialsManager: credentialsManager)
72 session = nil
73 email = nil
74 errorMessage = nil
Docs/QuickStart-macOS.md
+6 -20
@@ -27,24 +27,15 @@ final class AuthenticationStore: ObservableObject {
27 @Published private(set) var email: String?
28 @Published private(set) var errorMessage: String?
29
30 - private let webAuth: SmbCloudWebAuth?
30 + private let webAuth = SmbCloudWebAuth(
31 + environment: .production,
32 + oidcClientId: "YOUR_OIDC_CLIENT_ID",
33 + redirectURL: URL(string: "mymacapp://auth/callback")!
34 + )
35 private let credentialsManager = SmbCloudCredentialsManager(
36 service: "com.example.mymacapp.smbcloud-auth"
37 )
38
35 - init() {
36 - do {
37 - self.webAuth = try SmbCloudWebAuth(
38 - domain: "api.smbcloud.xyz",
39 - clientId: "YOUR_PUBLIC_CLIENT_ID",
40 - redirectURL: URL(string: "mymacapp://auth/callback")!
41 - )
42 - } catch {
43 - self.webAuth = nil
44 - self.errorMessage = error.localizedDescription
45 - }
46 - }
47 -
39 func restoreSession() {
40 do {
41 session = try credentialsManager.currentValidSession()
@@ -54,11 +45,6 @@ final class AuthenticationStore: ObservableObject {
45 }
46
47 func signIn() async {
57 - guard let webAuth else {
58 - errorMessage = errorMessage ?? "The smbCloud web auth client could not be created."
59 - return
60 - }
61 -
48 do {
49 let session = try await webAuth.login(
50 presentationAnchorProvider: {
@@ -77,7 +63,7 @@ final class AuthenticationStore: ObservableObject {
63
64 func signOut() {
65 do {
80 - try webAuth?.clearSession(credentialsManager: credentialsManager)
66 + try webAuth.clearSession(credentialsManager: credentialsManager)
67 session = nil
68 email = nil
69 } catch {
Docs/Release.md
+5 -22
@@ -7,16 +7,13 @@ The package is consumable directly from the Git tag.
7
8 ## What gets released
9
10 -Public product:
10 +Public products (both pure Swift):
11
12 -- `SmbCloudAuth`
12 +- `AuthCore` — cross-platform headless engine
13 +- `SmbCloudAuth` — Apple UI layer on top of `AuthCore`
14
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.
15 +There is no Rust/UniFFI artifact to build or publish; the package is fully
16 +source-based.
17
18 ## CI validation
19
@@ -61,17 +58,3 @@ Before tagging, run:
58 make verify
59 make verify-apple-destinations
60 ```
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 -```bash
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/README.md
+1 -1
@@ -18,7 +18,7 @@ This folder contains a small SwiftUI-oriented example of the v1 MVP API surface:
18
19 Before using the example, replace:
20
21 -- `YOUR_PUBLIC_CLIENT_ID`
21 +- `YOUR_OIDC_CLIENT_ID`
22 - `hostedloginexample://auth/callback`
23 - Keychain service name
24
Examples/HostedLoginExample/Sources/HostedLoginExample/AuthenticationStore.swift
+6 -20
@@ -8,24 +8,15 @@ final class AuthenticationStore: ObservableObject {
8 @Published private(set) var errorMessage: String?
9 @Published private(set) var isLoading = false
10
11 - private let webAuth: SmbCloudWebAuth?
11 + private let webAuth = SmbCloudWebAuth(
12 + environment: .production,
13 + oidcClientId: "YOUR_OIDC_CLIENT_ID",
14 + redirectURL: URL(string: "hostedloginexample://auth/callback")!
15 + )
16 private let credentialsManager = SmbCloudCredentialsManager(
17 service: "com.example.hosted-login-example.smbcloud-auth"
18 )
19
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 -
20 func restoreSession() {
21 do {
22 session = try credentialsManager.currentValidSession()
@@ -35,11 +26,6 @@ final class AuthenticationStore: ObservableObject {
26 }
27
28 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 -
29 isLoading = true
30 errorMessage = nil
31 defer { isLoading = false }
@@ -60,7 +46,7 @@ final class AuthenticationStore: ObservableObject {
46
47 func signOut() {
48 do {
63 - try webAuth?.clearSession(credentialsManager: credentialsManager)
49 + try webAuth.clearSession(credentialsManager: credentialsManager)
50 session = nil
51 userEmail = nil
52 errorMessage = nil
Makefile
+13 -168
@@ -1,180 +1,26 @@
1 SHELL := /bin/sh
2
3 SWIFT_REPO := $(CURDIR)
4 -CLI_REPO ?= $(abspath ../smbcloud-cli)
5 -CARGO_MANIFEST := $(CLI_REPO)/Cargo.toml
6 -APPLE_CRATE_MANIFEST := $(CLI_REPO)/crates/smbcloud-auth-sdk-apple/Cargo.toml
7 -BINDGEN := $(CLI_REPO)/target/release/uniffi-bindgen
8 -
9 -RUST_STABLE ?= +$(shell sed -n 's/^channel = "\(.*\)"/\1/p' "$(CLI_REPO)/rust-toolchain.toml" | head -n 1)
10 -RUST_NIGHTLY ?= +nightly
11 -
12 -IOS_DEPLOYMENT_TARGET ?= 16.0
13 -MACOS_DEPLOYMENT_TARGET ?= 14.0
14 -TVOS_DEPLOYMENT_TARGET ?= 16.0
15 -VISIONOS_DEPLOYMENT_TARGET ?= 1.0
16 -
4 LOCAL_DIR := $(SWIFT_REPO)/.local
18 -GENERATED_DIR := $(LOCAL_DIR)/generated/SmbCloudAuth
19 -HEADERS_DIR := $(LOCAL_DIR)/Headers
20 -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/SmbCloudAuthFFI/smbcloud_auth.swift
25 -LOCAL_FFI_MARKER := $(LOCAL_DIR)/use-local-ffi
5
27 -LIB_NAME := libsmbcloud_auth.a
28 -SUPPORTED_PLATFORMS := ios macos tvos visionos
29 -
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-linux verify-apple-destinations verify-apple-destination
6 +.PHONY: help verify verify-example verify-linux verify-apple-destinations verify-apple-destination clean
7
8 help:
9 @printf '%s\n' \
34 - 'SmbCloudAuth Swift — local Apple-platform builds' \
10 + 'SmbCloudAuth Swift — pure-Swift SDK' \
11 '' \
12 'Usage:' \
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-linux Build + test the package on a non-Apple toolchain (AuthCore)' \
44 - ' make verify-apple-destinations Build the public package for iOS, tvOS, and visionOS generic destinations' \
45 - ' make deactivate-local-ffi Hide the optional local SmbCloudAuthFFI product' \
46 - '' \
47 - 'Artifacts:' \
48 - ' smbcloud_authFFI.xcframework/' \
49 - ' Sources/SmbCloudAuthFFI/smbcloud_auth.swift (regenerated from the local Rust build)' \
50 - '' \
51 - 'Notes:' \
52 - ' AuthCore is the cross-platform (Apple/Linux/Windows/Android) headless core.' \
53 - ' SmbCloudAuth is the Apple UI layer (hosted login + Keychain) on top of AuthCore.' \
54 - ' Both pure-Swift products build without local Rust artifacts.' \
55 - ' Local Rust/UniFFI builds enable the optional SmbCloudAuthFFI product.' \
13 + ' make verify Build + test the package and build the example app' \
14 + ' make verify-example Build the packaged macOS example app' \
15 + ' make verify-linux Build + test AuthCore on a non-Apple toolchain' \
16 + ' make verify-apple-destinations Build the package for iOS, tvOS, and visionOS' \
17 + ' make clean Remove local build artifacts' \
18 '' \
57 - 'Optional overrides:' \
58 - ' CLI_REPO=/absolute/path/to/smbcloud-cli'
59 -
60 -platform:
61 - @test -n "$(PLATFORM)" || { echo "Set PLATFORM=$(SUPPORTED_PLATFORMS)"; exit 1; }
62 - @case "$(PLATFORM)" in \
63 - ios|macos|tvos|visionos) $(MAKE) "$(PLATFORM)" ;; \
64 - *) echo "Unsupported PLATFORM='$(PLATFORM)'. Expected one of: $(SUPPORTED_PLATFORMS)"; exit 1 ;; \
65 - esac
66 -
67 -validate:
68 - @test -f "$(CARGO_MANIFEST)" || { echo "Could not find workspace Cargo.toml at $(CARGO_MANIFEST). Set CLI_REPO."; exit 1; }
69 - @test -f "$(APPLE_CRATE_MANIFEST)" || { echo "Could not find smbcloud-auth-sdk-apple crate at $(APPLE_CRATE_MANIFEST)"; exit 1; }
70 - @test -d "$(SWIFT_REPO)/Sources/SmbCloudAuth" || { echo "Run make from the smbcloud-auth Swift package root."; exit 1; }
71 -
72 -build-bindgen: validate
73 - cargo $(RUST_STABLE) build --manifest-path "$(APPLE_CRATE_MANIFEST)" --features bindgen --bin uniffi-bindgen --release
74 -
75 -prepare:
76 - @rm -rf "$(FRAMEWORK_DIR)" "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)" "$(LOCAL_FFI_MARKER)"
77 - @mkdir -p "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)" "$(SWIFT_REPO)/Sources/SmbCloudAuthFFI"
78 -
79 -generate-swift:
80 - @test -n "$(BINDGEN_INPUT)" || { echo "BINDGEN_INPUT is required"; exit 1; }
81 - cd "$(CLI_REPO)" && "$(BINDGEN)" generate --library "$(BINDGEN_INPUT)" --language swift --out-dir "$(GENERATED_DIR)"
82 - cp "$(GENERATED_DIR)/smbcloud_auth.swift" "$(SWIFT_GLUE)"
83 - cp "$(GENERATED_DIR)/smbcloud_authFFI.h" "$(HEADERS_DIR)/smbcloud_authFFI.h"
84 - @echo "Updated $(SWIFT_GLUE)"
85 -
86 -activate-local-ffi:
87 - @mkdir -p "$(LOCAL_DIR)"
88 - @touch "$(LOCAL_FFI_MARKER)"
89 - @echo "Enabled local SmbCloudAuthFFI product"
90 -
91 -deactivate-local-ffi:
92 - @rm -f "$(LOCAL_FFI_MARKER)"
93 - @echo "Disabled local SmbCloudAuthFFI product"
94 -
95 -stage-framework:
96 - @test -n "$(LIB_INPUT)" || { echo "LIB_INPUT is required"; exit 1; }
97 - @test -n "$(FRAMEWORK_OUTPUT)" || { echo "FRAMEWORK_OUTPUT is required"; exit 1; }
98 - @rm -rf "$(FRAMEWORK_OUTPUT)"
99 - @mkdir -p "$(FRAMEWORK_OUTPUT)/Headers" "$(FRAMEWORK_OUTPUT)/Modules"
100 - cp "$(LIB_INPUT)" "$(FRAMEWORK_OUTPUT)/$(FRAMEWORK_BINARY_NAME)"
101 - cp "$(HEADERS_DIR)/smbcloud_authFFI.h" "$(FRAMEWORK_OUTPUT)/Headers/smbcloud_authFFI.h"
102 - @printf '%s\n' \
103 - 'framework module smbcloud_authFFI {' \
104 - ' header "smbcloud_authFFI.h"' \
105 - ' export *' \
106 - ' use "Darwin"' \
107 - ' use "_Builtin_stdbool"' \
108 - ' use "_Builtin_stdint"' \
109 - '}' > "$(FRAMEWORK_OUTPUT)/Modules/module.modulemap"
110 - @printf '%s\n' \
111 - '<?xml version="1.0" encoding="UTF-8"?>' \
112 - '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
113 - '<plist version="1.0">' \
114 - '<dict>' \
115 - ' <key>CFBundleIdentifier</key>' \
116 - ' <string>xyz.smbcloud.smbcloud-auth-ffi</string>' \
117 - ' <key>CFBundleName</key>' \
118 - ' <string>$(FRAMEWORK_BINARY_NAME)</string>' \
119 - ' <key>CFBundleExecutable</key>' \
120 - ' <string>$(FRAMEWORK_BINARY_NAME)</string>' \
121 - ' <key>CFBundlePackageType</key>' \
122 - ' <string>FMWK</string>' \
123 - ' <key>CFBundleShortVersionString</key>' \
124 - ' <string>1.0.1</string>' \
125 - ' <key>CFBundleVersion</key>' \
126 - ' <string>1</string>' \
127 - '</dict>' \
128 - '</plist>' > "$(FRAMEWORK_OUTPUT)/Info.plist"
129 -
130 -ios: build-bindgen prepare
131 - IPHONEOS_DEPLOYMENT_TARGET=$(IOS_DEPLOYMENT_TARGET) cargo $(RUST_STABLE) rustc --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-ios --release --lib --crate-type staticlib
132 - IPHONEOS_DEPLOYMENT_TARGET=$(IOS_DEPLOYMENT_TARGET) cargo $(RUST_STABLE) rustc --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-ios-sim --release --lib --crate-type staticlib
133 - $(MAKE) generate-swift BINDGEN_INPUT="$(CLI_REPO)/target/aarch64-apple-ios/release/$(LIB_NAME)"
134 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-ios/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/ios/$(SLICE_FRAMEWORK_NAME)"
135 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-ios-sim/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/ios-sim/$(SLICE_FRAMEWORK_NAME)"
136 - xcodebuild -create-xcframework \
137 - -framework "$(FRAMEWORK_STAGING_DIR)/ios/$(SLICE_FRAMEWORK_NAME)" \
138 - -framework "$(FRAMEWORK_STAGING_DIR)/ios-sim/$(SLICE_FRAMEWORK_NAME)" \
139 - -output "$(FRAMEWORK_DIR)"
140 - $(MAKE) activate-local-ffi
141 - @echo "Built $(FRAMEWORK_DIR) for iOS"
142 -
143 -macos: build-bindgen prepare
144 - MACOSX_DEPLOYMENT_TARGET=$(MACOS_DEPLOYMENT_TARGET) cargo $(RUST_STABLE) rustc --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-darwin --release --lib --crate-type staticlib
145 - $(MAKE) generate-swift BINDGEN_INPUT="$(CLI_REPO)/target/aarch64-apple-darwin/release/$(LIB_NAME)"
146 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-darwin/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/macos/$(SLICE_FRAMEWORK_NAME)"
147 - xcodebuild -create-xcframework \
148 - -framework "$(FRAMEWORK_STAGING_DIR)/macos/$(SLICE_FRAMEWORK_NAME)" \
149 - -output "$(FRAMEWORK_DIR)"
150 - $(MAKE) activate-local-ffi
151 - @echo "Built $(FRAMEWORK_DIR) for macOS"
152 -
153 -tvos: build-bindgen prepare
154 - TVOS_DEPLOYMENT_TARGET=$(TVOS_DEPLOYMENT_TARGET) cargo $(RUST_NIGHTLY) rustc -Z build-std --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-tvos --release --lib --crate-type staticlib
155 - TVOS_DEPLOYMENT_TARGET=$(TVOS_DEPLOYMENT_TARGET) cargo $(RUST_NIGHTLY) rustc -Z build-std --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-tvos-sim --release --lib --crate-type staticlib
156 - $(MAKE) generate-swift BINDGEN_INPUT="$(CLI_REPO)/target/aarch64-apple-tvos/release/$(LIB_NAME)"
157 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-tvos/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/tvos/$(SLICE_FRAMEWORK_NAME)"
158 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-tvos-sim/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/tvos-sim/$(SLICE_FRAMEWORK_NAME)"
159 - xcodebuild -create-xcframework \
160 - -framework "$(FRAMEWORK_STAGING_DIR)/tvos/$(SLICE_FRAMEWORK_NAME)" \
161 - -framework "$(FRAMEWORK_STAGING_DIR)/tvos-sim/$(SLICE_FRAMEWORK_NAME)" \
162 - -output "$(FRAMEWORK_DIR)"
163 - $(MAKE) activate-local-ffi
164 - @echo "Built $(FRAMEWORK_DIR) for tvOS"
165 -
166 -visionos: build-bindgen prepare
167 - XROS_DEPLOYMENT_TARGET=$(VISIONOS_DEPLOYMENT_TARGET) cargo $(RUST_NIGHTLY) rustc -Z build-std --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-visionos --release --lib --crate-type staticlib
168 - XROS_DEPLOYMENT_TARGET=$(VISIONOS_DEPLOYMENT_TARGET) cargo $(RUST_NIGHTLY) rustc -Z build-std --manifest-path "$(CARGO_MANIFEST)" --package smbcloud-auth-sdk-apple --target aarch64-apple-visionos-sim --release --lib --crate-type staticlib
169 - $(MAKE) generate-swift BINDGEN_INPUT="$(CLI_REPO)/target/aarch64-apple-visionos/release/$(LIB_NAME)"
170 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-visionos/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/visionos/$(SLICE_FRAMEWORK_NAME)"
171 - $(MAKE) stage-framework LIB_INPUT="$(CLI_REPO)/target/aarch64-apple-visionos-sim/release/$(LIB_NAME)" FRAMEWORK_OUTPUT="$(FRAMEWORK_STAGING_DIR)/visionos-sim/$(SLICE_FRAMEWORK_NAME)"
172 - xcodebuild -create-xcframework \
173 - -framework "$(FRAMEWORK_STAGING_DIR)/visionos/$(SLICE_FRAMEWORK_NAME)" \
174 - -framework "$(FRAMEWORK_STAGING_DIR)/visionos-sim/$(SLICE_FRAMEWORK_NAME)" \
175 - -output "$(FRAMEWORK_DIR)"
176 - $(MAKE) activate-local-ffi
177 - @echo "Built $(FRAMEWORK_DIR) for visionOS"
19 + 'Layering:' \
20 + ' AuthCore cross-platform (Apple/Linux/Windows/Android) headless core,' \
21 + ' a native Swift port of smbcloud-cli/crates/smbcloud-auth-sdk.' \
22 + ' SmbCloudAuth Apple UI layer (hosted login + Keychain) on top of AuthCore.' \
23 + ' Both are pure Swift — no Rust/UniFFI artifacts required.'
24
25 verify:
26 swift build
@@ -207,6 +53,5 @@ verify-apple-destination:
53 xcodebuild -quiet -scheme SmbCloudAuth -destination "$${destination}" -derivedDataPath "$${derived_data}" build
54
55 clean:
210 - rm -rf "$(FRAMEWORK_DIR)" "$(LOCAL_DIR)"
211 - rm -f "$(SWIFT_GLUE)"
56 + rm -rf "$(LOCAL_DIR)"
57 @echo "Removed local SmbCloudAuth Swift build artifacts"
Package.swift
+26 -76
@@ -1,31 +1,36 @@
1 // swift-tools-version:6.1
2 -import Foundation
2 import PackageDescription
3
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
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] = {
21 - var targets: [Target] = [
4 +let package = Package(
5 + name: "SmbCloudAuth",
6 + platforms: [
7 + .iOS(.v16), .macOS(.v14), .tvOS(.v16), .visionOS(.v1),
8 + ],
9 + products: [
10 + .library(name: "AuthCore", targets: ["AuthCore"]),
11 + .library(name: "SmbCloudAuth", targets: ["SmbCloudAuth"]),
12 + ],
13 + dependencies: [
14 + .package(
15 + url: "https://github.com/apple/swift-crypto.git",
16 + "3.0.0" ..< "5.0.0"
17 + )
18 + ],
19 + targets: [
20 // Cross-platform core. Builds on Apple platforms, Linux, Windows, and
21 // Android. No UIKit/AppKit, AuthenticationServices, or Keychain.
22 + //
23 + // A faithful native Swift port of the canonical client SDK,
24 + // `smbcloud-cli/crates/smbcloud-auth-sdk` (the crate the `-py`/`-wasm`
25 + // bindings wrap). The Rust crate is the contract authority; AuthCore is
26 + // pinned to it by a shared conformance suite — there is intentionally no
27 + // Rust/UniFFI dependency, so this builds with plain `swift build`
28 + // everywhere, including server-side.
29 .target(
30 name: "AuthCore",
31 dependencies: [
32 // CryptoKit is used on Apple platforms; swift-crypto provides the
28 - // same SHA256 API everywhere else.
33 + // same SHA256 API everywhere else (used for OIDC PKCE).
34 .product(
35 name: "Crypto",
36 package: "swift-crypto",
@@ -41,70 +46,15 @@ let targets: [Target] = {
46 dependencies: ["AuthCore"],
47 path: "Sources/SmbCloudAuth"
48 ),
44 - ]
45 -
46 - if shouldEnableLocalFfi {
47 - targets.append(
48 - .binaryTarget(
49 - name: "smbcloud_authFFI",
50 - path: localFrameworkPath
51 - )
52 - )
53 -
54 - targets.append(
55 - .target(
56 - name: "SmbCloudAuthFFI",
57 - dependencies: [.target(name: "smbcloud_authFFI")],
58 - path: "Sources/SmbCloudAuthFFI"
59 - )
60 - )
61 - }
62 -
63 - targets.append(
49 .testTarget(
50 name: "AuthCoreTests",
51 dependencies: ["AuthCore"],
52 path: "Tests/AuthCoreTests"
68 - )
69 - )
70 -
71 - targets.append(
53 + ),
54 .testTarget(
55 name: "SmbCloudAuthTests",
56 dependencies: ["SmbCloudAuth"],
57 path: "Tests/SmbCloudAuthTests"
76 - )
77 - )
78 -
79 - return targets
80 -}()
81 -
82 -let products: [Product] = {
83 - var products: [Product] = [
84 - .library(name: "AuthCore", targets: ["AuthCore"]),
85 - .library(name: "SmbCloudAuth", targets: ["SmbCloudAuth"]),
58 + ),
59 ]
87 -
88 - if shouldEnableLocalFfi {
89 - products.append(
90 - .library(name: "SmbCloudAuthFFI", targets: ["SmbCloudAuthFFI"])
91 - )
92 - }
93 -
94 - return products
95 -}()
96 -
97 -let package = Package(
98 - name: "SmbCloudAuth",
99 - platforms: [
100 - .iOS(.v16), .macOS(.v14), .tvOS(.v16), .visionOS(.v1),
101 - ],
102 - products: products,
103 - dependencies: [
104 - .package(
105 - url: "https://github.com/apple/swift-crypto.git",
106 - "3.0.0" ..< "5.0.0"
107 - )
108 - ],
109 - targets: targets
60 )
README.md
+68 -98
@@ -1,7 +1,7 @@
1 <h1 align="center">smbCloud Auth for Swift</h1>
2
3 <p align="center">
4 - <strong>Hosted login for Apple apps.</strong>
4 + <strong>Native Swift SDK for smbCloud Auth — server-side and Apple apps.</strong>
5 </p>
6
7 <p align="center">
@@ -18,80 +18,79 @@
18
19 ---
20
21 -`smbcloud-auth-swift` is the Swift package for smbCloud Auth.
21 +`smbcloud-auth-swift` is the Swift SDK for smbCloud Auth. It authenticates a
22 +tenant auth app's end users (**`AuthUser`**) against the smbCloud tenant client
23 +API (`/v1/client/*`).
24
25 It ships **two products**:
26
27 | Product | Platforms | What it's for |
28 |---------|-----------|---------------|
27 -| **`AuthCore`** | Apple · Linux · Windows · Android | Headless, UI-free auth engine |
29 +| **`AuthCore`** | Apple · Linux · Windows · Android | Headless, UI-free auth engine (use server-side too) |
30 | **`SmbCloudAuth`** | Apple only | Hosted login UI + Keychain on top of `AuthCore` |
31
32 `AuthCore` gives you:
33
32 -- `SmbCloudAuthClient` — headless Authorization Code + PKCE engine
33 -- `SmbCloudSession`
34 -- `SmbCloudUserInfo` / `SmbCloudUserInfoClient`
35 -- `SmbCloudCredentialsStore` (protocol) + `SmbCloudInMemoryCredentialsStore`
34 +- `AuthCoreClient` — the tenant credential client: `login`, `signup`, `me`,
35 + `logout`, `remove`, plus the Apple provider helpers. Uses confidential
36 + `ClientCredentials` (`appId` + `appSecret`).
37 +- `OIDC` — the public-client OIDC Authorization Code + PKCE flow
38 + (`buildAuthorizationRequest`, `parseCallbackURL`, `exchangeCode`, `getUserInfo`).
39 +- `SmbCloudSession` and `SmbCloudCredentialsStore` (+ `SmbCloudInMemoryCredentialsStore`).
40 +- `SmbCloudError` / `SmbCloudErrorCode`, `AccountStatus`, `User`, `SignupResult`.
41
42 `SmbCloudAuth` adds the Apple UI/platform layer and re-exports `AuthCore`:
43
39 -- `SmbCloudWebAuth` — `ASWebAuthenticationSession` hosted login
44 +- `SmbCloudWebAuth` — `ASWebAuthenticationSession` hosted login over the OIDC flow
45 - `SmbCloudCredentialsManager` — Keychain-backed `SmbCloudCredentialsStore`
46
42 -The package is built for public clients:
47 +### Confidential vs public clients
48
44 -- Authorization Code + PKCE
45 -- `ASWebAuthenticationSession` (Apple) or your own user agent (everywhere else)
46 -- pluggable session storage (Keychain on Apple, your choice elsewhere)
47 -- no shipped client secret
49 +- **Confidential** (`AuthCoreClient`): `appId` + `appSecret`. Use it where the
50 + secret stays private — a backend / BFF, the CLI, server-side Swift. The
51 + email/password `login`/`signup` calls require it.
52 +- **Public** (`SmbCloudWebAuth` / `OIDC`): no secret. This is the path for
53 + native and browser apps. **Never ship `appSecret` in a distributed app.**
54
49 -## Status
55 +### Relationship to the Rust SDK
56
51 -Developer preview. The hosted-login MVP is in place and ready to try.
57 +The contract authority is the Rust crate
58 +[`smbcloud-cli/crates/smbcloud-auth-sdk`](https://github.com/smbcloudXYZ/smbcloud-cli),
59 +which the `-py` and `-wasm` bindings wrap. `AuthCore` is a faithful **native**
60 +Swift port of it (no Rust/UniFFI dependency, so it builds with plain
61 +`swift build` everywhere, including Linux servers), pinned to the crate by a
62 +shared conformance suite. Apple consumers use this package directly — there is
63 +no UniFFI Apple binding. See
64 +[`Docs/AuthCore-SDK-Surface.md`](Docs/AuthCore-SDK-Surface.md).
65
66 ## Installation
67
55 -### Xcode
56 -
57 -Use **File → Add Package Dependencies** and enter:
58 -
59 -```text
60 -https://github.com/smbcloudXYZ/smbcloud-auth-swift
61 -```
62 -
68 ### Swift Package Manager
69
70 ```swift
71 dependencies: [
67 - .package(url: "https://github.com/smbcloudXYZ/smbcloud-auth-swift", from: "1.0.1")
72 + .package(url: "https://github.com/smbcloudXYZ/smbcloud-auth-swift", from: "1.0.0")
73 ],
74 targets: [
75 + // Apple app: the UI layer.
76 .target(name: "MyApp", dependencies: [
77 .product(name: "SmbCloudAuth", package: "smbcloud-auth-swift")
72 - ])
73 -]
74 -```
75 -
76 -For the cross-platform (Linux/Windows/Android) core, depend on `AuthCore`
77 -instead:
78 -
79 -```swift
80 -targets: [
81 - .target(name: "MyApp", dependencies: [
78 + ]),
79 + // Server / cross-platform: the headless engine.
80 + .target(name: "MyServer", dependencies: [
81 .product(name: "AuthCore", package: "smbcloud-auth-swift")
82 ])
83 ]
84 ```
85
87 -## Quick example (Apple)
86 +## Quick example (Apple, public client)
87
88 ```swift
89 import SmbCloudAuth
90
92 -let webAuth = try SmbCloudWebAuth(
93 - domain: "api.smbcloud.xyz",
94 - clientId: "your-public-client-id",
91 +let webAuth = SmbCloudWebAuth(
92 + environment: .production,
93 + oidcClientId: "your-oidc-client-id",
94 redirectURL: URL(string: "myapp://auth/callback")!
95 )
96
@@ -100,9 +99,7 @@ let credentials = SmbCloudCredentialsManager(
99 )
100
101 let session = try await webAuth.login(
103 - presentationAnchorProvider: {
104 - window
105 - },
102 + presentationAnchorProvider: { window },
103 credentialsManager: credentials
104 )
105
@@ -110,35 +107,34 @@ let user = try await webAuth.userInfo(session: session)
107 print(user.email ?? "Signed in")
108 ```
109
113 -## Quick example (cross-platform / headless)
110 +## Quick example (server-side / headless, confidential client)
111
115 -On Linux, Windows, Android, servers, or any place without
116 -`ASWebAuthenticationSession`, drive the user agent yourself and feed the
117 -callback URL back into `AuthCore`:
112 +On a backend (e.g. a Vapor service) you hold the `appSecret` and can use the
113 +tenant credential flow directly:
114
115 ```swift
116 import AuthCore
117
122 -let client = try SmbCloudAuthClient(
123 - domain: "api.smbcloud.xyz",
124 - clientId: "your-public-client-id",
125 - redirectURL: URL(string: "myapp://auth/callback")!
118 +let client = AuthCoreClient(
119 + environment: .production,
120 + credentials: ClientCredentials(appId: appId, appSecret: appSecret)
121 )
122
128 -// 1. Build the PKCE request and open `request.authorizeURL` in a browser.
129 -let request = try client.authorizationRequest()
130 -
131 -// 2. After the redirect, hand the callback URL back to AuthCore.
132 -let session = try await client.exchangeCallback(callbackURL, authorizationRequest: request)
133 -
134 -// 3. Persist however you like (e.g. SmbCloudInMemoryCredentialsStore or your own store).
135 -let store = SmbCloudInMemoryCredentialsStore()
136 -try store.store(session)
137 -
138 -let user = try await client.userInfo(session: session)
139 -print(user.email ?? "Signed in")
123 +switch try await client.login(username: email, password: password) {
124 +case .ready(let accessToken):
125 + let user = try await client.me(accessToken: accessToken) // GET /v1/client/me
126 + print(user.email)
127 +case .incomplete(let status):
128 + print("Account not ready: \(status)") // e.g. .emailUnverified
129 +case .notFound:
130 + print("No such account")
131 +}
132 ```
133
134 +`AuthCoreClient.login` returns the access token inside `.ready(accessToken:)` as
135 +the full `Authorization` header value (it already includes `Bearer `); pass it
136 +back to `me`/`logout`/`remove` verbatim.
137 +
138 ## Platforms
139
140 `SmbCloudAuth` (hosted-login UI + Keychain):
@@ -150,46 +146,20 @@ print(user.email ?? "Signed in")
146 | tvOS | 16.0 |
147 | visionOS | 1.0 |
148
153 -Hosted web login (`SmbCloudWebAuth`) runs on iOS, macOS, and visionOS. The session, storage, and user info helpers can still be used on other supported Apple platforms.
154 -
155 -`AuthCore` (headless engine) additionally builds on **Linux, Windows, and Android**. It has no dependency on UIKit/AppKit, `AuthenticationServices`, or the Keychain, and uses [swift-crypto](https://github.com/apple/swift-crypto) for PKCE off Apple platforms. Bring your own `SmbCloudCredentialsStore` (or use `SmbCloudInMemoryCredentialsStore`) for persistence.
149 +`SmbCloudWebAuth` runs on iOS, macOS, and visionOS. `AuthCore` additionally
150 +builds on **Linux, Windows, and Android**, with no dependency on UIKit/AppKit,
151 +`AuthenticationServices`, or the Keychain (it uses
152 +[swift-crypto](https://github.com/apple/swift-crypto) for OIDC PKCE off Apple
153 +platforms). Bring your own `SmbCloudCredentialsStore` (or use
154 +`SmbCloudInMemoryCredentialsStore`) for persistence.
155
157 -## Security note
156 +## Security notes
157
159 -- Use a public client ID.
160 -- Do not embed an smbCloud Auth `app_secret` in the app.
158 +- Native and browser apps are **public clients**: use `SmbCloudWebAuth` / `OIDC`,
159 + not a shipped `appSecret`.
160 +- Keep `appSecret` and email/password (`AuthCoreClient`) on a backend / BFF.
161 - Use system browser auth, not embedded webviews.
162 -- If you need native email/password forms, keep confidential credentials on your backend or BFF.
163 -- `logout()` currently clears local credentials only.
164 -
165 -## Guides
166 -
167 -- [Quick Start — iOS](Docs/QuickStart-iOS.md)
168 -- [Quick Start — macOS](Docs/QuickStart-macOS.md)
169 -- [Migration from proxy/native-form auth](Docs/Migration-From-Proxy-Native-Forms.md)
170 -- [Hosted login example](Examples/HostedLoginExample/README.md)
171 -- [Release process](Docs/Release.md)
172 -
173 -## Local development
174 -
175 -Clone this repo alongside `smbcloud-cli`:
176 -
177 -```text
178 -Repositories/
179 -├── smbcloud-cli/
180 -└── smbcloud-auth-swift/
181 -```
182 -
183 -Build the optional local XCFramework and UniFFI shim from the Rust source:
184 -
185 -```bash
186 -make ios
187 -make macos
188 -make tvos
189 -make visionos
190 -```
191 -
192 -The public `SmbCloudAuth` product builds without local Rust artifacts. `SmbCloudAuthFFI` is there for sibling-repo development when you need the local bridge.
162 +- `logout()` clears local credentials.
163
164 ## License
165
Sources/AuthCore/AuthCoreClient.swift new
+250
@@ -0,0 +1,250 @@
1 +import Foundation
2 +
3 +#if canImport(FoundationNetworking)
4 + import FoundationNetworking
5 +#endif
6 +
7 +/// Headless smbCloud Auth client for a tenant auth app.
8 +///
9 +/// A faithful Swift port of the canonical `smbcloud-auth-sdk` tenant client
10 +/// surface (`login`, `signup`, `me`, `logout`, `remove`, and the Apple provider
11 +/// helpers). It performs no UI and has no dependency on AuthenticationServices
12 +/// or the Keychain, so it builds on Apple platforms, Linux, Windows, and
13 +/// Android — including inside a server.
14 +///
15 +/// This client uses confidential ``ClientCredentials`` (`appId` + `appSecret`).
16 +/// Construct and use it where the secret is safe (a backend / BFF). For native
17 +/// or browser public clients, use ``OIDC`` or the Apple flow, or call your own
18 +/// backend.
19 +public struct AuthCoreClient: Sendable {
20 + public let environment: SmbCloudEnvironment
21 + public let credentials: ClientCredentials
22 + private let http: HTTPTransport
23 +
24 + public init(environment: SmbCloudEnvironment, credentials: ClientCredentials) {
25 + self.environment = environment
26 + self.credentials = credentials
27 + self.http = HTTPTransport()
28 + }
29 +
30 + init(
31 + environment: SmbCloudEnvironment,
32 + credentials: ClientCredentials,
33 + session: URLSession
34 + ) {
35 + self.environment = environment
36 + self.credentials = credentials
37 + self.http = HTTPTransport(session: session)
38 + }
39 +
40 + // MARK: - Email / password (tenant credential flow)
41 +
42 + /// `POST /v1/client/users/sign_in` — mirrors `login_with_client`.
43 + public func login(username: String, password: String) async throws -> AccountStatus {
44 + let url = credentials.tenantURL(environment: environment, path: "v1/client/users/sign_in")
45 + var request = URLRequest(url: url)
46 + request.httpMethod = "POST"
47 + request.setValue("application/json", forHTTPHeaderField: "Content-Type")
48 + request.setValue("application/json", forHTTPHeaderField: "Accept")
49 + request.setValue(credentials.appId, forHTTPHeaderField: "User-agent")
50 + request.httpBody = try JSONEncoder().encode(
51 + UserEnvelope(user: .init(email: username, password: password))
52 + )
53 + return try await http.requestLogin(request)
54 + }
55 +
56 + /// `POST /v1/client/users` — mirrors `signup_with_client`.
57 + public func signup(email: String, password: String) async throws -> SignupResult {
58 + let url = credentials.tenantURL(environment: environment, path: "v1/client/users")
59 + var request = URLRequest(url: url)
60 + request.httpMethod = "POST"
61 + request.setValue("application/json", forHTTPHeaderField: "Content-Type")
62 + request.setValue("application/json", forHTTPHeaderField: "Accept")
63 + request.setValue(credentials.appId, forHTTPHeaderField: "User-agent")
64 + request.httpBody = try JSONEncoder().encode(
65 + UserEnvelope(user: .init(email: email, password: password))
66 + )
67 + return try await http.requestJSON(request, as: SignupResult.self)
68 + }
69 +
70 + /// `GET /v1/client/me` — mirrors `me_with_client`.
71 + ///
72 + /// `accessToken` is the value returned in ``AccountStatus/ready(accessToken:)``
73 + /// and is sent verbatim as the `Authorization` header (it already carries
74 + /// the `Bearer ` prefix).
75 + public func me(accessToken: String) async throws -> User {
76 + let url = credentials.tenantURL(environment: environment, path: "v1/client/me")
77 + var request = URLRequest(url: url)
78 + request.httpMethod = "GET"
79 + request.setValue(accessToken, forHTTPHeaderField: "Authorization")
80 + request.setValue("application/json", forHTTPHeaderField: "Accept")
81 + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
82 + return try await http.requestJSON(request, as: User.self)
83 + }
84 +
85 + /// `DELETE /v1/client/users/sign_out` — mirrors `logout_with_client`.
86 + public func logout(accessToken: String) async throws {
87 + let url = credentials.tenantURL(environment: environment, path: "v1/client/users/sign_out")
88 + var request = URLRequest(url: url)
89 + request.httpMethod = "DELETE"
90 + request.setValue(accessToken, forHTTPHeaderField: "Authorization")
91 + request.setValue("application/json", forHTTPHeaderField: "Accept")
92 + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
93 + do {
94 + try await http.requestVoid(request)
95 + } catch {
96 + // Mirror the Rust SDK: any non-OK is surfaced as unauthorized.
97 + throw SmbCloudError(code: .unauthorized)
98 + }
99 + }
100 +
101 + /// `DELETE /v1/client/me` — mirrors `remove_with_client`.
102 + public func remove(accessToken: String) async throws {
103 + let url = credentials.tenantURL(environment: environment, path: "v1/client/me")
104 + var request = URLRequest(url: url)
105 + request.httpMethod = "DELETE"
106 + request.setValue(accessToken, forHTTPHeaderField: "Authorization")
107 + request.setValue(credentials.appId, forHTTPHeaderField: "User-agent")
108 + try await http.requestVoid(request)
109 + }
110 +
111 + // MARK: - Account recovery
112 + //
113 + // NOTE: these wrap backend endpoints that the canonical `smbcloud-auth-sdk`
114 + // crate does not expose yet (only the CLI-internal `smbcloud-auth` crate
115 + // does). They're faithful to the backend contract; upstream them to
116 + // `smbcloud-auth-sdk` to restore full cross-binding parity. Each returns the
117 + // server's user-facing message.
118 +
119 + /// `POST /v1/client/users/reset_password` — sends reset instructions.
120 + @discardableResult
121 + public func requestPasswordReset(email: String) async throws -> String {
122 + try await postMessage(path: "v1/client/users/reset_password", body: EmailEnvelope(user: .init(email: email)))
123 + }
124 +
125 + /// `POST /v1/client/users/reset_password/complete` — sets a new password
126 + /// using the token from the reset email.
127 + @discardableResult
128 + public func completePasswordReset(
129 + token: String,
130 + password: String,
131 + passwordConfirmation: String? = nil
132 + ) async throws -> String {
133 + let body = ResetPasswordComplete(
134 + resetPasswordToken: token,
135 + password: password,
136 + passwordConfirmation: passwordConfirmation ?? password
137 + )
138 + return try await postMessage(path: "v1/client/users/reset_password/complete", body: body)
139 + }
140 +
141 + /// `POST /v1/client/users/resend_confirmation` — re-sends the confirmation email.
142 + @discardableResult
143 + public func resendConfirmation(email: String) async throws -> String {
144 + try await postMessage(path: "v1/client/users/resend_confirmation", body: EmailEnvelope(user: .init(email: email)))
145 + }
146 +
147 + private func postMessage<Body: Encodable>(path: String, body: Body) async throws -> String {
148 + let url = credentials.tenantURL(environment: environment, path: path)
149 + var request = URLRequest(url: url)
150 + request.httpMethod = "POST"
151 + request.setValue("application/json", forHTTPHeaderField: "Content-Type")
152 + request.setValue("application/json", forHTTPHeaderField: "Accept")
153 + request.setValue(credentials.appId, forHTTPHeaderField: "User-agent")
154 + request.httpBody = try JSONEncoder().encode(body)
155 + return try await http.requestJSON(request, as: MessageResponse.self).message
156 + }
157 +
158 + // MARK: - Apple provider (tenant)
159 +
160 + /// Builds the tenant Apple authorization request — mirrors
161 + /// `apple::build_authorization_request_with_client`.
162 + public func buildAppleAuthorizationRequest(
163 + redirectURI: String,
164 + state: String? = nil
165 + ) -> AppleAuthorizationRequest {
166 + let resolvedState = state ?? UUID().uuidString
167 + let url = credentials.tenantURL(
168 + environment: environment,
169 + path: "v1/client/oauth/apple/authorize",
170 + extraQuery: [
171 + URLQueryItem(name: "redirect_uri", value: redirectURI),
172 + URLQueryItem(name: "state", value: resolvedState),
173 + ]
174 + )
175 + return AppleAuthorizationRequest(
176 + authorizeURL: url,
177 + redirectURI: redirectURI,
178 + state: resolvedState
179 + )
180 + }
181 +
182 + /// Parses an Apple callback URL into a session — mirrors `apple::parse_callback_url`.
183 + public func parseAppleCallbackURL(
184 + _ callbackURL: String,
185 + expectedState: String? = nil
186 + ) throws -> AppleAuthSession {
187 + guard let components = URLComponents(string: callbackURL) else {
188 + throw SmbCloudError(code: .parseError, message: "Invalid Apple callback URL.")
189 + }
190 + let items = components.queryItems ?? []
191 + func value(_ name: String) -> String? {
192 + items.first { $0.name == name }?.value
193 + }
194 +
195 + if let error = value("error") {
196 + throw SmbCloudError(code: .invalidParams, message: error)
197 + }
198 + let state = value("state")
199 + if let expectedState, state != expectedState {
200 + throw SmbCloudError(code: .invalidParams, message: "Apple callback state mismatch.")
201 + }
202 + guard let accessToken = value("access_token") else {
203 + throw SmbCloudError(code: .invalidParams, message: "Missing `access_token` in Apple callback URL.")
204 + }
205 + guard let providerAccountId = value("provider_account_id") else {
206 + throw SmbCloudError(code: .invalidParams, message: "Missing `provider_account_id` in Apple callback URL.")
207 + }
208 + return AppleAuthSession(
209 + accessToken: accessToken,
210 + refreshToken: value("refresh_token"),
211 + email: value("email"),
212 + name: value("name"),
213 + provider: value("provider") ?? "apple",
214 + providerAccountId: providerAccountId,
215 + state: state
216 + )
217 + }
218 +}
219 +
220 +// MARK: - Request bodies
221 +
222 +private struct UserEnvelope: Encodable {
223 + struct Credentials: Encodable {
224 + let email: String
225 + let password: String
226 + }
227 + let user: Credentials
228 +}
229 +
230 +private struct EmailEnvelope: Encodable {
231 + struct EmailOnly: Encodable { let email: String }
232 + let user: EmailOnly
233 +}
234 +
235 +private struct ResetPasswordComplete: Encodable {
236 + let resetPasswordToken: String
237 + let password: String
238 + let passwordConfirmation: String
239 +
240 + enum CodingKeys: String, CodingKey {
241 + case resetPasswordToken = "reset_password_token"
242 + case password
243 + case passwordConfirmation = "password_confirmation"
244 + }
245 +}
246 +
247 +/// `{ "message": "…" }` — the recovery endpoints' response envelope.
248 +private struct MessageResponse: Decodable {
249 + let message: String
250 +}
Sources/AuthCore/ClientCredentials.swift new
+46
@@ -0,0 +1,46 @@
1 +import Foundation
2 +
3 +/// A tenant auth app's credentials.
4 +///
5 +/// Mirrors `client_credentials::ClientCredentials`. On every `/v1/client/*`
6 +/// request these are sent as the `client_id` and `client_secret` **query
7 +/// parameters** (see `tenantURL(path:)`).
8 +///
9 +/// The `appSecret` is confidential: hold it server-side (a Backend-for-Frontend
10 +/// such as a Vapor service). Do not ship it in a distributed client binary or
11 +/// browser bundle — native/public clients should use the OIDC or Apple flows,
12 +/// or route through a backend.
13 +public struct ClientCredentials: Sendable, Equatable {
14 + public let appId: String
15 + public let appSecret: String
16 +
17 + public init(appId: String, appSecret: String) {
18 + self.appId = appId
19 + self.appSecret = appSecret
20 + }
21 +}
22 +
23 +extension ClientCredentials {
24 + /// Builds `<scheme>://<host>/<path>?client_id=…&client_secret=…`, plus any
25 + /// extra query items, matching the Rust SDK's `base_url_builder`.
26 + func tenantURL(
27 + environment: SmbCloudEnvironment,
28 + path: String,
29 + extraQuery: [URLQueryItem] = []
30 + ) -> URL {
31 + var components = URLComponents()
32 + components.scheme = environment.apiProtocol
33 + // host may carry a port (e.g. "localhost:8088"); URLComponents.host
34 + // can't hold the port, so set them via the full string instead.
35 + let hostAndPort = environment.apiHost.split(separator: ":", maxSplits: 1)
36 + components.host = String(hostAndPort[0])
37 + if hostAndPort.count == 2 { components.port = Int(hostAndPort[1]) }
38 + components.path = "/" + path
39 + components.queryItems =
40 + [
41 + URLQueryItem(name: "client_id", value: appId),
42 + URLQueryItem(name: "client_secret", value: appSecret),
43 + ] + extraQuery
44 + return components.url!
45 + }
46 +}
Sources/AuthCore/Models.swift new
+78
@@ -0,0 +1,78 @@
1 +import Foundation
2 +
3 +/// Result of ``AuthCoreClient/login(username:password:)``.
4 +///
5 +/// Mirrors `smbcloud_model::login::AccountStatus`, as mapped from the HTTP
6 +/// response by the Rust SDK's `request_login`.
7 +public enum AccountStatus: Sendable, Equatable {
8 + /// No account matches the supplied email.
9 + case notFound
10 + /// Logged in. `accessToken` is the full `Authorization` header value — it
11 + /// already includes the `Bearer ` prefix and is passed back verbatim to
12 + /// ``AuthCoreClient/me(accessToken:)`` and friends.
13 + case ready(accessToken: String)
14 + /// The account exists but isn't usable yet (e.g. email unverified).
15 + case incomplete(status: AccountErrorCode)
16 +}
17 +
18 +/// A tenant auth app end user, as returned by `GET /v1/client/me`.
19 +///
20 +/// Mirrors `smbcloud_model::account::User` — the canonical SDK decodes only
21 +/// these fields even though the backend payload carries more.
22 +public struct User: Codable, Sendable, Equatable {
23 + public let id: Int
24 + public let email: String
25 + public let createdAt: Date
26 + public let updatedAt: Date
27 +
28 + enum CodingKeys: String, CodingKey {
29 + case id, email
30 + case createdAt = "created_at"
31 + case updatedAt = "updated_at"
32 + }
33 +}
34 +
35 +/// Result of ``AuthCoreClient/signup(email:password:)``.
36 +///
37 +/// Mirrors `smbcloud_model::signup::SignupResult`.
38 +public struct SignupResult: Codable, Sendable, Equatable {
39 + public let code: Int?
40 + public let message: String
41 + public let data: AccountData?
42 +}
43 +
44 +/// Mirrors `smbcloud_model::account::Data`.
45 +public struct AccountData: Codable, Sendable, Equatable {
46 + public let id: Int
47 + public let email: String
48 + public let createdAt: String
49 +
50 + enum CodingKeys: String, CodingKey {
51 + case id, email
52 + case createdAt = "created_at"
53 + }
54 +}
55 +
56 +// MARK: - Apple provider (tenant)
57 +
58 +/// A prepared Apple sign-in authorization request (tenant plane).
59 +///
60 +/// Mirrors `apple::AppleAuthorizationRequest`.
61 +public struct AppleAuthorizationRequest: Sendable, Equatable {
62 + public let authorizeURL: URL
63 + public let redirectURI: String
64 + public let state: String
65 +}
66 +
67 +/// The tenant auth session parsed from an Apple callback URL.
68 +///
69 +/// Mirrors `apple::AppleAuthSession`.
70 +public struct AppleAuthSession: Sendable, Equatable {
71 + public let accessToken: String
72 + public let refreshToken: String?
73 + public let email: String?
74 + public let name: String?
75 + public let provider: String
76 + public let providerAccountId: String
77 + public let state: String?
78 +}
Sources/AuthCore/Networking.swift new
+156
@@ -0,0 +1,156 @@
1 +import Foundation
2 +
3 +#if canImport(FoundationNetworking)
4 + // URLSession lives in FoundationNetworking on non-Apple Swift toolchains.
5 + import FoundationNetworking
6 +#endif
7 +
8 +/// Thin async HTTP layer shared by ``AuthCoreClient`` and ``OIDC``.
9 +///
10 +/// Faithfully reproduces the response handling of the Rust SDK's
11 +/// `smbcloud_network::network::{request, request_login}`: 2xx decodes the
12 +/// payload, anything else is mapped to a ``SmbCloudError``.
13 +struct HTTPTransport: Sendable {
14 + let session: URLSession
15 +
16 + init(session: URLSession = .shared) {
17 + self.session = session
18 + }
19 +
20 + private func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
21 + let data: Data
22 + let response: URLResponse
23 + do {
24 + (data, response) = try await session.data(for: request)
25 + } catch {
26 + throw SmbCloudError(code: .networkError, message: error.localizedDescription)
27 + }
28 + guard let http = response as? HTTPURLResponse else {
29 + throw SmbCloudError(code: .networkError)
30 + }
31 + return (data, http)
32 + }
33 +
34 + /// Decodes a 2xx JSON body as `T`; otherwise throws the parsed API error.
35 + func requestJSON<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
36 + let (data, http) = try await send(request)
37 + guard (200..<300).contains(http.statusCode) else {
38 + throw Self.apiError(from: data, statusCode: http.statusCode)
39 + }
40 + do {
41 + return try smbCloudDecoder.decode(T.self, from: data)
42 + } catch {
43 + throw SmbCloudError(code: .parseError, message: error.localizedDescription)
44 + }
45 + }
46 +
47 + /// Succeeds on any 2xx; otherwise throws the parsed API error.
48 + func requestVoid(_ request: URLRequest) async throws {
49 + let (data, http) = try await send(request)
50 + guard (200..<300).contains(http.statusCode) else {
51 + throw Self.apiError(from: data, statusCode: http.statusCode)
52 + }
53 + }
54 +
55 + /// Mirrors `request_login`: maps the sign-in response to an ``AccountStatus``.
56 + func requestLogin(_ request: URLRequest) async throws -> AccountStatus {
57 + let (data, http) = try await send(request)
58 +
59 + switch http.statusCode {
60 + case 200:
61 + if let auth = http.value(forHTTPHeaderField: "Authorization"), !auth.isEmpty {
62 + return .ready(accessToken: auth)
63 + }
64 + // 200 without a token: parse a status code from the body if present.
65 + if let parsed = try? smbCloudDecoder.decode(APIErrorBody.self, from: data) {
66 + switch parsed.errorCode {
67 + case SmbCloudErrorCode.emailNotVerified.rawValue:
68 + return .incomplete(status: .emailUnverified)
69 + case SmbCloudErrorCode.passwordNotSet.rawValue:
70 + return .incomplete(status: .passwordNotSet)
71 + default:
72 + return .notFound
73 + }
74 + }
75 + return .notFound
76 + case 404:
77 + return .notFound
78 + case 422:
79 + if let parsed = try? smbCloudDecoder.decode(APIErrorBody.self, from: data),
80 + let code = parsed.errorCode,
81 + let account = AccountErrorCode(rawValue: code)
82 + {
83 + return .incomplete(status: account)
84 + }
85 + throw Self.apiError(from: data, statusCode: 422)
86 + default:
87 + throw Self.apiError(from: data, statusCode: http.statusCode)
88 + }
89 + }
90 +
91 + /// Builds a ``SmbCloudError`` from a non-2xx response body.
92 + static func apiError(from data: Data, statusCode: Int) -> SmbCloudError {
93 + if let parsed = try? smbCloudDecoder.decode(APIErrorBody.self, from: data) {
94 + let code = parsed.errorCode.flatMap(SmbCloudErrorCode.init(rawValue:))
95 + ?? statusError(statusCode)
96 + return SmbCloudError(code: code, message: parsed.message ?? code.defaultMessage)
97 + }
98 + let code = statusError(statusCode)
99 + let body = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
100 + return SmbCloudError(code: code, message: (body?.isEmpty == false ? body : nil) ?? code.defaultMessage)
101 + }
102 +
103 + private static func statusError(_ statusCode: Int) -> SmbCloudErrorCode {
104 + switch statusCode {
105 + case 401: return .unauthorized
106 + case 404: return .emailNotFound
107 + case 400, 422: return .invalidParams
108 + default: return .networkError
109 + }
110 + }
111 +}
112 +
113 +/// `{ "error_code": <int>, "message": "…" }` — the API's error envelope.
114 +private struct APIErrorBody: Decodable {
115 + let errorCode: Int?
116 + let message: String?
117 +
118 + enum CodingKeys: String, CodingKey {
119 + case errorCode = "error_code"
120 + case message
121 + }
122 +}
123 +
124 +/// Shared decoder: snake_case is handled per-type via explicit `CodingKeys`,
125 +/// and dates use a tolerant ISO-8601 parser (with/without fractional seconds).
126 +let smbCloudDecoder: JSONDecoder = {
127 + let decoder = JSONDecoder()
128 + decoder.dateDecodingStrategy = .custom { decoder in
129 + let container = try decoder.singleValueContainer()
130 + let raw = try container.decode(String.self)
131 + if let date = SmbCloudDateParser.date(from: raw) {
132 + return date
133 + }
134 + throw DecodingError.dataCorruptedError(
135 + in: container, debugDescription: "Unrecognized date format: \(raw)"
136 + )
137 + }
138 + return decoder
139 +}()
140 +
141 +enum SmbCloudDateParser {
142 + nonisolated(unsafe) private static let withFraction: ISO8601DateFormatter = {
143 + let f = ISO8601DateFormatter()
144 + f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
145 + return f
146 + }()
147 + nonisolated(unsafe) private static let plain: ISO8601DateFormatter = {
148 + let f = ISO8601DateFormatter()
149 + f.formatOptions = [.withInternetDateTime]
150 + return f
151 + }()
152 +
153 + static func date(from string: String) -> Date? {
154 + withFraction.date(from: string) ?? plain.date(from: string)
155 + }
156 +}
Sources/AuthCore/OIDC.swift new
+179
@@ -0,0 +1,179 @@
1 +import Foundation
2 +
3 +#if canImport(FoundationNetworking)
4 + import FoundationNetworking
5 +#endif
6 +
7 +#if canImport(CryptoKit)
8 + import CryptoKit
9 +#else
10 + import Crypto
11 +#endif
12 +
13 +/// OpenID Connect Authorization Code + PKCE flow (public client).
14 +///
15 +/// A faithful port of the canonical SDK's `oidc.rs`. Unlike the tenant
16 +/// credential flow, this is a **public client**: it uses only an
17 +/// `oidcClientId` — no `app_secret` — so it's appropriate for native and
18 +/// browser apps. Drive the user agent yourself (or via `SmbCloudWebAuth` on
19 +/// Apple) and feed the callback URL back into ``parseCallbackURL(_:)``.
20 +public enum OIDC {
21 + /// A prepared OIDC authorization request. Open ``authorizeURL`` in a browser.
22 + public struct AuthorizationRequest: Sendable, Equatable {
23 + public let authorizeURL: URL
24 + public let redirectURI: String
25 + public let state: String
26 + public let codeVerifier: String
27 + }
28 +
29 + /// The `code` + `state` extracted from a redirect callback.
30 + public struct CallbackPayload: Sendable, Equatable {
31 + public let code: String
32 + public let state: String
33 + }
34 +
35 + /// The `oauth/token` response.
36 + public struct TokenResponse: Codable, Sendable, Equatable {
37 + public let accessToken: String
38 + public let tokenType: String
39 + public let expiresIn: Int?
40 + public let refreshToken: String?
41 + public let scope: String?
42 + public let idToken: String?
43 +
44 + enum CodingKeys: String, CodingKey {
45 + case accessToken = "access_token"
46 + case tokenType = "token_type"
47 + case expiresIn = "expires_in"
48 + case refreshToken = "refresh_token"
49 + case scope
50 + case idToken = "id_token"
51 + }
52 + }
53 +
54 + /// The `oauth/userinfo` response.
55 + public struct UserInfo: Codable, Sendable, Equatable {
56 + public let sub: String
57 + public let email: String?
58 + public let emailVerified: Bool?
59 + public let tenantId: UInt64?
60 + public let tenantSlug: String?
61 +
62 + enum CodingKeys: String, CodingKey {
63 + case sub, email
64 + case emailVerified = "email_verified"
65 + case tenantId = "tenant_id"
66 + case tenantSlug = "tenant_slug"
67 + }
68 + }
69 +
70 + static let scopes = "openid profile email"
71 +
72 + /// Mirrors `oidc::build_authorization_request`.
73 + public static func buildAuthorizationRequest(
74 + environment: SmbCloudEnvironment,
75 + oidcClientId: String,
76 + redirectURI: String
77 + ) throws -> AuthorizationRequest {
78 + let codeVerifier = UUID().uuidString.replacingOccurrences(of: "-", with: "")
79 + + UUID().uuidString.replacingOccurrences(of: "-", with: "")
80 + let codeChallenge = base64URLNoPad(Data(SHA256.hash(data: Data(codeVerifier.utf8))))
81 + let state = UUID().uuidString
82 +
83 + guard
84 + var components = URLComponents(
85 + url: environment.baseURL.appendingPathComponent("oauth/authorize"),
86 + resolvingAgainstBaseURL: false
87 + )
88 + else {
89 + throw SmbCloudError(code: .parseError, message: "Failed to build the authorize URL.")
90 + }
91 + components.queryItems = [
92 + URLQueryItem(name: "client_id", value: oidcClientId),
93 + URLQueryItem(name: "redirect_uri", value: redirectURI),
94 + URLQueryItem(name: "response_type", value: "code"),
95 + URLQueryItem(name: "scope", value: scopes),
96 + URLQueryItem(name: "state", value: state),
97 + URLQueryItem(name: "code_challenge", value: codeChallenge),
98 + URLQueryItem(name: "code_challenge_method", value: "S256"),
99 + ]
100 + guard let url = components.url else {
101 + throw SmbCloudError(code: .parseError, message: "Failed to build the authorize URL.")
102 + }
103 + return AuthorizationRequest(
104 + authorizeURL: url,
105 + redirectURI: redirectURI,
106 + state: state,
107 + codeVerifier: codeVerifier
108 + )
109 + }
110 +
111 + /// Mirrors `oidc::parse_callback_url`.
112 + public static func parseCallbackURL(_ callbackURL: String) throws -> CallbackPayload {
113 + guard let components = URLComponents(string: callbackURL) else {
114 + throw SmbCloudError(code: .parseError, message: "The callback URL was not valid.")
115 + }
116 + let items = components.queryItems ?? []
117 + let code = items.first { $0.name == "code" }?.value
118 + let state = items.first { $0.name == "state" }?.value
119 + guard let code, let state else {
120 + throw SmbCloudError(code: .invalidParams, message: "Missing authorization code or state.")
121 + }
122 + return CallbackPayload(code: code, state: state)
123 + }
124 +
125 + /// Mirrors `oidc::exchange_code`.
126 + public static func exchangeCode(
127 + environment: SmbCloudEnvironment,
128 + oidcClientId: String,
129 + redirectURI: String,
130 + code: String,
131 + codeVerifier: String,
132 + session: URLSession = .shared
133 + ) async throws -> TokenResponse {
134 + var request = URLRequest(url: environment.baseURL.appendingPathComponent("oauth/token"))
135 + request.httpMethod = "POST"
136 + request.setValue("application/json", forHTTPHeaderField: "Accept")
137 + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
138 + request.httpBody = formEncoded([
139 + ("grant_type", "authorization_code"),
140 + ("client_id", oidcClientId),
141 + ("code", code),
142 + ("redirect_uri", redirectURI),
143 + ("code_verifier", codeVerifier),
144 + ])
145 + return try await HTTPTransport(session: session).requestJSON(request, as: TokenResponse.self)
146 + }
147 +
148 + /// Mirrors `oidc::get_userinfo`.
149 + public static func getUserInfo(
150 + environment: SmbCloudEnvironment,
151 + accessToken: String,
152 + tenantId: String? = nil,
153 + session: URLSession = .shared
154 + ) async throws -> UserInfo {
155 + var request = URLRequest(url: environment.baseURL.appendingPathComponent("oauth/userinfo"))
156 + request.httpMethod = "GET"
157 + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
158 + request.setValue("application/json", forHTTPHeaderField: "Accept")
159 + if let tenantId {
160 + request.setValue(tenantId, forHTTPHeaderField: "X-Smbcloud-Tenant-Id")
161 + }
162 + return try await HTTPTransport(session: session).requestJSON(request, as: UserInfo.self)
163 + }
164 +
165 + // MARK: - Helpers
166 +
167 + private static func base64URLNoPad(_ data: Data) -> String {
168 + data.base64EncodedString()
169 + .replacingOccurrences(of: "+", with: "-")
170 + .replacingOccurrences(of: "/", with: "_")
171 + .replacingOccurrences(of: "=", with: "")
172 + }
173 +
174 + private static func formEncoded(_ items: [(String, String)]) -> Data? {
175 + var components = URLComponents()
176 + components.queryItems = items.map { URLQueryItem(name: $0.0, value: $0.1) }
177 + return components.percentEncodedQuery?.data(using: .utf8)
178 + }
179 +}
Sources/AuthCore/SmbCloudAuthClient.swift deleted
-168
@@ -1,168 +0,0 @@
1 -import Foundation
2 -
3 -#if canImport(FoundationNetworking)
4 - import FoundationNetworking
5 -#endif
6 -
7 -/// A prepared OAuth 2.0 Authorization Code + PKCE request.
8 -///
9 -/// Hold on to this value for the lifetime of a single login attempt. After the
10 -/// user agent redirects back to your `redirectURL`, pass the callback URL and
11 -/// this request to ``SmbCloudAuthClient/exchangeCallback(_:authorizationRequest:)``.
12 -public struct SmbCloudAuthorizationRequest: Equatable, Hashable, Sendable {
13 - /// The fully formed `oauth/authorize` URL to load in a browser/user agent.
14 - public let authorizeURL: URL
15 - /// The opaque `state` value used to protect against CSRF / callback mix-ups.
16 - public let state: String
17 - /// The PKCE code verifier that must be presented when exchanging the code.
18 - public let codeVerifier: String
19 -
20 - public init(authorizeURL: URL, state: String, codeVerifier: String) {
21 - self.authorizeURL = authorizeURL
22 - self.state = state
23 - self.codeVerifier = codeVerifier
24 - }
25 -}
26 -
27 -/// Headless, cross-platform smbCloud Auth engine.
28 -///
29 -/// `SmbCloudAuthClient` performs the platform-independent parts of the hosted
30 -/// login flow — building the PKCE authorization request, validating the
31 -/// callback, exchanging the authorization code for a session, and fetching
32 -/// user info. It does **not** present any UI and has no dependency on
33 -/// `AuthenticationServices`, the Keychain, or UIKit/AppKit, so it builds and
34 -/// runs on Apple platforms as well as Linux, Windows, and Android.
35 -///
36 -/// On Apple platforms, prefer `SmbCloudWebAuth` (from the `SmbCloudAuth`
37 -/// product) for an end-to-end `ASWebAuthenticationSession`-based login. On
38 -/// other platforms, drive the user agent yourself and feed the callback URL
39 -/// back into ``exchangeCallback(_:authorizationRequest:)``.
40 -public final class SmbCloudAuthClient: @unchecked Sendable {
41 - /// The default OpenID Connect scopes (`openid profile email`).
42 - public static let defaultScopes: [String] = SmbCloudOpenIDConnectClient.defaultScopes
43 -
44 - /// The public OAuth client identifier.
45 - public let clientId: String
46 - /// The redirect URL registered for this client.
47 - public let redirectURL: URL
48 -
49 - private let client: SmbCloudOpenIDConnectClient
50 - private let userInfoClient: SmbCloudUserInfoClient
51 -
52 - public init(
53 - environment: SmbCloudEnvironment = .production,
54 - clientId: String,
55 - redirectURL: URL
56 - ) {
57 - let baseURL = SmbCloudBaseURLFactory.makeURL(for: environment)
58 - self.clientId = clientId
59 - self.redirectURL = redirectURL
60 - self.client = SmbCloudOpenIDConnectClient(baseURL: baseURL)
61 - self.userInfoClient = SmbCloudUserInfoClient(baseURL: baseURL)
62 - }
63 -
64 - public init(baseURL: URL, clientId: String, redirectURL: URL) {
65 - self.clientId = clientId
66 - self.redirectURL = redirectURL
67 - self.client = SmbCloudOpenIDConnectClient(baseURL: baseURL)
68 - self.userInfoClient = SmbCloudUserInfoClient(baseURL: baseURL)
69 - }
70 -
71 - public convenience init(domain: String, clientId: String, redirectURL: URL) throws {
72 - let baseURL = try SmbCloudBaseURLFactory.makeURL(from: domain)
73 - self.init(baseURL: baseURL, clientId: clientId, redirectURL: redirectURL)
74 - }
75 -
76 - /// The callback scheme derived from ``redirectURL``, if any.
77 - ///
78 - /// Useful when configuring a platform web-authentication session.
79 - public var callbackScheme: String? {
80 - guard let scheme = redirectURL.scheme, scheme.isEmpty == false else {
81 - return nil
82 - }
83 -
84 - return scheme
85 - }
86 -
87 - /// Builds a fresh PKCE authorization request.
88 - public func authorizationRequest(
89 - scopes: [String] = SmbCloudAuthClient.defaultScopes,
90 - audience: String? = nil
91 - ) throws -> SmbCloudAuthorizationRequest {
92 - let request = try client.authorizationRequest(
93 - clientId: clientId,
94 - redirectURL: redirectURL,
95 - scopes: scopes,
96 - audience: audience
97 - )
98 -
99 - return SmbCloudAuthorizationRequest(
100 - authorizeURL: request.authorizeURL,
101 - state: request.state,
102 - codeVerifier: request.codeVerifier
103 - )
104 - }
105 -
106 - /// Validates a redirect callback URL and extracts the authorization code.
107 - public func authorizationCode(from callbackURL: URL, expectedState: String) throws -> String {
108 - try client.parseAuthorizationCode(from: callbackURL, expectedState: expectedState)
109 - }
110 -
111 - /// Exchanges an authorization code for a session using the PKCE verifier.
112 - public func exchangeCode(_ code: String, codeVerifier: String) async throws -> SmbCloudSession {
113 - try await client.exchangeCode(
114 - clientId: clientId,
115 - redirectURL: redirectURL,
116 - code: code,
117 - codeVerifier: codeVerifier
118 - )
119 - }
120 -
121 - /// Completes the flow: validates the callback URL against `authorizationRequest`
122 - /// and exchanges the code for a session.
123 - public func exchangeCallback(
124 - _ callbackURL: URL,
125 - authorizationRequest: SmbCloudAuthorizationRequest
126 - ) async throws -> SmbCloudSession {
127 - let code = try authorizationCode(
128 - from: callbackURL,
129 - expectedState: authorizationRequest.state
130 - )
131 -
132 - return try await exchangeCode(code, codeVerifier: authorizationRequest.codeVerifier)
133 - }
134 -
135 - /// Convenience overload that persists the resulting session to a store.
136 - @discardableResult
137 - public func exchangeCallback(
138 - _ callbackURL: URL,
139 - authorizationRequest: SmbCloudAuthorizationRequest,
140 - credentialsStore: SmbCloudCredentialsStore?
141 - ) async throws -> SmbCloudSession {
142 - let session = try await exchangeCallback(
143 - callbackURL,
144 - authorizationRequest: authorizationRequest
145 - )
146 -
147 - try credentialsStore?.store(session)
148 - return session
149 - }
150 -
151 - public func userInfo(accessToken: String, tenantId: String? = nil) async throws
152 - -> SmbCloudUserInfo
153 - {
154 - try await userInfoClient.userInfo(accessToken: accessToken, tenantId: tenantId)
155 - }
156 -
157 - public func userInfo(session: SmbCloudSession, tenantId: String? = nil) async throws
158 - -> SmbCloudUserInfo
159 - {
160 - try await userInfo(accessToken: session.accessToken, tenantId: tenantId)
161 - }
162 -
163 - /// Clears any locally stored session. Hosted-login logout currently only
164 - /// removes the local session; it does not revoke tokens server-side.
165 - public func logout(credentialsStore: SmbCloudCredentialsStore? = nil) throws {
166 - try credentialsStore?.clear()
167 - }
168 -}
Sources/AuthCore/SmbCloudClientError.swift deleted
-58
@@ -1,58 +0,0 @@
1 -import Foundation
2 -
3 -public enum SmbCloudClientError: Error, LocalizedError, Sendable {
4 - case invalidBaseURL(String)
5 - case invalidRedirectURL(String)
6 - case invalidCallbackURL(String)
7 - case missingAuthorizationCode
8 - case missingAuthorizationState
9 - case stateMismatch(expected: String, received: String)
10 - case unsupportedPlatform
11 - case loginInProgress
12 - case cancelled
13 - case authenticationFailed(String?)
14 - case transportError(String)
15 - case invalidResponse
16 - case decodingFailed(String)
17 - case apiError(statusCode: Int?, errorCode: Int?, message: String)
18 -
19 - public var errorDescription: String? {
20 - switch self {
21 - case .invalidBaseURL(let message):
22 - return message
23 - case .invalidRedirectURL(let message):
24 - return message
25 - case .invalidCallbackURL(let message):
26 - return message
27 - case .missingAuthorizationCode:
28 - return "The callback URL did not include an authorization code."
29 - case .missingAuthorizationState:
30 - return "The callback URL did not include an authorization state value."
31 - case .stateMismatch:
32 - return "The callback state did not match."
33 - case .unsupportedPlatform:
34 - return "Hosted web authentication is not supported on this platform."
35 - case .loginInProgress:
36 - return "A web authentication session is already in progress."
37 - case .cancelled:
38 - return "The authentication session was cancelled."
39 - case .authenticationFailed(let message):
40 - return message ?? "The authentication session failed."
41 - case .transportError(let message):
42 - return message
43 - case .invalidResponse:
44 - return "The server returned an invalid response."
45 - case .decodingFailed(let message):
46 - return message
47 - case .apiError(let statusCode, let errorCode, let message):
48 - let parts = [
49 - statusCode.map { "HTTP \($0)" },
50 - errorCode.map { "API \($0)" },
51 - message.nilIfEmpty,
52 - ].compactMap { $0 }
53 -
54 - return parts.isEmpty
55 - ? "The smbCloud Auth API returned an error." : parts.joined(separator: " — ")
56 - }
57 - }
58 -}
Sources/AuthCore/SmbCloudEnvironment.swift new
+36
@@ -0,0 +1,36 @@
1 +import Foundation
2 +
3 +/// The smbCloud environment to target.
4 +///
5 +/// Mirrors `smbcloud_network::environment::Environment` in the canonical Rust
6 +/// SDK: `dev` points at the local auth service over plain HTTP loopback, and
7 +/// `production` at the hosted service over HTTPS.
8 +public enum SmbCloudEnvironment: Sendable, Equatable {
9 + case dev
10 + case production
11 + /// Escape hatch for self-hosted / staging deployments.
12 + case custom(scheme: String, host: String)
13 +
14 + /// `http` for `dev`, `https` for `production`.
15 + public var apiProtocol: String {
16 + switch self {
17 + case .dev: return "http"
18 + case .production: return "https"
19 + case .custom(let scheme, _): return scheme
20 + }
21 + }
22 +
23 + /// `localhost:8088` for `dev`, `api.smbcloud.xyz` for `production`.
24 + public var apiHost: String {
25 + switch self {
26 + case .dev: return "localhost:8088"
27 + case .production: return "api.smbcloud.xyz"
28 + case .custom(_, let host): return host
29 + }
30 + }
31 +
32 + /// The issuer base URL (`<scheme>://<host>/`), used by the OIDC endpoints.
33 + var baseURL: URL {
34 + URL(string: "\(apiProtocol)://\(apiHost)/")!
35 + }
36 +}
Sources/AuthCore/SmbCloudError.swift new
+82
@@ -0,0 +1,82 @@
1 +import Foundation
2 +
3 +/// An error returned by the smbCloud Auth API.
4 +///
5 +/// Mirrors `smbcloud_model::error_codes::ErrorResponse::Error { error_code,
6 +/// message }` from the canonical SDK.
7 +public struct SmbCloudError: Error, Sendable, Equatable {
8 + public let code: SmbCloudErrorCode
9 + public let message: String
10 +
11 + public init(code: SmbCloudErrorCode, message: String? = nil) {
12 + self.code = code
13 + self.message = message ?? code.defaultMessage
14 + }
15 +}
16 +
17 +extension SmbCloudError: LocalizedError {
18 + public var errorDescription: String? { message }
19 +}
20 +
21 +/// Mirrors `smbcloud_model::error_codes::ErrorCode` (the `i32`-repr enum).
22 +public enum SmbCloudErrorCode: Int, Sendable, Equatable {
23 + // Generic
24 + case unknown = 0
25 + case parseError = 1
26 + case networkError = 2
27 + case inputError = 3
28 + case missingConfig = 4
29 + case cancel = 5
30 + // Account / access
31 + case unauthorized = 100
32 + case invalidParams = 101
33 + // Account-not-ready
34 + case emailNotFound = 1000
35 + case emailNotVerified = 1001
36 + case emailConfirmationFailed = 1002
37 + case passwordNotSet = 1003
38 + case gitHubEmailNotConnected = 1004
39 + case emailAlreadyExist = 1005
40 + case invalidPassword = 1006
41 + // Projects
42 + case projectNotFound = 2000
43 + case unsupportedRunner = 2001
44 +
45 + public var defaultMessage: String {
46 + switch self {
47 + case .unknown: return "Unknown error."
48 + case .parseError: return "Parse error."
49 + case .networkError:
50 + return "Network error. Please check your internet connection and try again."
51 + case .inputError: return "Input error."
52 + case .missingConfig: return "Missing config."
53 + case .cancel: return "Cancelled."
54 + case .unauthorized: return "Unauthorized access."
55 + case .invalidParams: return "Invalid parameters."
56 + case .emailNotFound: return "Email not found."
57 + case .emailNotVerified: return "Email not verified."
58 + case .emailConfirmationFailed: return "Email confirmation failed."
59 + case .passwordNotSet: return "Password is not set."
60 + case .gitHubEmailNotConnected: return "GitHub email is not connected."
61 + case .emailAlreadyExist: return "Email already exists."
62 + case .invalidPassword: return "Invalid password."
63 + case .projectNotFound: return "Project not found."
64 + case .unsupportedRunner: return "Unsupported runner."
65 + }
66 + }
67 +}
68 +
69 +/// Account-status error codes used inside ``AccountStatus/incomplete(status:)``.
70 +///
71 +/// Mirrors `smbcloud_model::account::ErrorCode` (the `u32`-repr enum), which is
72 +/// distinct from ``SmbCloudErrorCode``.
73 +public enum AccountErrorCode: Int, Sendable, Equatable {
74 + case emailNotFound = 1000
75 + case emailUnverified = 1001
76 + case emailConfirmationFailed = 1002
77 + case passwordNotSet = 1003
78 + case githubNotLinked = 1004
79 + case emailAlreadyExist = 1005
80 + case invalidPassword = 1006
81 + case hostedMailAccountUnverified = 1007
82 +}
Sources/AuthCore/SmbCloudModels.swift deleted
-28
@@ -1,28 +0,0 @@
1 -import Foundation
2 -
3 -public enum SmbCloudEnvironment: Sendable {
4 - case dev
5 - case production
6 -}
7 -
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 -
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 - }
28 -}
Sources/AuthCore/SmbCloudOpenIDConnectClient.swift deleted
-380
@@ -1,380 +0,0 @@
1 -import Foundation
2 -
3 -#if canImport(FoundationNetworking)
4 - // URLSession lives in FoundationNetworking on non-Apple Swift distributions (Linux, Windows, Android).
5 - import FoundationNetworking
6 -#endif
7 -
8 -#if canImport(CryptoKit)
9 - import CryptoKit
10 -#else
11 - // swift-crypto provides the same SHA256 API on Linux, Windows, and Android.
12 - import Crypto
13 -#endif
14 -
15 -internal struct SmbCloudOpenIDConnectAuthorizationRequest {
16 - let authorizeURL: URL
17 - let state: String
18 - let codeVerifier: String
19 -}
20 -
21 -internal struct SmbCloudTokenPayload: Decodable {
22 - let accessToken: String
23 - let tokenType: String
24 - let expiresIn: Int32?
25 - let refreshToken: String?
26 - let scope: String?
27 - let idToken: String?
28 -
29 - private enum CodingKeys: String, CodingKey {
30 - case accessToken = "access_token"
31 - case tokenType = "token_type"
32 - case expiresIn = "expires_in"
33 - case refreshToken = "refresh_token"
34 - case scope
35 - case idToken = "id_token"
36 - }
37 -}
38 -
39 -internal struct SmbCloudUserInfoPayload: Decodable {
40 - let sub: String
41 - let email: String?
42 - let emailVerified: Bool?
43 - let tenantId: UInt64?
44 - let tenantSlug: String?
45 -
46 - private enum CodingKeys: String, CodingKey {
47 - case sub
48 - case email
49 - case emailVerified = "email_verified"
50 - case tenantId = "tenant_id"
51 - case tenantSlug = "tenant_slug"
52 - }
53 -
54 - var userInfo: SmbCloudUserInfo {
55 - SmbCloudUserInfo(
56 - sub: sub,
57 - email: email,
58 - emailVerified: emailVerified,
59 - tenantId: tenantId,
60 - tenantSlug: tenantSlug
61 - )
62 - }
63 -}
64 -
65 -internal struct SmbCloudAPIErrorPayload: Decodable {
66 - let errorCode: Int?
67 - let message: String
68 -
69 - private enum CodingKeys: String, CodingKey {
70 - case errorCode = "error_code"
71 - case message
72 - }
73 -}
74 -
75 -internal struct SmbCloudOAuthErrorPayload: Decodable {
76 - let error: String
77 - let errorDescription: String?
78 -
79 - private enum CodingKeys: String, CodingKey {
80 - case error
81 - case errorDescription = "error_description"
82 - }
83 -}
84 -
85 -internal struct SmbCloudOpenIDConnectClient {
86 - static let defaultScopes = ["openid", "profile", "email"]
87 -
88 - let baseURL: URL
89 - let urlSession: URLSession
90 -
91 - init(baseURL: URL, urlSession: URLSession = .shared) {
92 - self.baseURL = baseURL
93 - self.urlSession = urlSession
94 - }
95 -
96 - func authorizationRequest(
97 - clientId: String,
98 - redirectURL: URL,
99 - scopes: [String],
100 - audience: String?
101 - ) throws -> SmbCloudOpenIDConnectAuthorizationRequest {
102 - guard redirectURL.scheme?.nilIfEmpty != nil else {
103 - throw SmbCloudClientError.invalidRedirectURL(
104 - "The redirect URL must include a callback scheme."
105 - )
106 - }
107 -
108 - let state = Self.randomURLSafeString(byteCount: 16)
109 - let codeVerifier = Self.randomURLSafeString(byteCount: 32)
110 - let codeChallenge = Self.sha256Base64URL(from: codeVerifier)
111 - let normalizedScopes = Self.normalizedScopes(scopes)
112 -
113 - let authorizeEndpoint = endpointURL(path: "oauth/authorize")
114 - try Self.validateTransportSecurity(for: authorizeEndpoint)
115 -
116 - guard
117 - var components = URLComponents(
118 - url: authorizeEndpoint,
119 - resolvingAgainstBaseURL: false
120 - )
121 - else {
122 - throw SmbCloudClientError.invalidBaseURL(
123 - "Failed to build the smbCloud authorization URL."
124 - )
125 - }
126 -
127 - var queryItems = [
128 - URLQueryItem(name: "client_id", value: clientId),
129 - URLQueryItem(name: "redirect_uri", value: redirectURL.absoluteString),
130 - URLQueryItem(name: "response_type", value: "code"),
131 - URLQueryItem(name: "scope", value: normalizedScopes.joined(separator: " ")),
132 - URLQueryItem(name: "state", value: state),
133 - URLQueryItem(name: "code_challenge", value: codeChallenge),
134 - URLQueryItem(name: "code_challenge_method", value: "S256"),
135 - ]
136 -
137 - if let audience = audience?.nilIfEmpty {
138 - queryItems.append(URLQueryItem(name: "audience", value: audience))
139 - }
140 -
141 - components.queryItems = queryItems
142 -
143 - guard let authorizeURL = components.url else {
144 - throw SmbCloudClientError.invalidBaseURL(
145 - "Failed to build the smbCloud authorization URL."
146 - )
147 - }
148 -
149 - return SmbCloudOpenIDConnectAuthorizationRequest(
150 - authorizeURL: authorizeURL,
151 - state: state,
152 - codeVerifier: codeVerifier
153 - )
154 - }
155 -
156 - func parseAuthorizationCode(from callbackURL: URL, expectedState: String) throws -> String {
157 - guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)
158 - else {
159 - throw SmbCloudClientError.invalidCallbackURL(
160 - "The callback URL was not valid."
161 - )
162 - }
163 -
164 - let queryItems = components.queryItems ?? []
165 -
166 - if let error = queryItems.firstValue(named: "error") {
167 - let description = queryItems.firstValue(named: "error_description") ?? error
168 - throw SmbCloudClientError.authenticationFailed(description)
169 - }
170 -
171 - guard let code = queryItems.firstValue(named: "code") else {
172 - throw SmbCloudClientError.missingAuthorizationCode
173 - }
174 -
175 - guard let state = queryItems.firstValue(named: "state") else {
176 - throw SmbCloudClientError.missingAuthorizationState
177 - }
178 -
179 - guard state == expectedState else {
180 - throw SmbCloudClientError.stateMismatch(expected: expectedState, received: state)
181 - }
182 -
183 - return code
184 - }
185 -
186 - func exchangeCode(
187 - clientId: String,
188 - redirectURL: URL,
189 - code: String,
190 - codeVerifier: String
191 - ) async throws -> SmbCloudSession {
192 - var request = URLRequest(url: endpointURL(path: "oauth/token"))
193 - request.httpMethod = "POST"
194 - request.setValue("application/json", forHTTPHeaderField: "Accept")
195 - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
196 - request.httpBody = Self.formEncodedData(
197 - from: [
198 - URLQueryItem(name: "grant_type", value: "authorization_code"),
199 - URLQueryItem(name: "client_id", value: clientId),
200 - URLQueryItem(name: "code", value: code),
201 - URLQueryItem(name: "redirect_uri", value: redirectURL.absoluteString),
202 - URLQueryItem(name: "code_verifier", value: codeVerifier),
203 - ]
204 - )
205 -
206 - let payload: SmbCloudTokenPayload = try await send(
207 - request, decodeAs: SmbCloudTokenPayload.self)
208 - return SmbCloudSession(tokenResponse: payload)
209 - }
210 -
211 - func userInfo(accessToken: String, tenantId: String?) async throws -> SmbCloudUserInfo {
212 - var request = URLRequest(url: endpointURL(path: "oauth/userinfo"))
213 - request.httpMethod = "GET"
214 - request.setValue("application/json", forHTTPHeaderField: "Accept")
215 - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
216 -
217 - if let tenantId = tenantId?.nilIfEmpty {
218 - request.setValue(tenantId, forHTTPHeaderField: "X-Smbcloud-Tenant-Id")
219 - }
220 -
221 - let payload: SmbCloudUserInfoPayload = try await send(
222 - request, decodeAs: SmbCloudUserInfoPayload.self)
223 - return payload.userInfo
224 - }
225 -
226 - func endpointURL(path: String) -> URL {
227 - baseURL.appendingPathComponent(path)
228 - }
229 -
230 - func send<Response: Decodable>(_ request: URLRequest, decodeAs: Response.Type) async throws
231 - -> Response
232 - {
233 - guard let requestURL = request.url else {
234 - throw SmbCloudClientError.invalidResponse
235 - }
236 -
237 - try Self.validateTransportSecurity(for: requestURL)
238 -
239 - let data: Data
240 - let response: URLResponse
241 -
242 - do {
243 - (data, response) = try await urlSession.data(for: request)
244 - } catch {
245 - throw SmbCloudClientError.transportError(error.localizedDescription)
246 - }
247 -
248 - guard let httpResponse = response as? HTTPURLResponse else {
249 - throw SmbCloudClientError.invalidResponse
250 - }
251 -
252 - guard (200..<300).contains(httpResponse.statusCode) else {
253 - throw apiError(from: data, statusCode: httpResponse.statusCode)
254 - }
255 -
256 - do {
257 - return try JSONDecoder().decode(Response.self, from: data)
258 - } catch {
259 - throw SmbCloudClientError.decodingFailed(
260 - "Failed to decode the smbCloud Auth response: \(error.localizedDescription)"
261 - )
262 - }
263 - }
264 -
265 - func apiError(from data: Data, statusCode: Int) -> SmbCloudClientError {
266 - if let payload = try? JSONDecoder().decode(SmbCloudAPIErrorPayload.self, from: data) {
267 - return .apiError(
268 - statusCode: statusCode,
269 - errorCode: payload.errorCode,
270 - message: payload.message
271 - )
272 - }
273 -
274 - if let payload = try? JSONDecoder().decode(SmbCloudOAuthErrorPayload.self, from: data) {
275 - return .apiError(
276 - statusCode: statusCode,
277 - errorCode: nil,
278 - message: payload.errorDescription?.nilIfEmpty ?? payload.error
279 - )
280 - }
281 -
282 - let fallbackMessage =
283 - String(data: data, encoding: .utf8)?.nilIfEmpty
284 - ?? HTTPURLResponse.localizedString(forStatusCode: statusCode)
285 -
286 - return .apiError(statusCode: statusCode, errorCode: nil, message: fallbackMessage)
287 - }
288 -
289 - static func normalizedScopes(from scope: String?) -> [String] {
290 - normalizedScopes((scope ?? "").split(separator: " ").map(String.init))
291 - }
292 -
293 - static func normalizedScopes(_ scopes: [String]) -> [String] {
294 - let sanitizedScopes = scopes.smbCloudUniqued()
295 - return sanitizedScopes.isEmpty ? defaultScopes : sanitizedScopes
296 - }
297 -
298 - static func randomURLSafeString(byteCount: Int) -> String {
299 - let randomBytes = Data((0..<byteCount).map { _ in UInt8.random(in: .min ... .max) })
300 - return randomBytes.base64URLEncodedString()
301 - }
302 -
303 - static func sha256Base64URL(from value: String) -> String {
304 - let digest = SHA256.hash(data: Data(value.utf8))
305 - return Data(digest).base64URLEncodedString()
306 - }
307 -
308 - static func validateTransportSecurity(for url: URL) throws {
309 - let normalizedScheme = url.scheme?.lowercased()
310 - if normalizedScheme == "https" {
311 - return
312 - }
313 -
314 - if normalizedScheme == "http", isLoopbackHost(url.host) {
315 - return
316 - }
317 -
318 - throw SmbCloudClientError.invalidBaseURL(
319 - "smbCloud Auth URLs must use HTTPS. Plain HTTP is only allowed for local loopback development."
320 - )
321 - }
322 -
323 - static func isLoopbackHost(_ host: String?) -> Bool {
324 - guard let normalizedHost = host?.lowercased() else {
325 - return false
326 - }
327 -
328 - return normalizedHost == "localhost"
329 - || normalizedHost == "127.0.0.1"
330 - || normalizedHost == "::1"
331 - || normalizedHost == "[::1]"
332 - }
333 -
334 - static func formEncodedData(from items: [URLQueryItem]) -> Data? {
335 - var components = URLComponents()
336 - components.queryItems = items
337 - return components.percentEncodedQuery?.data(using: .utf8)
338 - }
339 -}
340 -
341 -internal enum SmbCloudBaseURLFactory {
342 - static func makeURL(for environment: SmbCloudEnvironment) -> URL {
343 - switch environment {
344 - case .dev:
345 - return URL(string: "http://localhost:8088")!
346 - case .production:
347 - return URL(string: "https://api.smbcloud.xyz")!
348 - }
349 - }
350 -
351 - static func makeURL(from domain: String) throws -> URL {
352 - let trimmedDomain = domain.trimmingCharacters(in: .whitespacesAndNewlines)
353 - guard trimmedDomain.isEmpty == false else {
354 - throw SmbCloudClientError.invalidBaseURL("The smbCloud Auth domain cannot be empty.")
355 - }
356 -
357 - let candidate = trimmedDomain.contains("://") ? trimmedDomain : "https://\(trimmedDomain)"
358 - guard let url = URL(string: candidate),
359 - url.scheme?.nilIfEmpty != nil,
360 - url.host?.nilIfEmpty != nil
361 - else {
362 - throw SmbCloudClientError.invalidBaseURL(
363 - "The smbCloud Auth domain or base URL is invalid: \(domain)"
364 - )
365 - }
366 -
367 - try SmbCloudOpenIDConnectClient.validateTransportSecurity(for: url)
368 -
369 - return url
370 - }
371 -}
372 -
373 -extension Data {
374 - func base64URLEncodedString() -> String {
375 - base64EncodedString()
376 - .replacingOccurrences(of: "+", with: "-")
377 - .replacingOccurrences(of: "/", with: "_")
378 - .replacingOccurrences(of: "=", with: "")
379 - }
380 -}
Sources/AuthCore/SmbCloudSession.swift
+22 -29
@@ -1,5 +1,8 @@
1 import Foundation
2
3 +/// A persisted credential — the access token plus optional refresh/id tokens
4 +/// and expiry. Produced by the OIDC token exchange (and storable from a tenant
5 +/// login), and what ``SmbCloudCredentialsStore`` persists.
6 public struct SmbCloudSession: Codable, Equatable, Hashable, Sendable {
7 public let accessToken: String
8 public let refreshToken: String?
@@ -25,11 +28,7 @@ public struct SmbCloudSession: Codable, Equatable, Hashable, Sendable {
28 }
29
30 public var scope: String? {
28 - guard scopes.isEmpty == false else {
29 - return nil
30 - }
31 -
32 - return scopes.joined(separator: " ")
31 + scopes.isEmpty ? nil : scopes.joined(separator: " ")
32 }
33
34 public var authorizationHeaderValue: String {
@@ -41,10 +40,7 @@ public struct SmbCloudSession: Codable, Equatable, Hashable, Sendable {
40 }
41
42 public func isExpired(leeway: TimeInterval = 0) -> Bool {
44 - guard let expiresAt else {
45 - return false
46 - }
47 -
43 + guard let expiresAt else { return false }
44 return expiresAt <= Date().addingTimeInterval(leeway)
45 }
46
@@ -54,23 +50,30 @@ public struct SmbCloudSession: Codable, Equatable, Hashable, Sendable {
50 }
51
52 extension SmbCloudSession {
57 - init(tokenResponse: SmbCloudTokenPayload, now: Date = Date()) {
53 + /// Builds a session from an OIDC token exchange response.
54 + public init(tokenResponse: OIDC.TokenResponse, now: Date = Date()) {
55 let expiresAt = tokenResponse.expiresIn.map { now.addingTimeInterval(TimeInterval($0)) }
56 + let scopes =
57 + tokenResponse.scope?
58 + .split(separator: " ")
59 + .map(String.init)
60 + .smbCloudUniqued() ?? []
61 self.init(
62 accessToken: tokenResponse.accessToken,
61 - refreshToken: tokenResponse.refreshToken?.nilIfEmpty,
62 - idToken: tokenResponse.idToken?.nilIfEmpty,
63 + refreshToken: tokenResponse.refreshToken?.smbCloudNilIfEmpty,
64 + idToken: tokenResponse.idToken?.smbCloudNilIfEmpty,
65 tokenType: tokenResponse.tokenType,
66 expiresAt: expiresAt,
65 - scopes: SmbCloudOpenIDConnectClient.normalizedScopes(from: tokenResponse.scope)
67 + scopes: scopes
68 )
69 }
70
71 + /// A normalized copy (trimmed/deduped scopes, empty tokens dropped).
72 public func storedRepresentation() -> SmbCloudSession {
73 SmbCloudSession(
74 accessToken: accessToken,
72 - refreshToken: refreshToken?.nilIfEmpty,
73 - idToken: idToken?.nilIfEmpty,
75 + refreshToken: refreshToken?.smbCloudNilIfEmpty,
76 + idToken: idToken?.smbCloudNilIfEmpty,
77 tokenType: tokenType,
78 expiresAt: expiresAt,
79 scopes: scopes.smbCloudUniqued()
@@ -79,7 +82,7 @@ extension SmbCloudSession {
82 }
83
84 extension String {
82 - var nilIfEmpty: String? {
85 + var smbCloudNilIfEmpty: String? {
86 trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
87 }
88 }
@@ -87,20 +90,10 @@ extension String {
90 extension Sequence where Element == String {
91 func smbCloudUniqued() -> [String] {
92 var seen = Set<String>()
90 -
93 return compactMap { value in
92 - let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
93 - guard trimmedValue.isEmpty == false else {
94 - return nil
95 - }
96 -
97 - return seen.insert(trimmedValue).inserted ? trimmedValue : nil
94 + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
95 + guard !trimmed.isEmpty else { return nil }
96 + return seen.insert(trimmed).inserted ? trimmed : nil
97 }
98 }
99 }
101 -
102 -extension Sequence where Element == URLQueryItem {
103 - func firstValue(named name: String) -> String? {
104 - first(where: { $0.name == name })?.value?.nilIfEmpty
105 - }
106 -}
Sources/AuthCore/SmbCloudUserInfoClient.swift deleted
-33
@@ -1,33 +0,0 @@
1 -import Foundation
2 -
3 -public struct SmbCloudUserInfoClient {
4 - private let client: SmbCloudOpenIDConnectClient
5 -
6 - public init(environment: SmbCloudEnvironment = .production) {
7 - self.client = SmbCloudOpenIDConnectClient(
8 - baseURL: SmbCloudBaseURLFactory.makeURL(for: environment)
9 - )
10 - }
11 -
12 - public init(baseURL: URL) {
13 - self.client = SmbCloudOpenIDConnectClient(baseURL: baseURL)
14 - }
15 -
16 - public init(domain: String) throws {
17 - self.client = SmbCloudOpenIDConnectClient(
18 - baseURL: try SmbCloudBaseURLFactory.makeURL(from: domain)
19 - )
20 - }
21 -
22 - public func userInfo(accessToken: String, tenantId: String? = nil) async throws
23 - -> SmbCloudUserInfo
24 - {
25 - try await client.userInfo(accessToken: accessToken, tenantId: tenantId)
26 - }
27 -
28 - public func userInfo(session: SmbCloudSession, tenantId: String? = nil) async throws
29 - -> SmbCloudUserInfo
30 - {
31 - try await userInfo(accessToken: session.accessToken, tenantId: tenantId)
32 - }
33 -}
Sources/SmbCloudAuth/AuthCoreExports.swift
+4 -4
@@ -1,6 +1,6 @@
1 // `SmbCloudAuth` is the Apple-platform UI layer on top of the cross-platform
2 -// `AuthCore` engine. Re-export `AuthCore` so existing `import SmbCloudAuth`
3 -// call sites keep seeing `SmbCloudSession`, `SmbCloudUserInfo`,
4 -// `SmbCloudClientError`, `SmbCloudAuthClient`, `SmbCloudCredentialsStore`,
5 -// `SmbCloudUserInfoClient`, and friends without an extra import.
2 +// `AuthCore` engine. Re-export `AuthCore` so `import SmbCloudAuth` call sites
3 +// also see `AuthCoreClient`, `OIDC`, `SmbCloudSession`, `ClientCredentials`,
4 +// `SmbCloudError`, `SmbCloudCredentialsStore`, and friends without an extra
5 +// import.
6 @_exported import AuthCore
Sources/SmbCloudAuth/SmbCloudWebAuth.swift
+83 -88
@@ -7,76 +7,76 @@ import Foundation
7 public typealias SmbCloudPresentationAnchor = ASPresentationAnchor
8 #endif
9
10 -/// Apple-platform hosted login built on `AuthCore`.
10 +/// Errors specific to the Apple hosted-login UI.
11 +public enum SmbCloudWebAuthError: Error, LocalizedError, Sendable {
12 + case loginInProgress
13 + case cancelled
14 + case invalidRedirectURL(String)
15 + case stateMismatch
16 + case missingCallbackURL
17 + case authenticationFailed(String?)
18 +
19 + public var errorDescription: String? {
20 + switch self {
21 + case .loginInProgress: return "A web authentication session is already in progress."
22 + case .cancelled: return "The authentication session was cancelled."
23 + case .invalidRedirectURL(let message): return message
24 + case .stateMismatch: return "The callback state did not match."
25 + case .missingCallbackURL: return "The authentication session completed without a callback URL."
26 + case .authenticationFailed(let message): return message ?? "The authentication session failed."
27 + }
28 + }
29 +}
30 +
31 +/// Apple-platform hosted login built on `AuthCore`'s OIDC public-client flow.
32 ///
12 -/// `SmbCloudWebAuth` wraps the cross-platform ``SmbCloudAuthClient`` engine and
13 -/// adds an `ASWebAuthenticationSession`-driven `login(...)` for iOS, macOS, and
14 -/// visionOS. The non-interactive helpers (`userInfo`, `logout`, `clearSession`)
15 -/// are available wherever the package builds.
33 +/// `SmbCloudWebAuth` is the **public client** entry point: it uses only an
34 +/// `oidcClientId` (no `app_secret`) and drives an `ASWebAuthenticationSession`
35 +/// through smbCloud's OIDC Authorization Code + PKCE flow. This is the
36 +/// recommended native sign-in path. Email/password sign-in (which needs the
37 +/// confidential `app_secret`) belongs on a backend via ``AuthCoreClient``.
38 ///
17 -/// For headless or non-Apple usage (Linux, Windows, Android, servers, tests),
18 -/// use ``SmbCloudAuthClient`` from the `AuthCore` product directly.
39 +/// For headless or non-Apple usage, use ``OIDC`` (or ``AuthCoreClient``) from
40 +/// the `AuthCore` product directly.
41 public final class SmbCloudWebAuth: @unchecked Sendable {
20 - public static let defaultScopes: [String] = SmbCloudAuthClient.defaultScopes
21 -
22 - private let authClient: SmbCloudAuthClient
42 + public let environment: SmbCloudEnvironment
43 + public let oidcClientId: String
44 + public let redirectURL: URL
45
46 #if canImport(AuthenticationServices) && (os(iOS) || os(macOS) || os(visionOS))
25 - @MainActor
26 - private var activeAuthenticationSession: ASWebAuthenticationSession?
27 -
28 - @MainActor
29 - private var activePresentationContextProvider: SmbCloudPresentationContextProvider?
47 + @MainActor private var activeAuthenticationSession: ASWebAuthenticationSession?
48 + @MainActor private var activePresentationContextProvider: SmbCloudPresentationContextProvider?
49 #endif
50
51 public init(
52 environment: SmbCloudEnvironment = .production,
34 - clientId: String,
53 + oidcClientId: String,
54 redirectURL: URL
55 ) {
37 - self.authClient = SmbCloudAuthClient(
38 - environment: environment,
39 - clientId: clientId,
40 - redirectURL: redirectURL
41 - )
42 - }
43 -
44 - public init(baseURL: URL, clientId: String, redirectURL: URL) {
45 - self.authClient = SmbCloudAuthClient(
46 - baseURL: baseURL,
47 - clientId: clientId,
48 - redirectURL: redirectURL
49 - )
56 + self.environment = environment
57 + self.oidcClientId = oidcClientId
58 + self.redirectURL = redirectURL
59 }
60
52 - public convenience init(domain: String, clientId: String, redirectURL: URL) throws {
53 - let authClient = try SmbCloudAuthClient(
54 - domain: domain,
55 - clientId: clientId,
56 - redirectURL: redirectURL
57 - )
58 - self.init(authClient: authClient)
61 + /// The callback scheme derived from ``redirectURL``.
62 + public var callbackScheme: String? {
63 + guard let scheme = redirectURL.scheme, !scheme.isEmpty else { return nil }
64 + return scheme
65 }
66
61 - private init(authClient: SmbCloudAuthClient) {
62 - self.authClient = authClient
63 - }
64 -
65 - /// The underlying headless engine, exposed for advanced/cross-cutting use.
66 - public var client: SmbCloudAuthClient {
67 - authClient
68 - }
67 + // Non-interactive helpers, available wherever the package builds.
68
69 public func userInfo(accessToken: String, tenantId: String? = nil) async throws
71 - -> SmbCloudUserInfo
70 + -> OIDC.UserInfo
71 {
73 - try await authClient.userInfo(accessToken: accessToken, tenantId: tenantId)
72 + try await OIDC.getUserInfo(
73 + environment: environment, accessToken: accessToken, tenantId: tenantId)
74 }
75
76 public func userInfo(session: SmbCloudSession, tenantId: String? = nil) async throws
77 - -> SmbCloudUserInfo
77 + -> OIDC.UserInfo
78 {
79 - try await authClient.userInfo(session: session, tenantId: tenantId)
79 + try await userInfo(accessToken: session.accessToken, tenantId: tenantId)
80 }
81
82 public func clearSession(credentialsManager: SmbCloudCredentialsStore? = nil) throws {
@@ -90,32 +90,29 @@ public final class SmbCloudWebAuth: @unchecked Sendable {
90
91 #if canImport(AuthenticationServices) && (os(iOS) || os(macOS) || os(visionOS))
92 extension SmbCloudWebAuth {
93 + /// Presents the hosted OIDC login and returns the resulting session.
94 @MainActor
95 public func login(
96 presentationAnchorProvider: @escaping () -> SmbCloudPresentationAnchor,
96 - scopes: [String] = SmbCloudWebAuth.defaultScopes,
97 - audience: String? = nil,
97 prefersEphemeralSession: Bool = false,
98 credentialsManager: SmbCloudCredentialsStore? = nil
99 ) async throws -> SmbCloudSession {
100 guard activeAuthenticationSession == nil else {
102 - throw SmbCloudClientError.loginInProgress
101 + throw SmbCloudWebAuthError.loginInProgress
102 + }
103 + guard let callbackScheme else {
104 + throw SmbCloudWebAuthError.invalidRedirectURL(
105 + "The redirect URL must include a callback scheme.")
106 }
107
105 - let authorizationRequest = try authClient.authorizationRequest(
106 - scopes: scopes,
107 - audience: audience
108 + let authRequest = try OIDC.buildAuthorizationRequest(
109 + environment: environment,
110 + oidcClientId: oidcClientId,
111 + redirectURI: redirectURL.absoluteString
112 )
113
110 - guard let callbackScheme = authClient.callbackScheme else {
111 - throw SmbCloudClientError.invalidRedirectURL(
112 - "The redirect URL must include a callback scheme."
113 - )
114 - }
115 -
114 let presentationContextProvider = SmbCloudPresentationContextProvider(
117 - presentationAnchorProvider: presentationAnchorProvider
118 - )
115 + presentationAnchorProvider: presentationAnchorProvider)
116
117 defer {
118 activeAuthenticationSession = nil
@@ -126,23 +123,17 @@ public final class SmbCloudWebAuth: @unchecked Sendable {
123 try await withCheckedThrowingContinuation {
124 (continuation: CheckedContinuation<URL, Error>) in
125 let session = ASWebAuthenticationSession(
129 - url: authorizationRequest.authorizeURL,
126 + url: authRequest.authorizeURL,
127 callbackURLScheme: callbackScheme
128 ) { callbackURL, error in
129 if let error {
130 continuation.resume(throwing: Self.authenticationError(from: error))
131 return
132 }
136 -
133 guard let callbackURL else {
138 - continuation.resume(
139 - throwing: SmbCloudClientError.authenticationFailed(
140 - "The authentication session completed without a callback URL."
141 - )
142 - )
134 + continuation.resume(throwing: SmbCloudWebAuthError.missingCallbackURL)
135 return
136 }
145 -
137 continuation.resume(returning: callbackURL)
138 }
139
@@ -156,18 +147,13 @@ public final class SmbCloudWebAuth: @unchecked Sendable {
147 activeAuthenticationSession = nil
148 activePresentationContextProvider = nil
149 continuation.resume(
159 - throwing: SmbCloudClientError.authenticationFailed(
160 - "Failed to start the smbCloud web authentication session."
161 - )
162 - )
150 + throwing: SmbCloudWebAuthError.authenticationFailed(
151 + "Failed to start the smbCloud web authentication session."))
152 return
153 }
154 }
155 } onCancel: { [weak self] in
167 - guard let self else {
168 - return
169 - }
170 -
156 + guard let self else { return }
157 Task { @MainActor in
158 self.activeAuthenticationSession?.cancel()
159 self.activeAuthenticationSession = nil
@@ -175,27 +161,36 @@ public final class SmbCloudWebAuth: @unchecked Sendable {
161 }
162 }
163
178 - return try await authClient.exchangeCallback(
179 - callbackURL,
180 - authorizationRequest: authorizationRequest,
181 - credentialsStore: credentialsManager
164 + let payload = try OIDC.parseCallbackURL(callbackURL.absoluteString)
165 + guard payload.state == authRequest.state else {
166 + throw SmbCloudWebAuthError.stateMismatch
167 + }
168 +
169 + let tokens = try await OIDC.exchangeCode(
170 + environment: environment,
171 + oidcClientId: oidcClientId,
172 + redirectURI: redirectURL.absoluteString,
173 + code: payload.code,
174 + codeVerifier: authRequest.codeVerifier
175 )
176 +
177 + let session = SmbCloudSession(tokenResponse: tokens)
178 + try credentialsManager?.store(session)
179 + return session
180 }
181
185 - private static func authenticationError(from error: Error) -> SmbCloudClientError {
186 - if let authenticationError = error as? ASWebAuthenticationSessionError,
187 - authenticationError.code == .canceledLogin
182 + private static func authenticationError(from error: Error) -> SmbCloudWebAuthError {
183 + if let authError = error as? ASWebAuthenticationSessionError,
184 + authError.code == .canceledLogin
185 {
186 return .cancelled
187 }
191 -
188 return .authenticationFailed(error.localizedDescription)
189 }
190 }
191
192 private final class SmbCloudPresentationContextProvider:
197 - NSObject,
198 - ASWebAuthenticationPresentationContextProviding
193 + NSObject, ASWebAuthenticationPresentationContextProviding
194 {
195 private let presentationAnchorProvider: () -> SmbCloudPresentationAnchor
196
Sources/SmbCloudAuthFFI/smbcloud_auth.swift deleted
-1973
@@ -1,1973 +0,0 @@
1 -// This file was autogenerated by some hot garbage in the `uniffi` crate.
2 -// Trust me, you don't want to mess with it!
3 -
4 -// swiftlint:disable all
5 -import Foundation
6 -
7 -// Depending on the consumer's build setup, the low-level FFI code
8 -// might be in a separate module, or it might be compiled inline into
9 -// this module. This is a bit of light hackery to work with both.
10 -#if canImport(smbcloud_authFFI)
11 -import smbcloud_authFFI
12 -#endif
13 -
14 -fileprivate extension RustBuffer {
15 - // Allocate a new buffer, copying the contents of a `UInt8` array.
16 - init(bytes: [UInt8]) {
17 - let rbuf = bytes.withUnsafeBufferPointer { ptr in
18 - RustBuffer.from(ptr)
19 - }
20 - self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data)
21 - }
22 -
23 - static func empty() -> RustBuffer {
24 - RustBuffer(capacity: 0, len:0, data: nil)
25 - }
26 -
27 - static func from(_ ptr: UnsafeBufferPointer<UInt8>) -> RustBuffer {
28 - try! rustCall { ffi_smbcloud_auth_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) }
29 - }
30 -
31 - // Frees the buffer in place.
32 - // The buffer must not be used after this is called.
33 - func deallocate() {
34 - try! rustCall { ffi_smbcloud_auth_rustbuffer_free(self, $0) }
35 - }
36 -}
37 -
38 -fileprivate extension ForeignBytes {
39 - init(bufferPointer: UnsafeBufferPointer<UInt8>) {
40 - self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress)
41 - }
42 -}
43 -
44 -// For every type used in the interface, we provide helper methods for conveniently
45 -// lifting and lowering that type from C-compatible data, and for reading and writing
46 -// values of that type in a buffer.
47 -
48 -// Helper classes/extensions that don't change.
49 -// Someday, this will be in a library of its own.
50 -
51 -fileprivate extension Data {
52 - init(rustBuffer: RustBuffer) {
53 - self.init(
54 - bytesNoCopy: rustBuffer.data!,
55 - count: Int(rustBuffer.len),
56 - deallocator: .none
57 - )
58 - }
59 -}
60 -
61 -// Define reader functionality. Normally this would be defined in a class or
62 -// struct, but we use standalone functions instead in order to make external
63 -// types work.
64 -//
65 -// With external types, one swift source file needs to be able to call the read
66 -// method on another source file's FfiConverter, but then what visibility
67 -// should Reader have?
68 -// - If Reader is fileprivate, then this means the read() must also
69 -// be fileprivate, which doesn't work with external types.
70 -// - If Reader is internal/public, we'll get compile errors since both source
71 -// files will try define the same type.
72 -//
73 -// Instead, the read() method and these helper functions input a tuple of data
74 -
75 -fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) {
76 - (data: data, offset: 0)
77 -}
78 -
79 -// Reads an integer at the current offset, in big-endian order, and advances
80 -// the offset on success. Throws if reading the integer would move the
81 -// offset past the end of the buffer.
82 -fileprivate func readInt<T: FixedWidthInteger>(_ reader: inout (data: Data, offset: Data.Index)) throws -> T {
83 - let range = reader.offset..<reader.offset + MemoryLayout<T>.size
84 - guard reader.data.count >= range.upperBound else {
85 - throw UniffiInternalError.bufferOverflow
86 - }
87 - if T.self == UInt8.self {
88 - let value = reader.data[reader.offset]
89 - reader.offset += 1
90 - return value as! T
91 - }
92 - var value: T = 0
93 - let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)})
94 - reader.offset = range.upperBound
95 - return value.bigEndian
96 -}
97 -
98 -// Reads an arbitrary number of bytes, to be used to read
99 -// raw bytes, this is useful when lifting strings
100 -fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array<UInt8> {
101 - let range = reader.offset..<(reader.offset+count)
102 - guard reader.data.count >= range.upperBound else {
103 - throw UniffiInternalError.bufferOverflow
104 - }
105 - var value = [UInt8](repeating: 0, count: count)
106 - value.withUnsafeMutableBufferPointer({ buffer in
107 - reader.data.copyBytes(to: buffer, from: range)
108 - })
109 - reader.offset = range.upperBound
110 - return value
111 -}
112 -
113 -// Reads a float at the current offset.
114 -fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float {
115 - return Float(bitPattern: try readInt(&reader))
116 -}
117 -
118 -// Reads a float at the current offset.
119 -fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double {
120 - return Double(bitPattern: try readInt(&reader))
121 -}
122 -
123 -// Indicates if the offset has reached the end of the buffer.
124 -fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool {
125 - return reader.offset < reader.data.count
126 -}
127 -
128 -// Define writer functionality. Normally this would be defined in a class or
129 -// struct, but we use standalone functions instead in order to make external
130 -// types work. See the above discussion on Readers for details.
131 -
132 -fileprivate func createWriter() -> [UInt8] {
133 - return []
134 -}
135 -
136 -fileprivate func writeBytes<S>(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 {
137 - writer.append(contentsOf: byteArr)
138 -}
139 -
140 -// Writes an integer in big-endian order.
141 -//
142 -// Warning: make sure what you are trying to write
143 -// is in the correct type!
144 -fileprivate func writeInt<T: FixedWidthInteger>(_ writer: inout [UInt8], _ value: T) {
145 - var value = value.bigEndian
146 - withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) }
147 -}
148 -
149 -fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) {
150 - writeInt(&writer, value.bitPattern)
151 -}
152 -
153 -fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) {
154 - writeInt(&writer, value.bitPattern)
155 -}
156 -
157 -// Protocol for types that transfer other types across the FFI. This is
158 -// analogous to the Rust trait of the same name.
159 -fileprivate protocol FfiConverter {
160 - associatedtype FfiType
161 - associatedtype SwiftType
162 -
163 - static func lift(_ value: FfiType) throws -> SwiftType
164 - static func lower(_ value: SwiftType) -> FfiType
165 - static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType
166 - static func write(_ value: SwiftType, into buf: inout [UInt8])
167 -}
168 -
169 -// Types conforming to `Primitive` pass themselves directly over the FFI.
170 -fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { }
171 -
172 -extension FfiConverterPrimitive {
173 -#if swift(>=5.8)
174 - @_documentation(visibility: private)
175 -#endif
176 - public static func lift(_ value: FfiType) throws -> SwiftType {
177 - return value
178 - }
179 -
180 -#if swift(>=5.8)
181 - @_documentation(visibility: private)
182 -#endif
183 - public static func lower(_ value: SwiftType) -> FfiType {
184 - return value
185 - }
186 -}
187 -
188 -// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`.
189 -// Used for complex types where it's hard to write a custom lift/lower.
190 -fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {}
191 -
192 -extension FfiConverterRustBuffer {
193 -#if swift(>=5.8)
194 - @_documentation(visibility: private)
195 -#endif
196 - public static func lift(_ buf: RustBuffer) throws -> SwiftType {
197 - var reader = createReader(data: Data(rustBuffer: buf))
198 - let value = try read(from: &reader)
199 - if hasRemaining(reader) {
200 - throw UniffiInternalError.incompleteData
201 - }
202 - buf.deallocate()
203 - return value
204 - }
205 -
206 -#if swift(>=5.8)
207 - @_documentation(visibility: private)
208 -#endif
209 - public static func lower(_ value: SwiftType) -> RustBuffer {
210 - var writer = createWriter()
211 - write(value, into: &writer)
212 - return RustBuffer(bytes: writer)
213 - }
214 -}
215 -// An error type for FFI errors. These errors occur at the UniFFI level, not
216 -// the library level.
217 -fileprivate enum UniffiInternalError: LocalizedError {
218 - case bufferOverflow
219 - case incompleteData
220 - case unexpectedOptionalTag
221 - case unexpectedEnumCase
222 - case unexpectedNullPointer
223 - case unexpectedRustCallStatusCode
224 - case unexpectedRustCallError
225 - case unexpectedStaleHandle
226 - case rustPanic(_ message: String)
227 -
228 - public var errorDescription: String? {
229 - switch self {
230 - case .bufferOverflow: return "Reading the requested value would read past the end of the buffer"
231 - case .incompleteData: return "The buffer still has data after lifting its containing value"
232 - case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1"
233 - case .unexpectedEnumCase: return "Raw enum value doesn't match any cases"
234 - case .unexpectedNullPointer: return "Raw pointer value was null"
235 - case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code"
236 - case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified"
237 - case .unexpectedStaleHandle: return "The object in the handle map has been dropped already"
238 - case let .rustPanic(message): return message
239 - }
240 - }
241 -}
242 -
243 -fileprivate extension NSLock {
244 - func withLock<T>(f: () throws -> T) rethrows -> T {
245 - self.lock()
246 - defer { self.unlock() }
247 - return try f()
248 - }
249 -}
250 -
251 -fileprivate let CALL_SUCCESS: Int8 = 0
252 -fileprivate let CALL_ERROR: Int8 = 1
253 -fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2
254 -fileprivate let CALL_CANCELLED: Int8 = 3
255 -
256 -fileprivate extension RustCallStatus {
257 - init() {
258 - self.init(
259 - code: CALL_SUCCESS,
260 - errorBuf: RustBuffer.init(
261 - capacity: 0,
262 - len: 0,
263 - data: nil
264 - )
265 - )
266 - }
267 -}
268 -
269 -private func rustCall<T>(_ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T {
270 - let neverThrow: ((RustBuffer) throws -> Never)? = nil
271 - return try makeRustCall(callback, errorHandler: neverThrow)
272 -}
273 -
274 -private func rustCallWithError<T, E: Swift.Error>(
275 - _ errorHandler: @escaping (RustBuffer) throws -> E,
276 - _ callback: (UnsafeMutablePointer<RustCallStatus>) -> T) throws -> T {
277 - try makeRustCall(callback, errorHandler: errorHandler)
278 -}
279 -
280 -private func makeRustCall<T, E: Swift.Error>(
281 - _ callback: (UnsafeMutablePointer<RustCallStatus>) -> T,
282 - errorHandler: ((RustBuffer) throws -> E)?
283 -) throws -> T {
284 - uniffiEnsureSmbcloudAuthInitialized()
285 - var callStatus = RustCallStatus.init()
286 - let returnedVal = callback(&callStatus)
287 - try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler)
288 - return returnedVal
289 -}
290 -
291 -private func uniffiCheckCallStatus<E: Swift.Error>(
292 - callStatus: RustCallStatus,
293 - errorHandler: ((RustBuffer) throws -> E)?
294 -) throws {
295 - switch callStatus.code {
296 - case CALL_SUCCESS:
297 - return
298 -
299 - case CALL_ERROR:
300 - if let errorHandler = errorHandler {
301 - throw try errorHandler(callStatus.errorBuf)
302 - } else {
303 - callStatus.errorBuf.deallocate()
304 - throw UniffiInternalError.unexpectedRustCallError
305 - }
306 -
307 - case CALL_UNEXPECTED_ERROR:
308 - // When the rust code sees a panic, it tries to construct a RustBuffer
309 - // with the message. But if that code panics, then it just sends back
310 - // an empty buffer.
311 - if callStatus.errorBuf.len > 0 {
312 - throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf))
313 - } else {
314 - callStatus.errorBuf.deallocate()
315 - throw UniffiInternalError.rustPanic("Rust panic")
316 - }
317 -
318 - case CALL_CANCELLED:
319 - fatalError("Cancellation not supported yet")
320 -
321 - default:
322 - throw UniffiInternalError.unexpectedRustCallStatusCode
323 - }
324 -}
325 -
326 -private func uniffiTraitInterfaceCall<T>(
327 - callStatus: UnsafeMutablePointer<RustCallStatus>,
328 - makeCall: () throws -> T,
329 - writeReturn: (T) -> ()
330 -) {
331 - do {
332 - try writeReturn(makeCall())
333 - } catch let error {
334 - callStatus.pointee.code = CALL_UNEXPECTED_ERROR
335 - callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error))
336 - }
337 -}
338 -
339 -private func uniffiTraitInterfaceCallWithError<T, E>(
340 - callStatus: UnsafeMutablePointer<RustCallStatus>,
341 - makeCall: () throws -> T,
342 - writeReturn: (T) -> (),
343 - lowerError: (E) -> RustBuffer
344 -) {
345 - do {
346 - try writeReturn(makeCall())
347 - } catch let error as E {
348 - callStatus.pointee.code = CALL_ERROR
349 - callStatus.pointee.errorBuf = lowerError(error)
350 - } catch {
351 - callStatus.pointee.code = CALL_UNEXPECTED_ERROR
352 - callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error))
353 - }
354 -}
355 -fileprivate final class UniffiHandleMap<T>: @unchecked Sendable {
356 - // All mutation happens with this lock held, which is why we implement @unchecked Sendable.
357 - private let lock = NSLock()
358 - private var map: [UInt64: T] = [:]
359 - private var currentHandle: UInt64 = 1
360 -
361 - func insert(obj: T) -> UInt64 {
362 - lock.withLock {
363 - let handle = currentHandle
364 - currentHandle += 1
365 - map[handle] = obj
366 - return handle
367 - }
368 - }
369 -
370 - func get(handle: UInt64) throws -> T {
371 - try lock.withLock {
372 - guard let obj = map[handle] else {
373 - throw UniffiInternalError.unexpectedStaleHandle
374 - }
375 - return obj
376 - }
377 - }
378 -
379 - @discardableResult
380 - func remove(handle: UInt64) throws -> T {
381 - try lock.withLock {
382 - guard let obj = map.removeValue(forKey: handle) else {
383 - throw UniffiInternalError.unexpectedStaleHandle
384 - }
385 - return obj
386 - }
387 - }
388 -
389 - var count: Int {
390 - get {
391 - map.count
392 - }
393 - }
394 -}
395 -
396 -
397 -// Public interface members begin here.
398 -
399 -
400 -#if swift(>=5.8)
401 -@_documentation(visibility: private)
402 -#endif
403 -fileprivate struct FfiConverterUInt32: FfiConverterPrimitive {
404 - typealias FfiType = UInt32
405 - typealias SwiftType = UInt32
406 -
407 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 {
408 - return try lift(readInt(&buf))
409 - }
410 -
411 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
412 - writeInt(&buf, lower(value))
413 - }
414 -}
415 -
416 -#if swift(>=5.8)
417 -@_documentation(visibility: private)
418 -#endif
419 -fileprivate struct FfiConverterInt32: FfiConverterPrimitive {
420 - typealias FfiType = Int32
421 - typealias SwiftType = Int32
422 -
423 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int32 {
424 - return try lift(readInt(&buf))
425 - }
426 -
427 - public static func write(_ value: Int32, into buf: inout [UInt8]) {
428 - writeInt(&buf, lower(value))
429 - }
430 -}
431 -
432 -#if swift(>=5.8)
433 -@_documentation(visibility: private)
434 -#endif
435 -fileprivate struct FfiConverterUInt64: FfiConverterPrimitive {
436 - typealias FfiType = UInt64
437 - typealias SwiftType = UInt64
438 -
439 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 {
440 - return try lift(readInt(&buf))
441 - }
442 -
443 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
444 - writeInt(&buf, lower(value))
445 - }
446 -}
447 -
448 -#if swift(>=5.8)
449 -@_documentation(visibility: private)
450 -#endif
451 -fileprivate struct FfiConverterBool : FfiConverter {
452 - typealias FfiType = Int8
453 - typealias SwiftType = Bool
454 -
455 - public static func lift(_ value: Int8) throws -> Bool {
456 - return value != 0
457 - }
458 -
459 - public static func lower(_ value: Bool) -> Int8 {
460 - return value ? 1 : 0
461 - }
462 -
463 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool {
464 - return try lift(readInt(&buf))
465 - }
466 -
467 - public static func write(_ value: Bool, into buf: inout [UInt8]) {
468 - writeInt(&buf, lower(value))
469 - }
470 -}
471 -
472 -#if swift(>=5.8)
473 -@_documentation(visibility: private)
474 -#endif
475 -fileprivate struct FfiConverterString: FfiConverter {
476 - typealias SwiftType = String
477 - typealias FfiType = RustBuffer
478 -
479 - public static func lift(_ value: RustBuffer) throws -> String {
480 - defer {
481 - value.deallocate()
482 - }
483 - if value.data == nil {
484 - return String()
485 - }
486 - let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len))
487 - return String(bytes: bytes, encoding: String.Encoding.utf8)!
488 - }
489 -
490 - public static func lower(_ value: String) -> RustBuffer {
491 - return value.utf8CString.withUnsafeBufferPointer { ptr in
492 - // The swift string gives us int8_t, we want uint8_t.
493 - ptr.withMemoryRebound(to: UInt8.self) { ptr in
494 - // The swift string gives us a trailing null byte, we don't want it.
495 - let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1))
496 - return RustBuffer.from(buf)
497 - }
498 - }
499 - }
500 -
501 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String {
502 - let len: Int32 = try readInt(&buf)
503 - return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)!
504 - }
505 -
506 - public static func write(_ value: String, into buf: inout [UInt8]) {
507 - let len = Int32(value.utf8.count)
508 - writeInt(&buf, len)
509 - writeBytes(&buf, value.utf8)
510 - }
511 -}
512 -
513 -
514 -
515 -
516 -public protocol SmbCloudAuthProtocol: AnyObject, Sendable {
517 -
518 - func buildAppleAuthorizationRequest(redirectUri: String, state: String?) throws -> SmbCloudAppleAuthorizationRequest
519 -
520 - func buildOidcAuthorizationRequest(oidcClientId: String, redirectUri: String) throws -> SmbCloudAuthorizationRequest
521 -
522 - func exchangeOidcCode(oidcClientId: String, redirectUri: String, code: String, codeVerifier: String) async throws -> SmbCloudTokenResponse
523 -
524 - func getOidcUserinfo(accessToken: String, tenantId: String?) async throws -> SmbCloudUserInfo
525 -
526 - func login(email: String, password: String) async throws -> SmbCloudAccountStatus
527 -
528 - func logout(accessToken: String) async throws
529 -
530 - func me(accessToken: String) async throws -> SmbCloudUser
531 -
532 - func parseAppleCallbackUrl(callbackUrl: String, expectedState: String?) throws -> SmbCloudAppleAuthSession
533 -
534 - func parseOidcCallbackUrl(callbackUrl: String) throws -> SmbCloudCallbackPayload
535 -
536 - func removeAccount(accessToken: String) async throws
537 -
538 - func signup(email: String, password: String) async throws -> SmbCloudSignupResult
539 -
540 -}
541 -open class SmbCloudAuth: SmbCloudAuthProtocol, @unchecked Sendable {
542 - fileprivate let pointer: UnsafeMutableRawPointer!
543 -
544 - /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly.
545 -#if swift(>=5.8)
546 - @_documentation(visibility: private)
547 -#endif
548 - public struct NoPointer {
549 - public init() {}
550 - }
551 -
552 - // TODO: We'd like this to be `private` but for Swifty reasons,
553 - // we can't implement `FfiConverter` without making this `required` and we can't
554 - // make it `required` without making it `public`.
555 -#if swift(>=5.8)
556 - @_documentation(visibility: private)
557 -#endif
558 - required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
559 - self.pointer = pointer
560 - }
561 -
562 - // This constructor can be used to instantiate a fake object.
563 - // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject].
564 - //
565 - // - Warning:
566 - // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash.
567 -#if swift(>=5.8)
568 - @_documentation(visibility: private)
569 -#endif
570 - public init(noPointer: NoPointer) {
571 - self.pointer = nil
572 - }
573 -
574 -#if swift(>=5.8)
575 - @_documentation(visibility: private)
576 -#endif
577 - public func uniffiClonePointer() -> UnsafeMutableRawPointer {
578 - return try! rustCall { uniffi_smbcloud_auth_fn_clone_smbcloudauth(self.pointer, $0) }
579 - }
580 -public convenience init(environment: SmbCloudEnvironment, appId: String, appSecret: String) {
581 - let pointer =
582 - try! rustCall() {
583 - uniffi_smbcloud_auth_fn_constructor_smbcloudauth_new(
584 - FfiConverterTypeSmbCloudEnvironment_lower(environment),
585 - FfiConverterString.lower(appId),
586 - FfiConverterString.lower(appSecret),$0
587 - )
588 -}
589 - self.init(unsafeFromRawPointer: pointer)
590 -}
591 -
592 - deinit {
593 - guard let pointer = pointer else {
594 - return
595 - }
596 -
597 - try! rustCall { uniffi_smbcloud_auth_fn_free_smbcloudauth(pointer, $0) }
598 - }
599 -
600 -
601 -
602 -
603 -open func buildAppleAuthorizationRequest(redirectUri: String, state: String?)throws -> SmbCloudAppleAuthorizationRequest {
604 - return try FfiConverterTypeSmbCloudAppleAuthorizationRequest_lift(try rustCallWithError(FfiConverterTypeSmbCloudAuthError_lift) {
605 - uniffi_smbcloud_auth_fn_method_smbcloudauth_build_apple_authorization_request(self.uniffiClonePointer(),
606 - FfiConverterString.lower(redirectUri),
607 - FfiConverterOptionString.lower(state),$0
608 - )
609 -})
610 -}
611 -
612 -open func buildOidcAuthorizationRequest(oidcClientId: String, redirectUri: String)throws -> SmbCloudAuthorizationRequest {
613 - return try FfiConverterTypeSmbCloudAuthorizationRequest_lift(try rustCallWithError(FfiConverterTypeSmbCloudAuthError_lift) {
614 - uniffi_smbcloud_auth_fn_method_smbcloudauth_build_oidc_authorization_request(self.uniffiClonePointer(),
615 - FfiConverterString.lower(oidcClientId),
616 - FfiConverterString.lower(redirectUri),$0
617 - )
618 -})
619 -}
620 -
621 -open func exchangeOidcCode(oidcClientId: String, redirectUri: String, code: String, codeVerifier: String)async throws -> SmbCloudTokenResponse {
622 - return
623 - try await uniffiRustCallAsync(
624 - rustFutureFunc: {
625 - uniffi_smbcloud_auth_fn_method_smbcloudauth_exchange_oidc_code(
626 - self.uniffiClonePointer(),
627 - FfiConverterString.lower(oidcClientId),FfiConverterString.lower(redirectUri),FfiConverterString.lower(code),FfiConverterString.lower(codeVerifier)
628 - )
629 - },
630 - pollFunc: ffi_smbcloud_auth_rust_future_poll_rust_buffer,
631 - completeFunc: ffi_smbcloud_auth_rust_future_complete_rust_buffer,
632 - freeFunc: ffi_smbcloud_auth_rust_future_free_rust_buffer,
633 - liftFunc: FfiConverterTypeSmbCloudTokenResponse_lift,
634 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
635 - )
636 -}
637 -
638 -open func getOidcUserinfo(accessToken: String, tenantId: String?)async throws -> SmbCloudUserInfo {
639 - return
640 - try await uniffiRustCallAsync(
641 - rustFutureFunc: {
642 - uniffi_smbcloud_auth_fn_method_smbcloudauth_get_oidc_userinfo(
643 - self.uniffiClonePointer(),
644 - FfiConverterString.lower(accessToken),FfiConverterOptionString.lower(tenantId)
645 - )
646 - },
647 - pollFunc: ffi_smbcloud_auth_rust_future_poll_rust_buffer,
648 - completeFunc: ffi_smbcloud_auth_rust_future_complete_rust_buffer,
649 - freeFunc: ffi_smbcloud_auth_rust_future_free_rust_buffer,
650 - liftFunc: FfiConverterTypeSmbCloudUserInfo_lift,
651 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
652 - )
653 -}
654 -
655 -open func login(email: String, password: String)async throws -> SmbCloudAccountStatus {
656 - return
657 - try await uniffiRustCallAsync(
658 - rustFutureFunc: {
659 - uniffi_smbcloud_auth_fn_method_smbcloudauth_login(
660 - self.uniffiClonePointer(),
661 - FfiConverterString.lower(email),FfiConverterString.lower(password)
662 - )
663 - },
664 - pollFunc: ffi_smbcloud_auth_rust_future_poll_rust_buffer,
665 - completeFunc: ffi_smbcloud_auth_rust_future_complete_rust_buffer,
666 - freeFunc: ffi_smbcloud_auth_rust_future_free_rust_buffer,
667 - liftFunc: FfiConverterTypeSmbCloudAccountStatus_lift,
668 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
669 - )
670 -}
671 -
672 -open func logout(accessToken: String)async throws {
673 - return
674 - try await uniffiRustCallAsync(
675 - rustFutureFunc: {
676 - uniffi_smbcloud_auth_fn_method_smbcloudauth_logout(
677 - self.uniffiClonePointer(),
678 - FfiConverterString.lower(accessToken)
679 - )
680 - },
681 - pollFunc: ffi_smbcloud_auth_rust_future_poll_void,
682 - completeFunc: ffi_smbcloud_auth_rust_future_complete_void,
683 - freeFunc: ffi_smbcloud_auth_rust_future_free_void,
684 - liftFunc: { $0 },
685 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
686 - )
687 -}
688 -
689 -open func me(accessToken: String)async throws -> SmbCloudUser {
690 - return
691 - try await uniffiRustCallAsync(
692 - rustFutureFunc: {
693 - uniffi_smbcloud_auth_fn_method_smbcloudauth_me(
694 - self.uniffiClonePointer(),
695 - FfiConverterString.lower(accessToken)
696 - )
697 - },
698 - pollFunc: ffi_smbcloud_auth_rust_future_poll_rust_buffer,
699 - completeFunc: ffi_smbcloud_auth_rust_future_complete_rust_buffer,
700 - freeFunc: ffi_smbcloud_auth_rust_future_free_rust_buffer,
701 - liftFunc: FfiConverterTypeSmbCloudUser_lift,
702 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
703 - )
704 -}
705 -
706 -open func parseAppleCallbackUrl(callbackUrl: String, expectedState: String?)throws -> SmbCloudAppleAuthSession {
707 - return try FfiConverterTypeSmbCloudAppleAuthSession_lift(try rustCallWithError(FfiConverterTypeSmbCloudAuthError_lift) {
708 - uniffi_smbcloud_auth_fn_method_smbcloudauth_parse_apple_callback_url(self.uniffiClonePointer(),
709 - FfiConverterString.lower(callbackUrl),
710 - FfiConverterOptionString.lower(expectedState),$0
711 - )
712 -})
713 -}
714 -
715 -open func parseOidcCallbackUrl(callbackUrl: String)throws -> SmbCloudCallbackPayload {
716 - return try FfiConverterTypeSmbCloudCallbackPayload_lift(try rustCallWithError(FfiConverterTypeSmbCloudAuthError_lift) {
717 - uniffi_smbcloud_auth_fn_method_smbcloudauth_parse_oidc_callback_url(self.uniffiClonePointer(),
718 - FfiConverterString.lower(callbackUrl),$0
719 - )
720 -})
721 -}
722 -
723 -open func removeAccount(accessToken: String)async throws {
724 - return
725 - try await uniffiRustCallAsync(
726 - rustFutureFunc: {
727 - uniffi_smbcloud_auth_fn_method_smbcloudauth_remove_account(
728 - self.uniffiClonePointer(),
729 - FfiConverterString.lower(accessToken)
730 - )
731 - },
732 - pollFunc: ffi_smbcloud_auth_rust_future_poll_void,
733 - completeFunc: ffi_smbcloud_auth_rust_future_complete_void,
734 - freeFunc: ffi_smbcloud_auth_rust_future_free_void,
735 - liftFunc: { $0 },
736 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
737 - )
738 -}
739 -
740 -open func signup(email: String, password: String)async throws -> SmbCloudSignupResult {
741 - return
742 - try await uniffiRustCallAsync(
743 - rustFutureFunc: {
744 - uniffi_smbcloud_auth_fn_method_smbcloudauth_signup(
745 - self.uniffiClonePointer(),
746 - FfiConverterString.lower(email),FfiConverterString.lower(password)
747 - )
748 - },
749 - pollFunc: ffi_smbcloud_auth_rust_future_poll_rust_buffer,
750 - completeFunc: ffi_smbcloud_auth_rust_future_complete_rust_buffer,
751 - freeFunc: ffi_smbcloud_auth_rust_future_free_rust_buffer,
752 - liftFunc: FfiConverterTypeSmbCloudSignupResult_lift,
753 - errorHandler: FfiConverterTypeSmbCloudAuthError_lift
754 - )
755 -}
756 -
757 -
758 -}
759 -
760 -
761 -#if swift(>=5.8)
762 -@_documentation(visibility: private)
763 -#endif
764 -public struct FfiConverterTypeSmbCloudAuth: FfiConverter {
765 -
766 - typealias FfiType = UnsafeMutableRawPointer
767 - typealias SwiftType = SmbCloudAuth
768 -
769 - public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> SmbCloudAuth {
770 - return SmbCloudAuth(unsafeFromRawPointer: pointer)
771 - }
772 -
773 - public static func lower(_ value: SmbCloudAuth) -> UnsafeMutableRawPointer {
774 - return value.uniffiClonePointer()
775 - }
776 -
777 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAuth {
778 - let v: UInt64 = try readInt(&buf)
779 - // The Rust code won't compile if a pointer won't fit in a UInt64.
780 - // We have to go via `UInt` because that's the thing that's the size of a pointer.
781 - let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v))
782 - if (ptr == nil) {
783 - throw UniffiInternalError.unexpectedNullPointer
784 - }
785 - return try lift(ptr!)
786 - }
787 -
788 - public static func write(_ value: SmbCloudAuth, into buf: inout [UInt8]) {
789 - // This fiddling is because `Int` is the thing that's the same size as a pointer.
790 - // The Rust code won't compile if a pointer won't fit in a `UInt64`.
791 - writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value)))))
792 - }
793 -}
794 -
795 -
796 -#if swift(>=5.8)
797 -@_documentation(visibility: private)
798 -#endif
799 -public func FfiConverterTypeSmbCloudAuth_lift(_ pointer: UnsafeMutableRawPointer) throws -> SmbCloudAuth {
800 - return try FfiConverterTypeSmbCloudAuth.lift(pointer)
801 -}
802 -
803 -#if swift(>=5.8)
804 -@_documentation(visibility: private)
805 -#endif
806 -public func FfiConverterTypeSmbCloudAuth_lower(_ value: SmbCloudAuth) -> UnsafeMutableRawPointer {
807 - return FfiConverterTypeSmbCloudAuth.lower(value)
808 -}
809 -
810 -
811 -
812 -
813 -public struct SmbCloudAppleAuthSession {
814 - public var accessToken: String
815 - public var refreshToken: String?
816 - public var email: String?
817 - public var name: String?
818 - public var provider: String
819 - public var providerAccountId: String
820 - public var state: String?
821 -
822 - // Default memberwise initializers are never public by default, so we
823 - // declare one manually.
824 - public init(accessToken: String, refreshToken: String?, email: String?, name: String?, provider: String, providerAccountId: String, state: String?) {
825 - self.accessToken = accessToken
826 - self.refreshToken = refreshToken
827 - self.email = email
828 - self.name = name
829 - self.provider = provider
830 - self.providerAccountId = providerAccountId
831 - self.state = state
832 - }
833 -}
834 -
835 -#if compiler(>=6)
836 -extension SmbCloudAppleAuthSession: Sendable {}
837 -#endif
838 -
839 -
840 -extension SmbCloudAppleAuthSession: Equatable, Hashable {
841 - public static func ==(lhs: SmbCloudAppleAuthSession, rhs: SmbCloudAppleAuthSession) -> Bool {
842 - if lhs.accessToken != rhs.accessToken {
843 - return false
844 - }
845 - if lhs.refreshToken != rhs.refreshToken {
846 - return false
847 - }
848 - if lhs.email != rhs.email {
849 - return false
850 - }
851 - if lhs.name != rhs.name {
852 - return false
853 - }
854 - if lhs.provider != rhs.provider {
855 - return false
856 - }
857 - if lhs.providerAccountId != rhs.providerAccountId {
858 - return false
859 - }
860 - if lhs.state != rhs.state {
861 - return false
862 - }
863 - return true
864 - }
865 -
866 - public func hash(into hasher: inout Hasher) {
867 - hasher.combine(accessToken)
868 - hasher.combine(refreshToken)
869 - hasher.combine(email)
870 - hasher.combine(name)
871 - hasher.combine(provider)
872 - hasher.combine(providerAccountId)
873 - hasher.combine(state)
874 - }
875 -}
876 -
877 -
878 -
879 -#if swift(>=5.8)
880 -@_documentation(visibility: private)
881 -#endif
882 -public struct FfiConverterTypeSmbCloudAppleAuthSession: FfiConverterRustBuffer {
883 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAppleAuthSession {
884 - return
885 - try SmbCloudAppleAuthSession(
886 - accessToken: FfiConverterString.read(from: &buf),
887 - refreshToken: FfiConverterOptionString.read(from: &buf),
888 - email: FfiConverterOptionString.read(from: &buf),
889 - name: FfiConverterOptionString.read(from: &buf),
890 - provider: FfiConverterString.read(from: &buf),
891 - providerAccountId: FfiConverterString.read(from: &buf),
892 - state: FfiConverterOptionString.read(from: &buf)
893 - )
894 - }
895 -
896 - public static func write(_ value: SmbCloudAppleAuthSession, into buf: inout [UInt8]) {
897 - FfiConverterString.write(value.accessToken, into: &buf)
898 - FfiConverterOptionString.write(value.refreshToken, into: &buf)
899 - FfiConverterOptionString.write(value.email, into: &buf)
900 - FfiConverterOptionString.write(value.name, into: &buf)
901 - FfiConverterString.write(value.provider, into: &buf)
902 - FfiConverterString.write(value.providerAccountId, into: &buf)
903 - FfiConverterOptionString.write(value.state, into: &buf)
904 - }
905 -}
906 -
907 -
908 -#if swift(>=5.8)
909 -@_documentation(visibility: private)
910 -#endif
911 -public func FfiConverterTypeSmbCloudAppleAuthSession_lift(_ buf: RustBuffer) throws -> SmbCloudAppleAuthSession {
912 - return try FfiConverterTypeSmbCloudAppleAuthSession.lift(buf)
913 -}
914 -
915 -#if swift(>=5.8)
916 -@_documentation(visibility: private)
917 -#endif
918 -public func FfiConverterTypeSmbCloudAppleAuthSession_lower(_ value: SmbCloudAppleAuthSession) -> RustBuffer {
919 - return FfiConverterTypeSmbCloudAppleAuthSession.lower(value)
920 -}
921 -
922 -
923 -public struct SmbCloudAppleAuthorizationRequest {
924 - public var authorizeUrl: String
925 - public var redirectUri: String
926 - public var state: String
927 -
928 - // Default memberwise initializers are never public by default, so we
929 - // declare one manually.
930 - public init(authorizeUrl: String, redirectUri: String, state: String) {
931 - self.authorizeUrl = authorizeUrl
932 - self.redirectUri = redirectUri
933 - self.state = state
934 - }
935 -}
936 -
937 -#if compiler(>=6)
938 -extension SmbCloudAppleAuthorizationRequest: Sendable {}
939 -#endif
940 -
941 -
942 -extension SmbCloudAppleAuthorizationRequest: Equatable, Hashable {
943 - public static func ==(lhs: SmbCloudAppleAuthorizationRequest, rhs: SmbCloudAppleAuthorizationRequest) -> Bool {
944 - if lhs.authorizeUrl != rhs.authorizeUrl {
945 - return false
946 - }
947 - if lhs.redirectUri != rhs.redirectUri {
948 - return false
949 - }
950 - if lhs.state != rhs.state {
951 - return false
952 - }
953 - return true
954 - }
955 -
956 - public func hash(into hasher: inout Hasher) {
957 - hasher.combine(authorizeUrl)
958 - hasher.combine(redirectUri)
959 - hasher.combine(state)
960 - }
961 -}
962 -
963 -
964 -
965 -#if swift(>=5.8)
966 -@_documentation(visibility: private)
967 -#endif
968 -public struct FfiConverterTypeSmbCloudAppleAuthorizationRequest: FfiConverterRustBuffer {
969 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAppleAuthorizationRequest {
970 - return
971 - try SmbCloudAppleAuthorizationRequest(
972 - authorizeUrl: FfiConverterString.read(from: &buf),
973 - redirectUri: FfiConverterString.read(from: &buf),
974 - state: FfiConverterString.read(from: &buf)
975 - )
976 - }
977 -
978 - public static func write(_ value: SmbCloudAppleAuthorizationRequest, into buf: inout [UInt8]) {
979 - FfiConverterString.write(value.authorizeUrl, into: &buf)
980 - FfiConverterString.write(value.redirectUri, into: &buf)
981 - FfiConverterString.write(value.state, into: &buf)
982 - }
983 -}
984 -
985 -
986 -#if swift(>=5.8)
987 -@_documentation(visibility: private)
988 -#endif
989 -public func FfiConverterTypeSmbCloudAppleAuthorizationRequest_lift(_ buf: RustBuffer) throws -> SmbCloudAppleAuthorizationRequest {
990 - return try FfiConverterTypeSmbCloudAppleAuthorizationRequest.lift(buf)
991 -}
992 -
993 -#if swift(>=5.8)
994 -@_documentation(visibility: private)
995 -#endif
996 -public func FfiConverterTypeSmbCloudAppleAuthorizationRequest_lower(_ value: SmbCloudAppleAuthorizationRequest) -> RustBuffer {
997 - return FfiConverterTypeSmbCloudAppleAuthorizationRequest.lower(value)
998 -}
999 -
1000 -
1001 -public struct SmbCloudAuthorizationRequest {
1002 - public var authorizeUrl: String
1003 - public var redirectUri: String
1004 - public var state: String
1005 - public var codeVerifier: String
1006 -
1007 - // Default memberwise initializers are never public by default, so we
1008 - // declare one manually.
1009 - public init(authorizeUrl: String, redirectUri: String, state: String, codeVerifier: String) {
1010 - self.authorizeUrl = authorizeUrl
1011 - self.redirectUri = redirectUri
1012 - self.state = state
1013 - self.codeVerifier = codeVerifier
1014 - }
1015 -}
1016 -
1017 -#if compiler(>=6)
1018 -extension SmbCloudAuthorizationRequest: Sendable {}
1019 -#endif
1020 -
1021 -
1022 -extension SmbCloudAuthorizationRequest: Equatable, Hashable {
1023 - public static func ==(lhs: SmbCloudAuthorizationRequest, rhs: SmbCloudAuthorizationRequest) -> Bool {
1024 - if lhs.authorizeUrl != rhs.authorizeUrl {
1025 - return false
1026 - }
1027 - if lhs.redirectUri != rhs.redirectUri {
1028 - return false
1029 - }
1030 - if lhs.state != rhs.state {
1031 - return false
1032 - }
1033 - if lhs.codeVerifier != rhs.codeVerifier {
1034 - return false
1035 - }
1036 - return true
1037 - }
1038 -
1039 - public func hash(into hasher: inout Hasher) {
1040 - hasher.combine(authorizeUrl)
1041 - hasher.combine(redirectUri)
1042 - hasher.combine(state)
1043 - hasher.combine(codeVerifier)
1044 - }
1045 -}
1046 -
1047 -
1048 -
1049 -#if swift(>=5.8)
1050 -@_documentation(visibility: private)
1051 -#endif
1052 -public struct FfiConverterTypeSmbCloudAuthorizationRequest: FfiConverterRustBuffer {
1053 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAuthorizationRequest {
1054 - return
1055 - try SmbCloudAuthorizationRequest(
1056 - authorizeUrl: FfiConverterString.read(from: &buf),
1057 - redirectUri: FfiConverterString.read(from: &buf),
1058 - state: FfiConverterString.read(from: &buf),
1059 - codeVerifier: FfiConverterString.read(from: &buf)
1060 - )
1061 - }
1062 -
1063 - public static func write(_ value: SmbCloudAuthorizationRequest, into buf: inout [UInt8]) {
1064 - FfiConverterString.write(value.authorizeUrl, into: &buf)
1065 - FfiConverterString.write(value.redirectUri, into: &buf)
1066 - FfiConverterString.write(value.state, into: &buf)
1067 - FfiConverterString.write(value.codeVerifier, into: &buf)
1068 - }
1069 -}
1070 -
1071 -
1072 -#if swift(>=5.8)
1073 -@_documentation(visibility: private)
1074 -#endif
1075 -public func FfiConverterTypeSmbCloudAuthorizationRequest_lift(_ buf: RustBuffer) throws -> SmbCloudAuthorizationRequest {
1076 - return try FfiConverterTypeSmbCloudAuthorizationRequest.lift(buf)
1077 -}
1078 -
1079 -#if swift(>=5.8)
1080 -@_documentation(visibility: private)
1081 -#endif
1082 -public func FfiConverterTypeSmbCloudAuthorizationRequest_lower(_ value: SmbCloudAuthorizationRequest) -> RustBuffer {
1083 - return FfiConverterTypeSmbCloudAuthorizationRequest.lower(value)
1084 -}
1085 -
1086 -
1087 -public struct SmbCloudCallbackPayload {
1088 - public var code: String
1089 - public var state: String
1090 -
1091 - // Default memberwise initializers are never public by default, so we
1092 - // declare one manually.
1093 - public init(code: String, state: String) {
1094 - self.code = code
1095 - self.state = state
1096 - }
1097 -}
1098 -
1099 -#if compiler(>=6)
1100 -extension SmbCloudCallbackPayload: Sendable {}
1101 -#endif
1102 -
1103 -
1104 -extension SmbCloudCallbackPayload: Equatable, Hashable {
1105 - public static func ==(lhs: SmbCloudCallbackPayload, rhs: SmbCloudCallbackPayload) -> Bool {
1106 - if lhs.code != rhs.code {
1107 - return false
1108 - }
1109 - if lhs.state != rhs.state {
1110 - return false
1111 - }
1112 - return true
1113 - }
1114 -
1115 - public func hash(into hasher: inout Hasher) {
1116 - hasher.combine(code)
1117 - hasher.combine(state)
1118 - }
1119 -}
1120 -
1121 -
1122 -
1123 -#if swift(>=5.8)
1124 -@_documentation(visibility: private)
1125 -#endif
1126 -public struct FfiConverterTypeSmbCloudCallbackPayload: FfiConverterRustBuffer {
1127 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudCallbackPayload {
1128 - return
1129 - try SmbCloudCallbackPayload(
1130 - code: FfiConverterString.read(from: &buf),
1131 - state: FfiConverterString.read(from: &buf)
1132 - )
1133 - }
1134 -
1135 - public static func write(_ value: SmbCloudCallbackPayload, into buf: inout [UInt8]) {
1136 - FfiConverterString.write(value.code, into: &buf)
1137 - FfiConverterString.write(value.state, into: &buf)
1138 - }
1139 -}
1140 -
1141 -
1142 -#if swift(>=5.8)
1143 -@_documentation(visibility: private)
1144 -#endif
1145 -public func FfiConverterTypeSmbCloudCallbackPayload_lift(_ buf: RustBuffer) throws -> SmbCloudCallbackPayload {
1146 - return try FfiConverterTypeSmbCloudCallbackPayload.lift(buf)
1147 -}
1148 -
1149 -#if swift(>=5.8)
1150 -@_documentation(visibility: private)
1151 -#endif
1152 -public func FfiConverterTypeSmbCloudCallbackPayload_lower(_ value: SmbCloudCallbackPayload) -> RustBuffer {
1153 - return FfiConverterTypeSmbCloudCallbackPayload.lower(value)
1154 -}
1155 -
1156 -
1157 -public struct SmbCloudSignupResult {
1158 - public var code: Int32?
1159 - public var message: String
1160 - public var userId: Int32?
1161 - public var userEmail: String?
1162 - public var userCreatedAt: String?
1163 -
1164 - // Default memberwise initializers are never public by default, so we
1165 - // declare one manually.
1166 - public init(code: Int32?, message: String, userId: Int32?, userEmail: String?, userCreatedAt: String?) {
1167 - self.code = code
1168 - self.message = message
1169 - self.userId = userId
1170 - self.userEmail = userEmail
1171 - self.userCreatedAt = userCreatedAt
1172 - }
1173 -}
1174 -
1175 -#if compiler(>=6)
1176 -extension SmbCloudSignupResult: Sendable {}
1177 -#endif
1178 -
1179 -
1180 -extension SmbCloudSignupResult: Equatable, Hashable {
1181 - public static func ==(lhs: SmbCloudSignupResult, rhs: SmbCloudSignupResult) -> Bool {
1182 - if lhs.code != rhs.code {
1183 - return false
1184 - }
1185 - if lhs.message != rhs.message {
1186 - return false
1187 - }
1188 - if lhs.userId != rhs.userId {
1189 - return false
1190 - }
1191 - if lhs.userEmail != rhs.userEmail {
1192 - return false
1193 - }
1194 - if lhs.userCreatedAt != rhs.userCreatedAt {
1195 - return false
1196 - }
1197 - return true
1198 - }
1199 -
1200 - public func hash(into hasher: inout Hasher) {
1201 - hasher.combine(code)
1202 - hasher.combine(message)
1203 - hasher.combine(userId)
1204 - hasher.combine(userEmail)
1205 - hasher.combine(userCreatedAt)
1206 - }
1207 -}
1208 -
1209 -
1210 -
1211 -#if swift(>=5.8)
1212 -@_documentation(visibility: private)
1213 -#endif
1214 -public struct FfiConverterTypeSmbCloudSignupResult: FfiConverterRustBuffer {
1215 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudSignupResult {
1216 - return
1217 - try SmbCloudSignupResult(
1218 - code: FfiConverterOptionInt32.read(from: &buf),
1219 - message: FfiConverterString.read(from: &buf),
1220 - userId: FfiConverterOptionInt32.read(from: &buf),
1221 - userEmail: FfiConverterOptionString.read(from: &buf),
1222 - userCreatedAt: FfiConverterOptionString.read(from: &buf)
1223 - )
1224 - }
1225 -
1226 - public static func write(_ value: SmbCloudSignupResult, into buf: inout [UInt8]) {
1227 - FfiConverterOptionInt32.write(value.code, into: &buf)
1228 - FfiConverterString.write(value.message, into: &buf)
1229 - FfiConverterOptionInt32.write(value.userId, into: &buf)
1230 - FfiConverterOptionString.write(value.userEmail, into: &buf)
1231 - FfiConverterOptionString.write(value.userCreatedAt, into: &buf)
1232 - }
1233 -}
1234 -
1235 -
1236 -#if swift(>=5.8)
1237 -@_documentation(visibility: private)
1238 -#endif
1239 -public func FfiConverterTypeSmbCloudSignupResult_lift(_ buf: RustBuffer) throws -> SmbCloudSignupResult {
1240 - return try FfiConverterTypeSmbCloudSignupResult.lift(buf)
1241 -}
1242 -
1243 -#if swift(>=5.8)
1244 -@_documentation(visibility: private)
1245 -#endif
1246 -public func FfiConverterTypeSmbCloudSignupResult_lower(_ value: SmbCloudSignupResult) -> RustBuffer {
1247 - return FfiConverterTypeSmbCloudSignupResult.lower(value)
1248 -}
1249 -
1250 -
1251 -public struct SmbCloudTokenResponse {
1252 - public var accessToken: String
1253 - public var tokenType: String
1254 - public var expiresIn: Int32?
1255 - public var refreshToken: String?
1256 - public var scope: String?
1257 - public var idToken: String?
1258 -
1259 - // Default memberwise initializers are never public by default, so we
1260 - // declare one manually.
1261 - public init(accessToken: String, tokenType: String, expiresIn: Int32?, refreshToken: String?, scope: String?, idToken: String?) {
1262 - self.accessToken = accessToken
1263 - self.tokenType = tokenType
1264 - self.expiresIn = expiresIn
1265 - self.refreshToken = refreshToken
1266 - self.scope = scope
1267 - self.idToken = idToken
1268 - }
1269 -}
1270 -
1271 -#if compiler(>=6)
1272 -extension SmbCloudTokenResponse: Sendable {}
1273 -#endif
1274 -
1275 -
1276 -extension SmbCloudTokenResponse: Equatable, Hashable {
1277 - public static func ==(lhs: SmbCloudTokenResponse, rhs: SmbCloudTokenResponse) -> Bool {
1278 - if lhs.accessToken != rhs.accessToken {
1279 - return false
1280 - }
1281 - if lhs.tokenType != rhs.tokenType {
1282 - return false
1283 - }
1284 - if lhs.expiresIn != rhs.expiresIn {
1285 - return false
1286 - }
1287 - if lhs.refreshToken != rhs.refreshToken {
1288 - return false
1289 - }
1290 - if lhs.scope != rhs.scope {
1291 - return false
1292 - }
1293 - if lhs.idToken != rhs.idToken {
1294 - return false
1295 - }
1296 - return true
1297 - }
1298 -
1299 - public func hash(into hasher: inout Hasher) {
1300 - hasher.combine(accessToken)
1301 - hasher.combine(tokenType)
1302 - hasher.combine(expiresIn)
1303 - hasher.combine(refreshToken)
1304 - hasher.combine(scope)
1305 - hasher.combine(idToken)
1306 - }
1307 -}
1308 -
1309 -
1310 -
1311 -#if swift(>=5.8)
1312 -@_documentation(visibility: private)
1313 -#endif
1314 -public struct FfiConverterTypeSmbCloudTokenResponse: FfiConverterRustBuffer {
1315 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudTokenResponse {
1316 - return
1317 - try SmbCloudTokenResponse(
1318 - accessToken: FfiConverterString.read(from: &buf),
1319 - tokenType: FfiConverterString.read(from: &buf),
1320 - expiresIn: FfiConverterOptionInt32.read(from: &buf),
1321 - refreshToken: FfiConverterOptionString.read(from: &buf),
1322 - scope: FfiConverterOptionString.read(from: &buf),
1323 - idToken: FfiConverterOptionString.read(from: &buf)
1324 - )
1325 - }
1326 -
1327 - public static func write(_ value: SmbCloudTokenResponse, into buf: inout [UInt8]) {
1328 - FfiConverterString.write(value.accessToken, into: &buf)
1329 - FfiConverterString.write(value.tokenType, into: &buf)
1330 - FfiConverterOptionInt32.write(value.expiresIn, into: &buf)
1331 - FfiConverterOptionString.write(value.refreshToken, into: &buf)
1332 - FfiConverterOptionString.write(value.scope, into: &buf)
1333 - FfiConverterOptionString.write(value.idToken, into: &buf)
1334 - }
1335 -}
1336 -
1337 -
1338 -#if swift(>=5.8)
1339 -@_documentation(visibility: private)
1340 -#endif
1341 -public func FfiConverterTypeSmbCloudTokenResponse_lift(_ buf: RustBuffer) throws -> SmbCloudTokenResponse {
1342 - return try FfiConverterTypeSmbCloudTokenResponse.lift(buf)
1343 -}
1344 -
1345 -#if swift(>=5.8)
1346 -@_documentation(visibility: private)
1347 -#endif
1348 -public func FfiConverterTypeSmbCloudTokenResponse_lower(_ value: SmbCloudTokenResponse) -> RustBuffer {
1349 - return FfiConverterTypeSmbCloudTokenResponse.lower(value)
1350 -}
1351 -
1352 -
1353 -public struct SmbCloudUser {
1354 - public var id: Int32
1355 - public var email: String
1356 - public var createdAt: String
1357 - public var updatedAt: String
1358 -
1359 - // Default memberwise initializers are never public by default, so we
1360 - // declare one manually.
1361 - public init(id: Int32, email: String, createdAt: String, updatedAt: String) {
1362 - self.id = id
1363 - self.email = email
1364 - self.createdAt = createdAt
1365 - self.updatedAt = updatedAt
1366 - }
1367 -}
1368 -
1369 -#if compiler(>=6)
1370 -extension SmbCloudUser: Sendable {}
1371 -#endif
1372 -
1373 -
1374 -extension SmbCloudUser: Equatable, Hashable {
1375 - public static func ==(lhs: SmbCloudUser, rhs: SmbCloudUser) -> Bool {
1376 - if lhs.id != rhs.id {
1377 - return false
1378 - }
1379 - if lhs.email != rhs.email {
1380 - return false
1381 - }
1382 - if lhs.createdAt != rhs.createdAt {
1383 - return false
1384 - }
1385 - if lhs.updatedAt != rhs.updatedAt {
1386 - return false
1387 - }
1388 - return true
1389 - }
1390 -
1391 - public func hash(into hasher: inout Hasher) {
1392 - hasher.combine(id)
1393 - hasher.combine(email)
1394 - hasher.combine(createdAt)
1395 - hasher.combine(updatedAt)
1396 - }
1397 -}
1398 -
1399 -
1400 -
1401 -#if swift(>=5.8)
1402 -@_documentation(visibility: private)
1403 -#endif
1404 -public struct FfiConverterTypeSmbCloudUser: FfiConverterRustBuffer {
1405 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudUser {
1406 - return
1407 - try SmbCloudUser(
1408 - id: FfiConverterInt32.read(from: &buf),
1409 - email: FfiConverterString.read(from: &buf),
1410 - createdAt: FfiConverterString.read(from: &buf),
1411 - updatedAt: FfiConverterString.read(from: &buf)
1412 - )
1413 - }
1414 -
1415 - public static func write(_ value: SmbCloudUser, into buf: inout [UInt8]) {
1416 - FfiConverterInt32.write(value.id, into: &buf)
1417 - FfiConverterString.write(value.email, into: &buf)
1418 - FfiConverterString.write(value.createdAt, into: &buf)
1419 - FfiConverterString.write(value.updatedAt, into: &buf)
1420 - }
1421 -}
1422 -
1423 -
1424 -#if swift(>=5.8)
1425 -@_documentation(visibility: private)
1426 -#endif
1427 -public func FfiConverterTypeSmbCloudUser_lift(_ buf: RustBuffer) throws -> SmbCloudUser {
1428 - return try FfiConverterTypeSmbCloudUser.lift(buf)
1429 -}
1430 -
1431 -#if swift(>=5.8)
1432 -@_documentation(visibility: private)
1433 -#endif
1434 -public func FfiConverterTypeSmbCloudUser_lower(_ value: SmbCloudUser) -> RustBuffer {
1435 - return FfiConverterTypeSmbCloudUser.lower(value)
1436 -}
1437 -
1438 -
1439 -public struct SmbCloudUserInfo {
1440 - public var sub: String
1441 - public var email: String?
1442 - public var emailVerified: Bool?
1443 - public var tenantId: UInt64?
1444 - public var tenantSlug: String?
1445 -
1446 - // Default memberwise initializers are never public by default, so we
1447 - // declare one manually.
1448 - public init(sub: String, email: String?, emailVerified: Bool?, tenantId: UInt64?, tenantSlug: String?) {
1449 - self.sub = sub
1450 - self.email = email
1451 - self.emailVerified = emailVerified
1452 - self.tenantId = tenantId
1453 - self.tenantSlug = tenantSlug
1454 - }
1455 -}
1456 -
1457 -#if compiler(>=6)
1458 -extension SmbCloudUserInfo: Sendable {}
1459 -#endif
1460 -
1461 -
1462 -extension SmbCloudUserInfo: Equatable, Hashable {
1463 - public static func ==(lhs: SmbCloudUserInfo, rhs: SmbCloudUserInfo) -> Bool {
1464 - if lhs.sub != rhs.sub {
1465 - return false
1466 - }
1467 - if lhs.email != rhs.email {
1468 - return false
1469 - }
1470 - if lhs.emailVerified != rhs.emailVerified {
1471 - return false
1472 - }
1473 - if lhs.tenantId != rhs.tenantId {
1474 - return false
1475 - }
1476 - if lhs.tenantSlug != rhs.tenantSlug {
1477 - return false
1478 - }
1479 - return true
1480 - }
1481 -
1482 - public func hash(into hasher: inout Hasher) {
1483 - hasher.combine(sub)
1484 - hasher.combine(email)
1485 - hasher.combine(emailVerified)
1486 - hasher.combine(tenantId)
1487 - hasher.combine(tenantSlug)
1488 - }
1489 -}
1490 -
1491 -
1492 -
1493 -#if swift(>=5.8)
1494 -@_documentation(visibility: private)
1495 -#endif
1496 -public struct FfiConverterTypeSmbCloudUserInfo: FfiConverterRustBuffer {
1497 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudUserInfo {
1498 - return
1499 - try SmbCloudUserInfo(
1500 - sub: FfiConverterString.read(from: &buf),
1501 - email: FfiConverterOptionString.read(from: &buf),
1502 - emailVerified: FfiConverterOptionBool.read(from: &buf),
1503 - tenantId: FfiConverterOptionUInt64.read(from: &buf),
1504 - tenantSlug: FfiConverterOptionString.read(from: &buf)
1505 - )
1506 - }
1507 -
1508 - public static func write(_ value: SmbCloudUserInfo, into buf: inout [UInt8]) {
1509 - FfiConverterString.write(value.sub, into: &buf)
1510 - FfiConverterOptionString.write(value.email, into: &buf)
1511 - FfiConverterOptionBool.write(value.emailVerified, into: &buf)
1512 - FfiConverterOptionUInt64.write(value.tenantId, into: &buf)
1513 - FfiConverterOptionString.write(value.tenantSlug, into: &buf)
1514 - }
1515 -}
1516 -
1517 -
1518 -#if swift(>=5.8)
1519 -@_documentation(visibility: private)
1520 -#endif
1521 -public func FfiConverterTypeSmbCloudUserInfo_lift(_ buf: RustBuffer) throws -> SmbCloudUserInfo {
1522 - return try FfiConverterTypeSmbCloudUserInfo.lift(buf)
1523 -}
1524 -
1525 -#if swift(>=5.8)
1526 -@_documentation(visibility: private)
1527 -#endif
1528 -public func FfiConverterTypeSmbCloudUserInfo_lower(_ value: SmbCloudUserInfo) -> RustBuffer {
1529 - return FfiConverterTypeSmbCloudUserInfo.lower(value)
1530 -}
1531 -
1532 -// Note that we don't yet support `indirect` for enums.
1533 -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
1534 -
1535 -public enum SmbCloudAccountStatus {
1536 -
1537 - case notFound
1538 - case ready(accessToken: String
1539 - )
1540 - case incomplete(errorCode: UInt32
1541 - )
1542 -}
1543 -
1544 -
1545 -#if compiler(>=6)
1546 -extension SmbCloudAccountStatus: Sendable {}
1547 -#endif
1548 -
1549 -#if swift(>=5.8)
1550 -@_documentation(visibility: private)
1551 -#endif
1552 -public struct FfiConverterTypeSmbCloudAccountStatus: FfiConverterRustBuffer {
1553 - typealias SwiftType = SmbCloudAccountStatus
1554 -
1555 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAccountStatus {
1556 - let variant: Int32 = try readInt(&buf)
1557 - switch variant {
1558 -
1559 - case 1: return .notFound
1560 -
1561 - case 2: return .ready(accessToken: try FfiConverterString.read(from: &buf)
1562 - )
1563 -
1564 - case 3: return .incomplete(errorCode: try FfiConverterUInt32.read(from: &buf)
1565 - )
1566 -
1567 - default: throw UniffiInternalError.unexpectedEnumCase
1568 - }
1569 - }
1570 -
1571 - public static func write(_ value: SmbCloudAccountStatus, into buf: inout [UInt8]) {
1572 - switch value {
1573 -
1574 -
1575 - case .notFound:
1576 - writeInt(&buf, Int32(1))
1577 -
1578 -
1579 - case let .ready(accessToken):
1580 - writeInt(&buf, Int32(2))
1581 - FfiConverterString.write(accessToken, into: &buf)
1582 -
1583 -
1584 - case let .incomplete(errorCode):
1585 - writeInt(&buf, Int32(3))
1586 - FfiConverterUInt32.write(errorCode, into: &buf)
1587 -
1588 - }
1589 - }
1590 -}
1591 -
1592 -
1593 -#if swift(>=5.8)
1594 -@_documentation(visibility: private)
1595 -#endif
1596 -public func FfiConverterTypeSmbCloudAccountStatus_lift(_ buf: RustBuffer) throws -> SmbCloudAccountStatus {
1597 - return try FfiConverterTypeSmbCloudAccountStatus.lift(buf)
1598 -}
1599 -
1600 -#if swift(>=5.8)
1601 -@_documentation(visibility: private)
1602 -#endif
1603 -public func FfiConverterTypeSmbCloudAccountStatus_lower(_ value: SmbCloudAccountStatus) -> RustBuffer {
1604 - return FfiConverterTypeSmbCloudAccountStatus.lower(value)
1605 -}
1606 -
1607 -
1608 -extension SmbCloudAccountStatus: Equatable, Hashable {}
1609 -
1610 -
1611 -
1612 -
1613 -
1614 -
1615 -
1616 -public enum SmbCloudAuthError: Swift.Error {
1617 -
1618 -
1619 -
1620 - case Api(errorCode: Int32, message: String
1621 - )
1622 -}
1623 -
1624 -
1625 -#if swift(>=5.8)
1626 -@_documentation(visibility: private)
1627 -#endif
1628 -public struct FfiConverterTypeSmbCloudAuthError: FfiConverterRustBuffer {
1629 - typealias SwiftType = SmbCloudAuthError
1630 -
1631 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudAuthError {
1632 - let variant: Int32 = try readInt(&buf)
1633 - switch variant {
1634 -
1635 -
1636 -
1637 -
1638 - case 1: return .Api(
1639 - errorCode: try FfiConverterInt32.read(from: &buf),
1640 - message: try FfiConverterString.read(from: &buf)
1641 - )
1642 -
1643 - default: throw UniffiInternalError.unexpectedEnumCase
1644 - }
1645 - }
1646 -
1647 - public static func write(_ value: SmbCloudAuthError, into buf: inout [UInt8]) {
1648 - switch value {
1649 -
1650 -
1651 -
1652 -
1653 -
1654 - case let .Api(errorCode,message):
1655 - writeInt(&buf, Int32(1))
1656 - FfiConverterInt32.write(errorCode, into: &buf)
1657 - FfiConverterString.write(message, into: &buf)
1658 -
1659 - }
1660 - }
1661 -}
1662 -
1663 -
1664 -#if swift(>=5.8)
1665 -@_documentation(visibility: private)
1666 -#endif
1667 -public func FfiConverterTypeSmbCloudAuthError_lift(_ buf: RustBuffer) throws -> SmbCloudAuthError {
1668 - return try FfiConverterTypeSmbCloudAuthError.lift(buf)
1669 -}
1670 -
1671 -#if swift(>=5.8)
1672 -@_documentation(visibility: private)
1673 -#endif
1674 -public func FfiConverterTypeSmbCloudAuthError_lower(_ value: SmbCloudAuthError) -> RustBuffer {
1675 - return FfiConverterTypeSmbCloudAuthError.lower(value)
1676 -}
1677 -
1678 -
1679 -extension SmbCloudAuthError: Equatable, Hashable {}
1680 -
1681 -
1682 -
1683 -
1684 -extension SmbCloudAuthError: Foundation.LocalizedError {
1685 - public var errorDescription: String? {
1686 - String(reflecting: self)
1687 - }
1688 -}
1689 -
1690 -
1691 -
1692 -
1693 -// Note that we don't yet support `indirect` for enums.
1694 -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
1695 -
1696 -public enum SmbCloudEnvironment {
1697 -
1698 - case dev
1699 - case production
1700 -}
1701 -
1702 -
1703 -#if compiler(>=6)
1704 -extension SmbCloudEnvironment: Sendable {}
1705 -#endif
1706 -
1707 -#if swift(>=5.8)
1708 -@_documentation(visibility: private)
1709 -#endif
1710 -public struct FfiConverterTypeSmbCloudEnvironment: FfiConverterRustBuffer {
1711 - typealias SwiftType = SmbCloudEnvironment
1712 -
1713 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SmbCloudEnvironment {
1714 - let variant: Int32 = try readInt(&buf)
1715 - switch variant {
1716 -
1717 - case 1: return .dev
1718 -
1719 - case 2: return .production
1720 -
1721 - default: throw UniffiInternalError.unexpectedEnumCase
1722 - }
1723 - }
1724 -
1725 - public static func write(_ value: SmbCloudEnvironment, into buf: inout [UInt8]) {
1726 - switch value {
1727 -
1728 -
1729 - case .dev:
1730 - writeInt(&buf, Int32(1))
1731 -
1732 -
1733 - case .production:
1734 - writeInt(&buf, Int32(2))
1735 -
1736 - }
1737 - }
1738 -}
1739 -
1740 -
1741 -#if swift(>=5.8)
1742 -@_documentation(visibility: private)
1743 -#endif
1744 -public func FfiConverterTypeSmbCloudEnvironment_lift(_ buf: RustBuffer) throws -> SmbCloudEnvironment {
1745 - return try FfiConverterTypeSmbCloudEnvironment.lift(buf)
1746 -}
1747 -
1748 -#if swift(>=5.8)
1749 -@_documentation(visibility: private)
1750 -#endif
1751 -public func FfiConverterTypeSmbCloudEnvironment_lower(_ value: SmbCloudEnvironment) -> RustBuffer {
1752 - return FfiConverterTypeSmbCloudEnvironment.lower(value)
1753 -}
1754 -
1755 -
1756 -extension SmbCloudEnvironment: Equatable, Hashable {}
1757 -
1758 -
1759 -
1760 -
1761 -
1762 -
1763 -#if swift(>=5.8)
1764 -@_documentation(visibility: private)
1765 -#endif
1766 -fileprivate struct FfiConverterOptionInt32: FfiConverterRustBuffer {
1767 - typealias SwiftType = Int32?
1768 -
1769 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
1770 - guard let value = value else {
1771 - writeInt(&buf, Int8(0))
1772 - return
1773 - }
1774 - writeInt(&buf, Int8(1))
1775 - FfiConverterInt32.write(value, into: &buf)
1776 - }
1777 -
1778 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
1779 - switch try readInt(&buf) as Int8 {
1780 - case 0: return nil
1781 - case 1: return try FfiConverterInt32.read(from: &buf)
1782 - default: throw UniffiInternalError.unexpectedOptionalTag
1783 - }
1784 - }
1785 -}
1786 -
1787 -#if swift(>=5.8)
1788 -@_documentation(visibility: private)
1789 -#endif
1790 -fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer {
1791 - typealias SwiftType = UInt64?
1792 -
1793 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
1794 - guard let value = value else {
1795 - writeInt(&buf, Int8(0))
1796 - return
1797 - }
1798 - writeInt(&buf, Int8(1))
1799 - FfiConverterUInt64.write(value, into: &buf)
1800 - }
1801 -
1802 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
1803 - switch try readInt(&buf) as Int8 {
1804 - case 0: return nil
1805 - case 1: return try FfiConverterUInt64.read(from: &buf)
1806 - default: throw UniffiInternalError.unexpectedOptionalTag
1807 - }
1808 - }
1809 -}
1810 -
1811 -#if swift(>=5.8)
1812 -@_documentation(visibility: private)
1813 -#endif
1814 -fileprivate struct FfiConverterOptionBool: FfiConverterRustBuffer {
1815 - typealias SwiftType = Bool?
1816 -
1817 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
1818 - guard let value = value else {
1819 - writeInt(&buf, Int8(0))
1820 - return
1821 - }
1822 - writeInt(&buf, Int8(1))
1823 - FfiConverterBool.write(value, into: &buf)
1824 - }
1825 -
1826 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
1827 - switch try readInt(&buf) as Int8 {
1828 - case 0: return nil
1829 - case 1: return try FfiConverterBool.read(from: &buf)
1830 - default: throw UniffiInternalError.unexpectedOptionalTag
1831 - }
1832 - }
1833 -}
1834 -
1835 -#if swift(>=5.8)
1836 -@_documentation(visibility: private)
1837 -#endif
1838 -fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer {
1839 - typealias SwiftType = String?
1840 -
1841 - public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
1842 - guard let value = value else {
1843 - writeInt(&buf, Int8(0))
1844 - return
1845 - }
1846 - writeInt(&buf, Int8(1))
1847 - FfiConverterString.write(value, into: &buf)
1848 - }
1849 -
1850 - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
1851 - switch try readInt(&buf) as Int8 {
1852 - case 0: return nil
1853 - case 1: return try FfiConverterString.read(from: &buf)
1854 - default: throw UniffiInternalError.unexpectedOptionalTag
1855 - }
1856 - }
1857 -}
1858 -private let UNIFFI_RUST_FUTURE_POLL_READY: Int8 = 0
1859 -private let UNIFFI_RUST_FUTURE_POLL_MAYBE_READY: Int8 = 1
1860 -
1861 -fileprivate let uniffiContinuationHandleMap = UniffiHandleMap<UnsafeContinuation<Int8, Never>>()
1862 -
1863 -fileprivate func uniffiRustCallAsync<F, T>(
1864 - rustFutureFunc: () -> UInt64,
1865 - pollFunc: (UInt64, @escaping UniffiRustFutureContinuationCallback, UInt64) -> (),
1866 - completeFunc: (UInt64, UnsafeMutablePointer<RustCallStatus>) -> F,
1867 - freeFunc: (UInt64) -> (),
1868 - liftFunc: (F) throws -> T,
1869 - errorHandler: ((RustBuffer) throws -> Swift.Error)?
1870 -) async throws -> T {
1871 - // Make sure to call the ensure init function since future creation doesn't have a
1872 - // RustCallStatus param, so doesn't use makeRustCall()
1873 - uniffiEnsureSmbcloudAuthInitialized()
1874 - let rustFuture = rustFutureFunc()
1875 - defer {
1876 - freeFunc(rustFuture)
1877 - }
1878 - var pollResult: Int8;
1879 - repeat {
1880 - pollResult = await withUnsafeContinuation {
1881 - pollFunc(
1882 - rustFuture,
1883 - uniffiFutureContinuationCallback,
1884 - uniffiContinuationHandleMap.insert(obj: $0)
1885 - )
1886 - }
1887 - } while pollResult != UNIFFI_RUST_FUTURE_POLL_READY
1888 -
1889 - return try liftFunc(makeRustCall(
1890 - { completeFunc(rustFuture, $0) },
1891 - errorHandler: errorHandler
1892 - ))
1893 -}
1894 -
1895 -// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They
1896 -// lift the return value or error and resume the suspended function.
1897 -fileprivate func uniffiFutureContinuationCallback(handle: UInt64, pollResult: Int8) {
1898 - if let continuation = try? uniffiContinuationHandleMap.remove(handle: handle) {
1899 - continuation.resume(returning: pollResult)
1900 - } else {
1901 - print("uniffiFutureContinuationCallback invalid handle")
1902 - }
1903 -}
1904 -
1905 -private enum InitializationResult {
1906 - case ok
1907 - case contractVersionMismatch
1908 - case apiChecksumMismatch
1909 -}
1910 -// Use a global variable to perform the versioning checks. Swift ensures that
1911 -// the code inside is only computed once.
1912 -private let initializationResult: InitializationResult = {
1913 - // Get the bindings contract version from our ComponentInterface
1914 - let bindings_contract_version = 29
1915 - // Get the scaffolding contract version by calling the into the dylib
1916 - let scaffolding_contract_version = ffi_smbcloud_auth_uniffi_contract_version()
1917 - if bindings_contract_version != scaffolding_contract_version {
1918 - return InitializationResult.contractVersionMismatch
1919 - }
1920 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_build_apple_authorization_request() != 20401) {
1921 - return InitializationResult.apiChecksumMismatch
1922 - }
1923 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_build_oidc_authorization_request() != 27410) {
1924 - return InitializationResult.apiChecksumMismatch
1925 - }
1926 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_exchange_oidc_code() != 62992) {
1927 - return InitializationResult.apiChecksumMismatch
1928 - }
1929 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_get_oidc_userinfo() != 55608) {
1930 - return InitializationResult.apiChecksumMismatch
1931 - }
1932 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_login() != 22565) {
1933 - return InitializationResult.apiChecksumMismatch
1934 - }
1935 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_logout() != 50854) {
1936 - return InitializationResult.apiChecksumMismatch
1937 - }
1938 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_me() != 14252) {
1939 - return InitializationResult.apiChecksumMismatch
1940 - }
1941 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_parse_apple_callback_url() != 36073) {
1942 - return InitializationResult.apiChecksumMismatch
1943 - }
1944 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_parse_oidc_callback_url() != 50343) {
1945 - return InitializationResult.apiChecksumMismatch
1946 - }
1947 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_remove_account() != 59938) {
1948 - return InitializationResult.apiChecksumMismatch
1949 - }
1950 - if (uniffi_smbcloud_auth_checksum_method_smbcloudauth_signup() != 1373) {
1951 - return InitializationResult.apiChecksumMismatch
1952 - }
1953 - if (uniffi_smbcloud_auth_checksum_constructor_smbcloudauth_new() != 47975) {
1954 - return InitializationResult.apiChecksumMismatch
1955 - }
1956 -
1957 - return InitializationResult.ok
1958 -}()
1959 -
1960 -// Make the ensure init function public so that other modules which have external type references to
1961 -// our types can call it.
1962 -public func uniffiEnsureSmbcloudAuthInitialized() {
1963 - switch initializationResult {
1964 - case .ok:
1965 - break
1966 - case .contractVersionMismatch:
1967 - fatalError("UniFFI contract version mismatch: try cleaning and rebuilding your project")
1968 - case .apiChecksumMismatch:
1969 - fatalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
1970 - }
1971 -}
1972 -
1973 -// swiftlint:enable all
\ No newline at end of file
Tests/AuthCoreTests/AuthCoreTests.swift
+140 -117
@@ -4,34 +4,26 @@ import XCTest
4 @testable import AuthCore
5
6 final class AuthCoreTests: XCTestCase {
7 - func testSessionScopeJoinsScopesWithSpaces() {
8 - let session = SmbCloudSession(
9 - accessToken: "token",
10 - scopes: ["openid", "profile", "email"]
11 - )
7
8 + // MARK: - Session
9 +
10 + func testSessionScopeJoinsScopesWithSpaces() {
11 + let session = SmbCloudSession(accessToken: "token", scopes: ["openid", "profile", "email"])
12 XCTAssertEqual(session.scope, "openid profile email")
13 XCTAssertEqual(session.authorizationHeaderValue, "Bearer token")
14 }
15
16 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)
17 + let future = SmbCloudSession(accessToken: "f", expiresAt: Date().addingTimeInterval(300))
18 + let past = SmbCloudSession(accessToken: "p", expiresAt: Date().addingTimeInterval(-300))
19 + XCTAssertTrue(future.isValid())
20 + XCTAssertFalse(future.isExpired)
21 + XCTAssertFalse(past.isValid(leeway: 0))
22 + XCTAssertTrue(past.isExpired)
23 }
24
25 func testSessionCodableRoundTripPreservesValues() throws {
34 - let originalSession = SmbCloudSession(
26 + let original = SmbCloudSession(
27 accessToken: "access-token",
28 refreshToken: "refresh-token",
29 idToken: "id-token",
@@ -39,144 +31,175 @@ final class AuthCoreTests: XCTestCase {
31 expiresAt: Date(timeIntervalSince1970: 1_750_000_000),
32 scopes: ["openid", "profile", "email"]
33 )
34 + let data = try JSONEncoder().encode(original)
35 + let decoded = try JSONDecoder().decode(SmbCloudSession.self, from: data)
36 + XCTAssertEqual(decoded, original)
37 + }
38
43 - let encodedSession = try JSONEncoder().encode(originalSession)
44 - let decodedSession = try JSONDecoder().decode(SmbCloudSession.self, from: encodedSession)
45 -
46 - XCTAssertEqual(decodedSession, originalSession)
39 + func testSessionFromTokenResponseMapsFields() {
40 + let now = Date(timeIntervalSince1970: 1_000_000)
41 + let token = OIDC.TokenResponse(
42 + accessToken: "at", tokenType: "Bearer", expiresIn: 3600,
43 + refreshToken: "rt", scope: "openid profile openid", idToken: "it")
44 + let session = SmbCloudSession(tokenResponse: token, now: now)
45 + XCTAssertEqual(session.accessToken, "at")
46 + XCTAssertEqual(session.refreshToken, "rt")
47 + XCTAssertEqual(session.idToken, "it")
48 + XCTAssertEqual(session.expiresAt, now.addingTimeInterval(3600))
49 + XCTAssertEqual(session.scopes, ["openid", "profile"]) // deduped
50 }
51
52 + // MARK: - Credentials store
53 +
54 func testInMemoryCredentialsStoreRoundTrips() throws {
55 let store = SmbCloudInMemoryCredentialsStore()
56 XCTAssertNil(try store.current())
57
53 - let session = SmbCloudSession(
54 - accessToken: "access-token",
55 - expiresAt: Date().addingTimeInterval(300),
56 - scopes: ["openid", "openid", " profile "]
57 - )
58 - try store.store(session)
58 + try store.store(
59 + SmbCloudSession(
60 + accessToken: "access-token",
61 + expiresAt: Date().addingTimeInterval(300),
62 + scopes: ["openid", "openid", " profile "]))
63
60 - // storedRepresentation() normalizes/uniques scopes.
64 XCTAssertEqual(try store.current()?.scopes, ["openid", "profile"])
65 XCTAssertTrue(try store.hasValidSession())
66 XCTAssertNotNil(try store.currentValidSession())
67
68 try store.clear()
69 XCTAssertNil(try store.current())
67 - XCTAssertFalse(try store.hasValidSession())
70 }
71
72 func testExpiredSessionIsNotReturnedAsValid() throws {
73 let store = SmbCloudInMemoryCredentialsStore(
72 - session: SmbCloudSession(
73 - accessToken: "stale",
74 - expiresAt: Date().addingTimeInterval(-300)
75 - )
76 - )
77 -
74 + session: SmbCloudSession(accessToken: "stale", expiresAt: Date().addingTimeInterval(-300)))
75 XCTAssertNotNil(try store.current())
76 XCTAssertNil(try store.currentValidSession())
77 XCTAssertFalse(try store.hasValidSession())
78 }
79
83 - func testInvalidUserInfoDomainThrowsInvalidBaseUrlError() {
84 - XCTAssertThrowsError(try SmbCloudUserInfoClient(domain: " ")) { error in
85 - guard case SmbCloudClientError.invalidBaseURL = error else {
86 - return XCTFail("Expected invalidBaseURL error, got: \(error)")
87 - }
88 - }
80 + // MARK: - Environment
81 +
82 + func testEnvironmentHostsAndProtocols() {
83 + XCTAssertEqual(SmbCloudEnvironment.dev.apiProtocol, "http")
84 + XCTAssertEqual(SmbCloudEnvironment.dev.apiHost, "localhost:8088")
85 + XCTAssertEqual(SmbCloudEnvironment.production.apiProtocol, "https")
86 + XCTAssertEqual(SmbCloudEnvironment.production.apiHost, "api.smbcloud.xyz")
87 + XCTAssertEqual(SmbCloudEnvironment.production.baseURL.absoluteString, "https://api.smbcloud.xyz/")
88 }
89
91 - func testInvalidAuthClientDomainThrowsInvalidBaseUrlError() {
92 - XCTAssertThrowsError(
93 - try SmbCloudAuthClient(
94 - domain: " ",
95 - clientId: "public-client-id",
96 - redirectURL: URL(string: "myapp://auth/callback")!
97 - )
98 - ) { error in
99 - guard case SmbCloudClientError.invalidBaseURL = error else {
100 - return XCTFail("Expected invalidBaseURL error, got: \(error)")
101 - }
102 - }
90 + // MARK: - ClientCredentials URL building
91 +
92 + func testTenantURLCarriesCredentialsAsQuery() {
93 + let creds = ClientCredentials(appId: "app-1", appSecret: "secret-1")
94 + let url = creds.tenantURL(environment: .production, path: "v1/client/me")
95 + let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
96 + XCTAssertEqual(comps?.scheme, "https")
97 + XCTAssertEqual(comps?.host, "api.smbcloud.xyz")
98 + XCTAssertEqual(comps?.path, "/v1/client/me")
99 + XCTAssertEqual(query(url, "client_id"), "app-1")
100 + XCTAssertEqual(query(url, "client_secret"), "secret-1")
101 }
102
105 - func testHttpLoopbackDomainIsAllowed() {
106 - XCTAssertNoThrow(
107 - try SmbCloudAuthClient(
108 - domain: "http://localhost:8088",
109 - clientId: "public-client-id",
110 - redirectURL: URL(string: "myapp://auth/callback")!
111 - )
112 - )
113 - XCTAssertNoThrow(try SmbCloudUserInfoClient(domain: "http://127.0.0.1:8088"))
103 + func testTenantURLDevPreservesPort() {
104 + let creds = ClientCredentials(appId: "a", appSecret: "s")
105 + let url = creds.tenantURL(environment: .dev, path: "v1/client/users/sign_in")
106 + let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
107 + XCTAssertEqual(comps?.host, "localhost")
108 + XCTAssertEqual(comps?.port, 8088)
109 + XCTAssertEqual(comps?.scheme, "http")
110 + }
111 +
112 + // MARK: - OIDC
113 +
114 + func testOIDCAuthorizationRequestProducesPkceChallenge() throws {
115 + let request = try OIDC.buildAuthorizationRequest(
116 + environment: .production, oidcClientId: "oidc-1", redirectURI: "myapp://auth/callback")
117 + let url = request.authorizeURL
118 + XCTAssertEqual(URLComponents(url: url, resolvingAgainstBaseURL: false)?.path, "/oauth/authorize")
119 + XCTAssertEqual(query(url, "response_type"), "code")
120 + XCTAssertEqual(query(url, "code_challenge_method"), "S256")
121 + XCTAssertEqual(query(url, "client_id"), "oidc-1")
122 + XCTAssertEqual(query(url, "scope"), "openid profile email")
123 + XCTAssertEqual(query(url, "state"), request.state)
124 + XCTAssertNotNil(query(url, "code_challenge"))
125 + XCTAssertEqual(request.codeVerifier.count, 64) // two uuid-simple strings
126 + }
127 +
128 + func testOIDCParseCallbackURL() throws {
129 + let payload = try OIDC.parseCallbackURL("myapp://cb?code=abc&state=xyz")
130 + XCTAssertEqual(payload.code, "abc")
131 + XCTAssertEqual(payload.state, "xyz")
132 }
133
116 - func testHttpRemoteDomainThrowsInvalidBaseUrlError() {
117 - XCTAssertThrowsError(try SmbCloudUserInfoClient(domain: "http://api.smbcloud.xyz")) {
118 - error in
119 - guard case SmbCloudClientError.invalidBaseURL = error else {
120 - return XCTFail("Expected invalidBaseURL error, got: \(error)")
121 - }
134 + func testOIDCParseCallbackURLMissingParamsThrows() {
135 + XCTAssertThrowsError(try OIDC.parseCallbackURL("myapp://cb?code=abc")) { error in
136 + XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
137 }
138 + }
139 +
140 + // MARK: - Apple provider
141 +
142 + func testBuildAppleAuthorizationRequest() {
143 + let client = AuthCoreClient(
144 + environment: .production, credentials: .init(appId: "app-1", appSecret: "secret-1"))
145 + let request = client.buildAppleAuthorizationRequest(redirectURI: "myapp://cb", state: "the-state")
146 + XCTAssertEqual(request.state, "the-state")
147 + XCTAssertEqual(
148 + URLComponents(url: request.authorizeURL, resolvingAgainstBaseURL: false)?.path,
149 + "/v1/client/oauth/apple/authorize")
150 + XCTAssertEqual(query(request.authorizeURL, "client_id"), "app-1")
151 + XCTAssertEqual(query(request.authorizeURL, "redirect_uri"), "myapp://cb")
152 + XCTAssertEqual(query(request.authorizeURL, "state"), "the-state")
153 + }
154 +
155 + func testParseAppleCallbackURL() throws {
156 + let client = AuthCoreClient(
157 + environment: .production, credentials: .init(appId: "a", appSecret: "s"))
158 + let session = try client.parseAppleCallbackURL(
159 + "myapp://cb?access_token=AT&provider_account_id=PA&email=e@x.com&state=S",
160 + expectedState: "S")
161 + XCTAssertEqual(session.accessToken, "AT")
162 + XCTAssertEqual(session.providerAccountId, "PA")
163 + XCTAssertEqual(session.email, "e@x.com")
164 + XCTAssertEqual(session.provider, "apple")
165 + XCTAssertEqual(session.state, "S")
166 + }
167
168 + func testParseAppleCallbackURLStateMismatchThrows() {
169 + let client = AuthCoreClient(
170 + environment: .production, credentials: .init(appId: "a", appSecret: "s"))
171 XCTAssertThrowsError(
125 - try SmbCloudAuthClient(
126 - domain: "http://api.smbcloud.xyz",
127 - clientId: "public-client-id",
128 - redirectURL: URL(string: "myapp://auth/callback")!
129 - )
172 + try client.parseAppleCallbackURL(
173 + "myapp://cb?access_token=AT&provider_account_id=PA&state=S", expectedState: "other")
174 ) { error in
131 - guard case SmbCloudClientError.invalidBaseURL = error else {
132 - return XCTFail("Expected invalidBaseURL error, got: \(error)")
133 - }
175 + XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
176 }
177 }
178
137 - func testAuthorizationRequestProducesPkceChallenge() throws {
138 - let client = SmbCloudAuthClient(
139 - baseURL: URL(string: "https://api.smbcloud.xyz")!,
140 - clientId: "public-client-id",
141 - redirectURL: URL(string: "myapp://auth/callback")!
142 - )
143 -
144 - let request = try client.authorizationRequest()
145 - let components = URLComponents(
146 - url: request.authorizeURL,
147 - resolvingAgainstBaseURL: false
148 - )
149 - let queryItems = components?.queryItems ?? []
150 -
151 - XCTAssertEqual(queryItems.firstValue(named: "response_type"), "code")
152 - XCTAssertEqual(queryItems.firstValue(named: "code_challenge_method"), "S256")
153 - XCTAssertEqual(queryItems.firstValue(named: "client_id"), "public-client-id")
154 - XCTAssertEqual(queryItems.firstValue(named: "state"), request.state)
155 - XCTAssertEqual(queryItems.firstValue(named: "scope"), "openid profile email")
156 - XCTAssertFalse(request.codeVerifier.isEmpty)
157 - XCTAssertNotNil(queryItems.firstValue(named: "code_challenge"))
158 - }
159 -
160 - func testCallbackScheme() {
161 - let client = SmbCloudAuthClient(
162 - baseURL: URL(string: "https://api.smbcloud.xyz")!,
163 - clientId: "public-client-id",
164 - redirectURL: URL(string: "myapp://auth/callback")!
165 - )
166 -
167 - XCTAssertEqual(client.callbackScheme, "myapp")
179 + func testParseAppleCallbackURLPropagatesError() {
180 + let client = AuthCoreClient(
181 + environment: .production, credentials: .init(appId: "a", appSecret: "s"))
182 + XCTAssertThrowsError(
183 + try client.parseAppleCallbackURL("myapp://cb?error=access_denied")
184 + ) { error in
185 + XCTAssertEqual((error as? SmbCloudError)?.code, .invalidParams)
186 + XCTAssertEqual((error as? SmbCloudError)?.message, "access_denied")
187 + }
188 }
189
170 - func testStateMismatchErrorDescriptionDoesNotExposeStateValues() {
171 - let error = SmbCloudClientError.stateMismatch(
172 - expected: "expected-state",
173 - received: "received-state"
174 - )
190 + // MARK: - Errors
191
176 - XCTAssertEqual(error.errorDescription, "The callback state did not match.")
192 + func testErrorCodeRawValuesMatchContract() {
193 + XCTAssertEqual(SmbCloudErrorCode.unauthorized.rawValue, 100)
194 + XCTAssertEqual(SmbCloudErrorCode.emailNotVerified.rawValue, 1001)
195 + XCTAssertEqual(AccountErrorCode.emailUnverified.rawValue, 1001)
196 + XCTAssertEqual(SmbCloudError(code: .unauthorized).message, "Unauthorized access.")
197 }
198
179 - func testDefaultScopesMatchOidcDefaults() {
180 - XCTAssertEqual(SmbCloudAuthClient.defaultScopes, ["openid", "profile", "email"])
199 + // MARK: - Helpers
200 +
201 + private func query(_ url: URL, _ name: String) -> String? {
202 + URLComponents(url: url, resolvingAgainstBaseURL: false)?
203 + .queryItems?.first { $0.name == name }?.value
204 }
205 }
Tests/SmbCloudAuthTests/SmbCloudAuthTests.swift
+14 -29
@@ -4,47 +4,32 @@ import XCTest
4 import SmbCloudAuth
5
6 final class SmbCloudAuthTests: XCTestCase {
7 - func testWebAuthDefaultScopesMatchOidcDefaults() {
8 - XCTAssertEqual(SmbCloudWebAuth.defaultScopes, ["openid", "profile", "email"])
9 - }
10 -
11 - func testInvalidWebAuthDomainThrowsInvalidBaseUrlError() {
12 - XCTAssertThrowsError(
13 - try SmbCloudWebAuth(
14 - domain: " ",
15 - clientId: "public-client-id",
16 - redirectURL: URL(string: "myapp://auth/callback")!
17 - )
18 - ) { error in
19 - guard case SmbCloudClientError.invalidBaseURL = error else {
20 - return XCTFail("Expected invalidBaseURL error, got: \(error)")
21 - }
22 - }
23 - }
24 -
25 - func testWebAuthExposesUnderlyingAuthClient() throws {
26 - let webAuth = try SmbCloudWebAuth(
27 - domain: "api.smbcloud.xyz",
28 - clientId: "public-client-id",
7 + func testWebAuthExposesConfiguration() {
8 + let webAuth = SmbCloudWebAuth(
9 + environment: .production,
10 + oidcClientId: "oidc-client-id",
11 redirectURL: URL(string: "myapp://auth/callback")!
12 )
13 + XCTAssertEqual(webAuth.oidcClientId, "oidc-client-id")
14 + XCTAssertEqual(webAuth.callbackScheme, "myapp")
15 + XCTAssertEqual(webAuth.environment, .production)
16 + }
17
32 - XCTAssertEqual(webAuth.client.clientId, "public-client-id")
33 - XCTAssertEqual(webAuth.client.callbackScheme, "myapp")
18 + func testWebAuthCallbackSchemeNilWhenMissing() {
19 + let webAuth = SmbCloudWebAuth(
20 + oidcClientId: "x", redirectURL: URL(string: "/no-scheme")!)
21 + XCTAssertNil(webAuth.callbackScheme)
22 }
23
24 #if canImport(Security)
25 func testCredentialsManagerUsesExplicitServiceWhenProvided() {
38 - let manager = SmbCloudCredentialsManager(
39 - service: "xyz.smbcloud.tests", account: "tester")
40 -
26 + let manager = SmbCloudCredentialsManager(service: "xyz.smbcloud.tests", account: "tester")
27 XCTAssertEqual(manager.service, "xyz.smbcloud.tests")
28 XCTAssertEqual(manager.account, "tester")
29 }
30
31 func testCredentialsManagerConformsToCredentialsStore() {
46 - let manager: SmbCloudCredentialsStore = SmbCloudCredentialsManager(
47 - service: "xyz.smbcloud.tests")
32 + let manager: SmbCloudCredentialsStore = SmbCloudCredentialsManager(service: "xyz.smbcloud.tests")
33 XCTAssertNotNil(manager)
34 }
35 #endif