this repo has no description

asked codex to make some documentation lol

Changed files
+157
Documentation
CoreATProtocol.docc
+157
Documentation/CoreATProtocol.docc/BuildBlueskyLogin.md
··· 1 + # Build a Bluesky Login Flow 2 + 3 + Learn how an iOS app can depend on ``CoreATProtocol`` and guide a user through the AT Protocol OAuth flow using Bluesky as the authorization server. 4 + 5 + ## Add the package to your app 6 + 7 + 1. In your app target's `Package.swift`, add the CoreATProtocol dependency: 8 + 9 + ```swift 10 + .package(url: "https://github.com/your-org/CoreATProtocol.git", from: "1.0.0") 11 + ``` 12 + 13 + 2. List ``CoreATProtocol`` in the target's dependencies: 14 + 15 + ```swift 16 + .target( 17 + name: "App", 18 + dependencies: [ 19 + .product(name: "CoreATProtocol", package: "CoreATProtocol") 20 + ] 21 + ) 22 + ``` 23 + 24 + 3. Import the module where you coordinate authentication: 25 + 26 + ```swift 27 + import CoreATProtocol 28 + ``` 29 + 30 + ## Persist a DPoP key 31 + 32 + Bluesky issues DPoP-bound access tokens, so the app must generate and persist a single ES256 key pair. The example below stores the private key in the Keychain and recreates it when needed. 33 + 34 + ```swift 35 + import CryptoKit 36 + import JWTKit 37 + 38 + final class DPoPKeyStore { 39 + private let keyTag = "com.example.app.dpop" 40 + 41 + func loadOrCreateKey() throws -> ES256PrivateKey { 42 + if let raw = try loadKeyData() { 43 + return try ES256PrivateKey(pem: raw) 44 + } 45 + 46 + let key = ES256PrivateKey() 47 + try persist(key.pemRepresentation) 48 + return key 49 + } 50 + 51 + private func loadKeyData() throws -> String? { 52 + // Read from the Keychain and return the PEM string if it exists. 53 + nil 54 + } 55 + 56 + private func persist(_ pem: String) throws { 57 + // Write the PEM string to the Keychain. 58 + } 59 + } 60 + ``` 61 + 62 + ## Expose a DPoP JWT generator 63 + 64 + Wrap the signing key with ``DPoPJWTGenerator`` so the library can mint proofs on demand. 65 + 66 + ```swift 67 + let keyStore = DPoPKeyStore() 68 + let privateKey = try await keyStore.loadOrCreateKey() 69 + let dpopGenerator = try await DPoPJWTGenerator(privateKey: privateKey) 70 + let jwtGenerator = dpopGenerator.jwtGenerator() 71 + ``` 72 + 73 + Pass ``DPoPJWTGenerator.jwtGenerator()`` to ``LoginService`` and later to ``applyAuthenticationContext(login:generator:resourceNonce:)`` so API calls share the same key material. 74 + 75 + ## Configure login storage 76 + 77 + Provide a ``LoginStorage`` implementation that reads and writes the user’s Bluesky session securely. The storage runs on the calling actor, so use async APIs. 78 + 79 + ```swift 80 + import OAuthenticator 81 + 82 + struct BlueskyLoginStore { 83 + func makeStorage() -> LoginStorage { 84 + LoginStorage { 85 + try await loadLogin() 86 + } storeLogin: { login in 87 + try await persist(login) 88 + } 89 + } 90 + 91 + private func loadLogin() async throws -> Login? { 92 + // Decode and return the previously stored login if one exists. 93 + nil 94 + } 95 + 96 + private func persist(_ login: Login) async throws { 97 + // Save the login (for example, in the Keychain or the file system). 98 + } 99 + } 100 + ``` 101 + 102 + ## Perform the OAuth flow 103 + 104 + 1. Configure shared environment state early in your app lifecycle: 105 + 106 + ```swift 107 + await setup( 108 + hostURL: "https://bsky.social", 109 + accessJWT: nil, 110 + refreshJWT: nil, 111 + delegate: self 112 + ) 113 + ``` 114 + 115 + 2. Create the services needed for authentication: 116 + 117 + ```swift 118 + let loginStorage = BlueskyLoginStore().makeStorage() 119 + let loginService = LoginService(jwtGenerator: jwtGenerator, loginStorage: loginStorage) 120 + ``` 121 + 122 + 3. Start the Bluesky OAuth flow. Use the client metadata URL registered with the Authorization Server (for example, the one served from your app’s hosted metadata file). 123 + 124 + ```swift 125 + let login = try await loginService.login( 126 + account: "did:plc:your-user", 127 + clientMetadataEndpoint: "https://example.com/.well-known/coreatprotocol-client.json" 128 + ) 129 + ``` 130 + 131 + 4. Share the authentication context with CoreATProtocol so the networking layer can add DPoP proofs automatically: 132 + 133 + ```swift 134 + await applyAuthenticationContext(login: login, generator: jwtGenerator) 135 + ``` 136 + 137 + 5. When Bluesky returns a new DPoP nonce (`DPoP-Nonce` header), call ``updateResourceDPoPNonce(_:)`` with the latest value before the next request. 138 + 139 + 6. To sign the user out, call ``clearAuthenticationContext()`` and erase any stored login and keychain items. 140 + 141 + ## Make API requests 142 + 143 + Attach the package’s router delegate to your networking stack (for example, the client that wraps ``URLSession``) so that access tokens and DPoP proofs are injected into outgoing requests. 144 + 145 + ```swift 146 + var router = NetworkRouter<SomeEndpoint>(decoder: .atDecoder) 147 + router.delegate = await APEnvironment.current.routerDelegate 148 + ``` 149 + 150 + With the context applied, subsequent calls through ``APRouterDelegate`` will refresh DPoP proofs, hash access tokens into the `ath` claim, and keep the nonce in sync with the server. 151 + 152 + ## Troubleshooting 153 + 154 + - Ensure the DPoP key persists across app launches. If the key changes, all tokens issued by Bluesky become invalid and the user must reauthenticate. 155 + - Always call ``applyAuthenticationContext(login:generator:resourceNonce:)`` after refreshing tokens via ``updateTokens(access:refresh:)`` or custom flows so the delegate has current credentials. 156 + - If Bluesky rejects requests with `use_dpop_nonce`, update the cached value via ``updateResourceDPoPNonce(_:)`` and retry. 157 +