this repo has no description
Swift 97.0%
Other 3.0%
4 3 0

Clone this repository

https://tangled.org/sparrowtek.com/CoreATProtocol
git@tangled.org:sparrowtek.com/CoreATProtocol

For self-hosted knots, clone URLs may differ based on your setup.

README.md

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 @APActor for consistent thread safety across all AT Protocol operations
  • Flexible Network Routing - Generic NetworkRouter that works with any endpoint conforming to EndpointType
  • 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"])
  • 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.