this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 259 lines 7.5 kB view raw view rendered
1# CoreATProtocol 2 3A Swift package providing the foundational networking layer for interacting with the [AT Protocol](https://atproto.com) (Authenticated Transfer Protocol). This library handles the core HTTP communication, authentication token management, and request/response encoding required to build AT Protocol clients. 4 5## Overview 6 7CoreATProtocol is designed to be protocol-agnostic within the AT Protocol ecosystem. It provides the networking infrastructure that higher-level packages (like [bskyKit](https://tangled.org/@sparrowtek.com/bskyKit) for Bluesky) can build upon to implement specific lexicons. 8 9### Key Features 10 11- **Modern Swift Concurrency** - Built with Swift 6.2 using async/await and actors for thread-safe operations 12- **Global Actor Isolation** - Uses `@APActor` for consistent thread safety across all AT Protocol operations 13- **Flexible Network Routing** - Generic `NetworkRouter` that works with any endpoint conforming to `EndpointType` 14- **ATProto OAuth** - First-party OAuth client with DPoP, PAR, issuer validation, and identity resolution (handle or DID) 15- **Automatic Token Management** - Built-in support for JWT access/refresh token handling with automatic retry on expiration 16- **Multiple Parameter Encodings** - URL, JSON, and combined encoding strategies for request parameters 17- **AT Protocol Error Handling** - Typed error responses matching AT Protocol error specifications 18- **Testable Architecture** - Protocol-based design allows easy mocking for unit tests 19 20## Requirements 21 22- Swift 6.2+ 23- iOS 17.0+ / macOS 14.0+ / watchOS 11.0+ / tvOS 17.0+ / Mac Catalyst 17.0+ 24 25## Installation 26 27### Swift Package Manager 28 29Add CoreATProtocol to your `Package.swift` dependencies: 30 31```swift 32dependencies: [ 33 .package(url: "https://tangled.org/@sparrowtek.com/CoreATProtocol", branch: "main"), 34] 35``` 36 37Then add it to your target dependencies: 38 39```swift 40.target( 41 name: "YourTarget", 42 dependencies: ["CoreATProtocol"] 43), 44``` 45 46Or in Xcode: File > Add Package Dependencies and enter: 47``` 48https://tangled.org/@sparrowtek.com/CoreATProtocol 49``` 50 51## Usage 52 53### Initial Setup 54 55Configure the environment with your host URL and authentication tokens: 56 57```swift 58import CoreATProtocol 59 60// Setup with host and tokens 61await setup( 62 hostURL: "https://bsky.social", 63 accessJWT: "your-access-token", 64 refreshJWT: "your-refresh-token" 65) 66 67// Or update tokens later 68await updateTokens(access: newAccessToken, refresh: newRefreshToken) 69 70// Change host 71await update(hostURL: "https://different-pds.example") 72``` 73 74### OAuth Authentication 75 76Use `ATProtoOAuth` for OAuth login flows: 77 78```swift 79import CoreATProtocol 80 81let oauth = await ATProtoOAuth( 82 config: .init( 83 clientMetadataURL: "https://example.com/client-metadata.json", 84 redirectURI: "com.example.app:/oauth/callback" 85 ), 86 storage: .init( 87 retrieveLogin: { nil }, 88 storeLogin: { _ in }, 89 retrievePrivateKey: { nil }, 90 storePrivateKey: { _ in } 91 ) 92) 93 94let result = try await oauth.authenticate(identifier: "alice.bsky.social") { authURL, _ in 95 // Present authURL to the user and return callback URL. 96 fatalError("Implement user-auth callback handling") 97} 98 99await setup(hostURL: result.pdsEndpoint, accessJWT: result.accessToken, refreshJWT: result.refreshToken) 100``` 101 102### Defining Endpoints 103 104Create endpoints by conforming to `EndpointType`: 105 106```swift 107import CoreATProtocol 108 109enum MyEndpoint: EndpointType { 110 case getProfile(actor: String) 111 case createPost(text: String) 112 113 var baseURL: URL { 114 get async { 115 URL(string: APEnvironment.current.host ?? "https://bsky.social")! 116 } 117 } 118 119 var path: String { 120 switch self { 121 case .getProfile: 122 return "/xrpc/app.bsky.actor.getProfile" 123 case .createPost: 124 return "/xrpc/com.atproto.repo.createRecord" 125 } 126 } 127 128 var httpMethod: HTTPMethod { 129 switch self { 130 case .getProfile: return .get 131 case .createPost: return .post 132 } 133 } 134 135 var task: HTTPTask { 136 get async { 137 switch self { 138 case .getProfile(let actor): 139 return .requestParameters(encoding: .urlEncoding(parameters: ["actor": actor])) 140 case .createPost(let text): 141 let body: [String: Any] = ["text": text] 142 return .requestParameters(encoding: .jsonEncoding(parameters: body)) 143 } 144 } 145 } 146 147 var headers: HTTPHeaders? { 148 get async { nil } 149 } 150} 151``` 152 153### Making Requests 154 155Use `NetworkRouter` to execute requests: 156 157```swift 158@APActor 159class MyATClient { 160 private let router = NetworkRouter<MyEndpoint>() 161 162 init() { 163 router.delegate = APEnvironment.current.routerDelegate 164 } 165 166 func getProfile(actor: String) async throws -> ProfileResponse { 167 try await router.execute(.getProfile(actor: actor)) 168 } 169} 170``` 171 172### Custom JSON Decoding 173 174Use the pre-configured AT Protocol decoder for proper date handling: 175 176```swift 177let router = NetworkRouter<MyEndpoint>(decoder: .atDecoder) 178``` 179 180### Error Handling 181 182Handle AT Protocol specific errors: 183 184```swift 185do { 186 let profile: Profile = try await router.execute(.getProfile(actor: "did:plc:example")) 187} catch let error as AtError { 188 switch error { 189 case .message(let errorMessage): 190 print("AT Error: \(errorMessage.error) - \(errorMessage.message ?? "")") 191 case .network(let networkError): 192 switch networkError { 193 case .statusCode(let code, let data): 194 print("HTTP \(code?.rawValue ?? 0)") 195 case .encodingFailed: 196 print("Failed to encode request") 197 default: 198 print("Network error: \(networkError)") 199 } 200 } 201} 202``` 203 204## Architecture 205 206### Core Components 207 208| Component | Description | 209|-----------|-------------| 210| `APActor` | Global actor ensuring thread-safe access to AT Protocol state | 211| `APEnvironment` | Singleton holding host URL, tokens, and delegates | 212| `NetworkRouter` | Generic router executing typed endpoint requests | 213| `EndpointType` | Protocol defining API endpoint requirements | 214| `ParameterEncoding` | Enum supporting URL, JSON, and hybrid encoding | 215| `AtError` | AT Protocol error types with message parsing | 216 217### Thread Safety 218 219All AT Protocol operations are isolated to `@APActor` ensuring thread-safe access: 220 221```swift 222@APActor 223public func myFunction() async { 224 // Safe access to APEnvironment.current 225} 226``` 227 228## Parameter Encoding Options 229 230```swift 231// URL query parameters 232.urlEncoding(parameters: ["key": "value"]) 233 234// JSON body 235.jsonEncoding(parameters: ["key": "value"]) 236 237// Pre-encoded JSON data 238.jsonDataEncoding(data: jsonData) 239 240// Encodable objects 241.jsonEncodableEncoding(encodable: myStruct) 242 243// Combined URL + JSON body 244.urlAndJsonEncoding(urlParameters: ["q": "search"], bodyParameters: ["data": "value"]) 245``` 246 247## Related Packages 248 249- **[bskyKit](https://tangled.org/@sparrowtek.com/bskyKit)** - Bluesky-specific lexicon implementations built on CoreATProtocol 250 251## License 252 253This project is licensed under an [MIT license](https://tangled.org/sparrowtek.com/CoreATProtocol/blob/main/LICENSE). 254 255## Contributing 256 257It is always a good idea to discuss before taking on a significant task. That said, I have a strong bias towards enthusiasm. If you are excited about doing something, I'll do my best to get out of your way. 258 259By participating in this project you agree to abide by the [Contributor Code of Conduct](https://tangled.org/sparrowtek.com/CoreATProtocol/blob/main/CODE_OF_CONDUCT.md).