this repo has no description

updates to docs about auth

+233 -4
+200
Sources/EffemKit/EffemKit.docc/Authentication.md
··· 1 + # Authentication 2 + 3 + Sign users in with AT Protocol OAuth and manage sessions. 4 + 5 + ## Overview 6 + 7 + EffemKit re-exports CoreATProtocol's full OAuth stack under `Effem*` typealiases. You can import only EffemKit and get everything needed for authentication — no separate CoreATProtocol import required. 8 + 9 + The OAuth flow uses AT Protocol's standard DPoP-based OAuth with PKCE. EffemKit handles identity resolution (handle to DID to PDS to auth server), token signing, and automatic token refresh. 10 + 11 + ## Configure OAuth 12 + 13 + Create an ``EffemOAuthConfig`` with your app's client metadata URL, redirect URI, and requested scopes: 14 + 15 + ```swift 16 + let config = EffemOAuthConfig( 17 + clientMetadataURL: "https://effem.xyz/client-metadata.json", 18 + redirectURI: "effem://oauth/callback", 19 + scopes: ["atproto", "transition:generic"] 20 + ) 21 + ``` 22 + 23 + The `clientMetadataURL` must point to a publicly accessible JSON document that describes your OAuth client, as specified by the AT Protocol OAuth spec. 24 + 25 + ## Run the Authentication Flow 26 + 27 + ``EffemOAuth`` orchestrates the full sign-in process. Provide a user authenticator callback that presents the authorization URL (typically via `ASWebAuthenticationSession`) and returns the callback URL containing the authorization code: 28 + 29 + ```swift 30 + import EffemKit 31 + import AuthenticationServices 32 + 33 + @APActor 34 + func signIn(handle: String) async throws -> EffemAuthResult { 35 + let config = EffemOAuthConfig( 36 + clientMetadataURL: "https://effem.xyz/client-metadata.json", 37 + redirectURI: "effem://oauth/callback", 38 + scopes: ["atproto", "transition:generic"] 39 + ) 40 + 41 + let oauth = EffemOAuth(config: config) 42 + 43 + let result = try await oauth.authenticate( 44 + identifier: handle, 45 + authenticator: { authURL in 46 + // Present authURL to the user (e.g., ASWebAuthenticationSession) 47 + // Return the callback URL after the user authorizes 48 + return try await presentAuthSession(url: authURL) 49 + } 50 + ) 51 + 52 + // result.did — the user's DID 53 + // result.handle — the user's handle 54 + // result.accessToken — bearer token (also set in APEnvironment) 55 + // result.pdsEndpoint — the user's PDS URL (also set in APEnvironment) 56 + return result 57 + } 58 + ``` 59 + 60 + After ``EffemOAuth/authenticate(identifier:authenticator:)`` completes, CoreATProtocol's `APEnvironment` is fully configured: 61 + 62 + - `APEnvironment.current.host` is set to the user's PDS 63 + - `APEnvironment.current.accessToken` holds the bearer token 64 + - `APEnvironment.current.dpopPrivateKey` holds the DPoP signing key 65 + 66 + Both ``EffemService`` and ``EffemRepoService`` are ready to use immediately. 67 + 68 + ## Provide Auth Storage 69 + 70 + Implement ``EffemAuthStorage`` to persist tokens and private keys across app launches. The protocol requires async methods for storing and retrieving `Login` and `PrivateKey` data: 71 + 72 + ```swift 73 + final class MyAuthStorage: EffemAuthStorage { 74 + func store(login: Login) async throws { 75 + // Persist to Keychain or secure storage 76 + } 77 + 78 + func retrieveLogin() async throws -> Login? { 79 + // Load from Keychain or secure storage 80 + } 81 + 82 + func store(privateKey: Data) async throws { 83 + // Persist the DPoP private key (PEM format) 84 + } 85 + 86 + func retrievePrivateKey() async throws -> Data? { 87 + // Load the DPoP private key 88 + } 89 + } 90 + ``` 91 + 92 + Pass storage when creating the OAuth client: 93 + 94 + ```swift 95 + let storage = MyAuthStorage() 96 + let oauth = EffemOAuth(config: config, storage: storage) 97 + ``` 98 + 99 + ## Refresh Tokens 100 + 101 + CoreATProtocol's networking layer (`APRouterDelegate`) automatically detects 401/403 responses and attempts token refresh using the stored refresh token and DPoP key. This happens transparently — you don't need to handle token expiry manually. 102 + 103 + To force a token refresh: 104 + 105 + ```swift 106 + @APActor 107 + func forceRefresh() async throws { 108 + let oauth = EffemOAuth(config: config, storage: storage) 109 + try await oauth.refreshLoginIfNeeded(force: true) 110 + } 111 + ``` 112 + 113 + ## Handle Errors 114 + 115 + Authentication can fail at multiple stages. ``EffemOAuthError`` covers all cases: 116 + 117 + ```swift 118 + @APActor 119 + func handleSignIn(handle: String) async { 120 + do { 121 + let result = try await signIn(handle: handle) 122 + // Success — store result.did for future API calls 123 + } catch let error as EffemOAuthError { 124 + switch error { 125 + case .identityResolutionFailed: 126 + // Could not resolve handle to DID/PDS 127 + print("Could not find user: \(handle)") 128 + case .tokenRequestFailed: 129 + // OAuth token exchange failed 130 + print("Authentication failed") 131 + default: 132 + print(error.localizedDescription) 133 + } 134 + } catch let error as EffemIdentityError { 135 + // Handle/DID resolution specific errors 136 + print("Identity error: \(error.localizedDescription)") 137 + } catch { 138 + print("Unexpected error: \(error.localizedDescription)") 139 + } 140 + } 141 + ``` 142 + 143 + ## Full App Example 144 + 145 + Putting it all together — configure the AppView, authenticate, then use both services: 146 + 147 + ```swift 148 + import SwiftUI 149 + import EffemKit 150 + 151 + @main 152 + struct EffemApp: App { 153 + init() { 154 + Task { @APActor in 155 + setup(appViewHost: "https://appview.effem.xyz") 156 + } 157 + } 158 + 159 + var body: some Scene { 160 + WindowGroup { 161 + ContentView() 162 + } 163 + } 164 + } 165 + 166 + @Observable 167 + @APActor 168 + final class AppState { 169 + var userDID: String? 170 + var isAuthenticated: Bool { userDID != nil } 171 + 172 + private let oauthConfig = EffemOAuthConfig( 173 + clientMetadataURL: "https://effem.xyz/client-metadata.json", 174 + redirectURI: "effem://oauth/callback", 175 + scopes: ["atproto", "transition:generic"] 176 + ) 177 + 178 + func signIn(handle: String, presentURL: @escaping (URL) async throws -> URL) async throws { 179 + let oauth = EffemOAuth(config: oauthConfig) 180 + let result = try await oauth.authenticate( 181 + identifier: handle, 182 + authenticator: presentURL 183 + ) 184 + userDID = result.did 185 + } 186 + 187 + func loadTrending() async throws -> [PodcastResult] { 188 + let service = EffemService() 189 + let response = try await service.getTrending(max: 20) 190 + return response.feeds 191 + } 192 + 193 + func subscribe(feedId: Int) async throws { 194 + guard let did = userDID else { return } 195 + let repoService = EffemRepoService() 196 + let podcast = PodcastRef(feedId: feedId) 197 + _ = try await repoService.subscribe(to: podcast, repo: did) 198 + } 199 + } 200 + ```
+12
Sources/EffemKit/EffemKit.docc/EffemKit.md
··· 19 19 - ``setup(appViewHost:)`` 20 20 - ``EffemEnvironment`` 21 21 22 + ### Authentication 23 + 24 + - <doc:Authentication> 25 + - ``EffemOAuth`` 26 + - ``EffemOAuthConfig`` 27 + - ``EffemAuthStorage`` 28 + - ``EffemAuthResult`` 29 + - ``EffemUserAuthenticator`` 30 + 22 31 ### Reading Data 23 32 24 33 - <doc:ReadingData> ··· 79 88 80 89 - ``EffemKitConfigurationError`` 81 90 - ``EffemRepoError`` 91 + - ``EffemOAuthError`` 92 + - ``EffemIdentityError`` 93 + - ``EffemErrorMessage``
+21 -4
Sources/EffemKit/EffemKit.docc/GettingStarted.md
··· 39 39 40 40 ## Authenticate the User 41 41 42 - EffemKit relies on CoreATProtocol for OAuth authentication. Once the user signs in through CoreATProtocol, the PDS host is automatically available and ``EffemRepoService`` can write records: 42 + EffemKit re-exports CoreATProtocol's OAuth types under `Effem*` names so you only need `import EffemKit`. Create an ``EffemOAuth`` instance, configure it, and run the authentication flow: 43 43 44 44 ```swift 45 - // After CoreATProtocol OAuth flow completes, 46 - // APEnvironment.current.host is set to the user's PDS. 47 - // No additional EffemKit configuration is needed for writes. 45 + import EffemKit 46 + 47 + @APActor 48 + func signIn(presentURL: @escaping (URL) async -> URL) async throws -> EffemAuthResult { 49 + let config = EffemOAuthConfig( 50 + clientMetadataURL: "https://effem.xyz/client-metadata.json", 51 + redirectURI: "effem://oauth/callback", 52 + scopes: ["atproto", "transition:generic"] 53 + ) 54 + 55 + let oauth = EffemOAuth(config: config) 56 + return try await oauth.authenticate( 57 + identifier: "alice.bsky.social", 58 + authenticator: presentURL 59 + ) 60 + } 48 61 ``` 62 + 63 + After authentication succeeds, CoreATProtocol's `APEnvironment` is automatically configured with the user's PDS host, access token, and DPoP keys. ``EffemRepoService`` can immediately write records — no additional setup needed. 64 + 65 + For a full walkthrough of the OAuth flow, token refresh, and session persistence, see <doc:Authentication>. 49 66 50 67 ## Read Data with EffemService 51 68