CoreATProtocol#
A Swift package providing the foundational networking layer for interacting with the AT Protocol (Authenticated Transfer Protocol). This library handles the core HTTP communication, authentication token management, and request/response encoding required to build AT Protocol clients.
Overview#
CoreATProtocol is designed to be protocol-agnostic within the AT Protocol ecosystem. It provides the networking infrastructure that higher-level packages (like bskyKit for Bluesky) can build upon to implement specific lexicons.
Key Features#
- Modern Swift Concurrency - Built with Swift 6.2 using async/await and actors for thread-safe operations
- Global Actor Isolation - Uses
@APActorfor consistent thread safety across all AT Protocol operations - Flexible Network Routing - Generic
NetworkRouterthat works with any endpoint conforming toEndpointType - Automatic Token Management - Built-in support for JWT access/refresh token handling with automatic retry on expiration
- Multiple Parameter Encodings - URL, JSON, and combined encoding strategies for request parameters
- AT Protocol Error Handling - Typed error responses matching AT Protocol error specifications
- Testable Architecture - Protocol-based design allows easy mocking for unit tests
Requirements#
- Swift 6.2+
- iOS 26.0+ / macOS 26.0+ / watchOS 26.0+ / tvOS 26.0+ / Mac Catalyst 26.0+
Installation#
Swift Package Manager#
Add CoreATProtocol to your Package.swift dependencies:
dependencies: [
.package(url: "https://tangled.org/@sparrowtek.com/CoreATProtocol", branch: "main"),
]
Then add it to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["CoreATProtocol"]
),
Or in Xcode: File > Add Package Dependencies and enter:
https://tangled.org/@sparrowtek.com/CoreATProtocol
Usage#
Initial Setup#
Configure the environment with your host URL and authentication tokens:
import CoreATProtocol
// Setup with host and tokens
await setup(
hostURL: "https://bsky.social",
accessJWT: "your-access-token",
refreshJWT: "your-refresh-token"
)
// Or update tokens later
await updateTokens(access: newAccessToken, refresh: newRefreshToken)
// Change host
await update(hostURL: "https://different-pds.example")
Defining Endpoints#
Create endpoints by conforming to EndpointType:
import CoreATProtocol
enum MyEndpoint: EndpointType {
case getProfile(actor: String)
case createPost(text: String)
var baseURL: URL {
get async {
URL(string: APEnvironment.current.host ?? "https://bsky.social")!
}
}
var path: String {
switch self {
case .getProfile:
return "/xrpc/app.bsky.actor.getProfile"
case .createPost:
return "/xrpc/com.atproto.repo.createRecord"
}
}
var httpMethod: HTTPMethod {
switch self {
case .getProfile: return .get
case .createPost: return .post
}
}
var task: HTTPTask {
get async {
switch self {
case .getProfile(let actor):
return .requestParameters(encoding: .urlEncoding(parameters: ["actor": actor]))
case .createPost(let text):
let body: [String: Any] = ["text": text]
return .requestParameters(encoding: .jsonEncoding(parameters: body))
}
}
}
var headers: HTTPHeaders? {
get async { nil }
}
}
Making Requests#
Use NetworkRouter to execute requests:
@APActor
class MyATClient {
private let router = NetworkRouter<MyEndpoint>()
init() {
router.delegate = APEnvironment.current.routerDelegate
}
func getProfile(actor: String) async throws -> ProfileResponse {
try await router.execute(.getProfile(actor: actor))
}
}
Custom JSON Decoding#
Use the pre-configured AT Protocol decoder for proper date handling:
let router = NetworkRouter<MyEndpoint>(decoder: .atDecoder)
Error Handling#
Handle AT Protocol specific errors:
do {
let profile: Profile = try await router.execute(.getProfile(actor: "did:plc:example"))
} catch let error as AtError {
switch error {
case .message(let errorMessage):
print("AT Error: \(errorMessage.error) - \(errorMessage.message ?? "")")
case .network(let networkError):
switch networkError {
case .statusCode(let code, let data):
print("HTTP \(code?.rawValue ?? 0)")
case .encodingFailed:
print("Failed to encode request")
default:
print("Network error: \(networkError)")
}
}
}
Architecture#
Core Components#
| Component | Description |
|---|---|
APActor |
Global actor ensuring thread-safe access to AT Protocol state |
APEnvironment |
Singleton holding host URL, tokens, and delegates |
NetworkRouter |
Generic router executing typed endpoint requests |
EndpointType |
Protocol defining API endpoint requirements |
ParameterEncoding |
Enum supporting URL, JSON, and hybrid encoding |
AtError |
AT Protocol error types with message parsing |
Thread Safety#
All AT Protocol operations are isolated to @APActor ensuring thread-safe access:
@APActor
public func myFunction() async {
// Safe access to APEnvironment.current
}
Parameter Encoding Options#
// URL query parameters
.urlEncoding(parameters: ["key": "value"])
// JSON body
.jsonEncoding(parameters: ["key": "value"])
// Pre-encoded JSON data
.jsonDataEncoding(data: jsonData)
// Encodable objects
.jsonEncodableEncoding(encodable: myStruct)
// Combined URL + JSON body
.urlAndJsonEncoding(urlParameters: ["q": "search"], bodyParameters: ["data": "value"])
Related Packages#
- bskyKit - Bluesky-specific lexicon implementations built on CoreATProtocol
License#
This project is licensed under an MIT license.
Contributing#
It 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.
By participating in this project you agree to abide by the Contributor Code of Conduct.