Skip to content

Latest commit

 

History

History
529 lines (413 loc) · 22.9 KB

File metadata and controls

529 lines (413 loc) · 22.9 KB

MistKit Logo

MistKit

SwiftPM Swift Versions Platforms License GitHub Workflow Status Codecov CodeFactor Grade Maintainability Documentation

A Swift Package for Server-Side and Command-Line Access to CloudKit Web Services

Table of Contents

Overview

MistKit provides a modern Swift interface to CloudKit Web Services REST API, enabling cross-platform CloudKit access for server-side Swift applications, command-line tools, and platforms where the CloudKit framework isn't available.

Built with Swift concurrency (async/await) and designed for modern Swift applications, MistKit supports all three CloudKit authentication methods and provides type-safe access to CloudKit operations.

Key Features

  • 🌍 Cross-Platform Support: Works on macOS, iOS, tvOS, watchOS, visionOS, and Linux
  • ⚡ Modern Swift: Built with Swift 6 concurrency features and structured error handling
  • 🔐 Multiple Authentication Methods: API token, web authentication, and server-to-server authentication
  • 🛡️ Type-Safe: Comprehensive type safety with Swift's type system
  • 📋 OpenAPI-Based: Generated from CloudKit Web Services OpenAPI specification using swift-openapi-generator
  • 🔒 Secure: Built-in security best practices and credential management

Getting Started

Installation

Add MistKit to your Package.swift:

dependencies: [
    .package(url: "https://github.com/brightdigit/MistKit.git", from: "1.0.0-beta.2")
]

Or add it through Xcode:

  1. File → Add Package Dependencies
  2. Enter: https://github.com/brightdigit/MistKit.git
  3. Select version and add to your target

Requirements

  • Swift 6.1+
  • Xcode 16.0+ (for iOS/macOS development)
  • Linux: Ubuntu 18.04+ with Swift 6.1+

Platform Support

Minimum Platform Versions

Platform Minimum Version
macOS 11.0+
iOS 14.0+
tvOS 14.0+
watchOS 7.0+
visionOS 1.0+
Linux Ubuntu 18.04+
Windows 10+

Quick Start

1. Choose Your Authentication Method

MistKit supports three credential types via the Credentials value. The service does not carry a database — each operation picks its database (and signing method, for the public database) at the call site.

API Token (read-only against the public database)
import MistKit

let credentials = try Credentials(
    apiAuth: APICredentials(
        apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]!
    )
)
let service = CloudKitService(
    containerIdentifier: "iCloud.com.example.MyApp",
    credentials: credentials
)
Web Authentication (user-context routes, private/shared database)
let credentials = try Credentials(
    apiAuth: APICredentials(
        apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]!,
        webAuthToken: userWebAuthToken
    )
)
let service = CloudKitService(
    containerIdentifier: "iCloud.com.example.MyApp",
    credentials: credentials
)
Server-to-Server (public database only)
let credentials = try Credentials(
    serverToServer: ServerToServerCredentials(
        keyID: ProcessInfo.processInfo.environment["CLOUDKIT_KEY_ID"]!,
        privateKey: .file(path: "private_key.pem")
    )
)
let service = CloudKitService(
    containerIdentifier: "iCloud.com.example.MyApp",
    credentials: credentials,
    environment: .production
)

Provide both apiAuth and serverToServer to a single Credentials when one service must hit public-database routes via S2S signing and user-context routes via web-auth — MistKit picks the appropriate token manager per call.

2. Call an Operation (database chosen per call)

let records = try await service.queryRecords(
    recordType: "Post",
    database: .public(.prefers(.serverToServer))
)

Database.public carries a PublicAuthPreference: .prefers(.serverToServer) / .prefers(.webAuth) (fall back if not configured) or .requires(.serverToServer) / .requires(.webAuth) (throw if not configured). Private/shared always use web-auth.

Usage

Authentication

API Token Authentication

  1. Get API Token:

  2. Set Environment Variable:

    export CLOUDKIT_API_TOKEN="your_api_token_here"
  3. Use in Code:

    let credentials = try Credentials(
        apiAuth: APICredentials(
            apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]!
        )
    )
    let service = CloudKitService(
        containerIdentifier: "iCloud.com.example.MyApp",
        credentials: credentials
    )

Web Authentication

Web authentication enables user-specific operations and requires both an API token and a web authentication token. The token can be obtained either through CloudKit JS authentication (browser flow) or from an iOS/macOS app via CKFetchWebAuthTokenOperation, which exchanges the user's existing iCloud session for a token your backend can use.

let credentials = try Credentials(
    apiAuth: APICredentials(apiToken: apiToken, webAuthToken: webAuthToken)
)
let service = CloudKitService(
    containerIdentifier: "iCloud.com.example.MyApp",
    credentials: credentials
)

Server-to-Server Authentication

Server-to-server authentication provides enterprise-level access using ECDSA P-256 key signing. Note that this method only supports the public database.

  1. Generate Key Pair:

    # Generate private key
    openssl ecparam -genkey -name prime256v1 -noout -out private_key.pem
    
    # Extract public key
    openssl ec -in private_key.pem -pubout -out public_key.pem
  2. Upload Public Key: Upload the public key to Apple Developer Console

  3. Use in Code (the simplest path — Credentials resolves the PEM at first use):

    let credentials = try Credentials(
        serverToServer: ServerToServerCredentials(
            keyID: "your_key_id",
            privateKey: .file(path: "private_key.pem")
        )
    )
    let service = CloudKitService(
        containerIdentifier: "iCloud.com.example.MyApp",
        credentials: credentials,
        environment: .production
    )
    
    // Each call selects its database scope explicitly:
    let records = try await service.queryRecords(
        recordType: "Post",
        database: .public(.requires(.serverToServer))
    )

    To plug in a custom TokenManager (e.g. with shared connection pooling), use the tokenManager: initializer instead:

    let pemString = try String(contentsOfFile: "private_key.pem", encoding: .utf8)
    let serverManager = try ServerToServerAuthManager(
        keyID: "your_key_id",
        pemString: pemString
    )
    let service = CloudKitService(
        containerIdentifier: "iCloud.com.example.MyApp",
        tokenManager: serverManager,
        environment: .production
    )

Error Handling

MistKit provides comprehensive error handling with typed errors:

do {
    let credentials = try Credentials(
        apiAuth: APICredentials(apiToken: apiToken)
    )
    let service = CloudKitService(
        containerIdentifier: "iCloud.com.example.MyApp",
        credentials: credentials
    )
    // Perform operations — each call picks its database, e.g.:
    let posts = try await service.queryRecords(
        recordType: "Post",
        database: .public(.prefers(.serverToServer))
    )
} catch let error as CloudKitError {
    print("CloudKit error: \\(error.localizedDescription)")
} catch let error as TokenManagerError {
    print("Authentication error: \\(error.localizedDescription)")
} catch let error as CredentialsValidationError {
    print("Credentials error: \\(error.localizedDescription)")
} catch {
    print("Unexpected error: \\(error)")
}

Error Types

  • CloudKitError: CloudKit Web Services API errors (typed throws on every operation)
  • CredentialsValidationError: Surfaces when Credentials.init is called with neither apiAuth nor serverToServer
  • TokenManagerError: Authentication and credential errors
  • TokenStorageError: Token storage and persistence errors

Advanced Usage

More Operations

Beyond querying and CRUD, MistKit covers zones, subscriptions, push tokens, and asset re-referencing. Every call takes an explicit database:.

// Zones
let zone = try await service.createZone(
    zoneName: "Notes",
    database: .private
)
try await service.deleteZone(zoneName: "Notes", database: .private)

// Subscriptions
let subs = try await service.listSubscriptions(database: .private)
let one = try await service.lookupSubscriptions(ids: ["sub-1"], database: .private)
// Create/update/delete via service.modifySubscriptions(_:database:)
// (takes [SubscriptionOperation], returns [SubscriptionResult]).

// APNs push tokens
let token = try await service.createAPNsToken(
    environment: .development,
    database: .private
)
try await service.registerAPNsToken(
    token.apnsToken,
    environment: .development,
    database: .private
)

// Re-reference existing CDN assets without re-uploading bytes
let assets = try await service.rereferenceAssets(
    [(recordName: "rec-1", fieldName: "photo")],
    database: .private
)

Auto-Chunking Conveniences

CloudKit caps batch requests at 200 items. lookupAllRecords and the lookupInfos: form of discoverAllUserIdentities split oversized inputs into ≤maxRecordsPerRequest (200) batches automatically and concatenate the results in input order — no manual chunking required.

let records = try await service.lookupAllRecords(
    recordNames: thousandsOfNames,   // chunked into 200-item requests
    database: .private
)

let identities = try await service.discoverAllUserIdentities(
    lookupInfos: manyLookupInfos,
    batchSize: 200
)

HTTP Transport

Non-WASI platforms default to URLSessionTransport — no transport plumbing is required. On Apple platforms, the default convenience initializer used in the examples above wires up URLSessionTransport automatically.

WASI builds use the generic, transport-accepting initializer; see Sources/MistKit/CloudKitService/CloudKitService+Initialization.swift for the internal entry point. A custom transport on Apple platforms (e.g. for server-side Swift with AsyncHTTPClient) is not yet exposed in the public v1.0.0-beta surface — track via the project roadmap.

Adaptive Token Manager

For applications that might upgrade from API-only to web authentication:

let adaptiveManager = AdaptiveTokenManager(
    apiToken: apiToken,
    storage: storage
)

// Later, upgrade to web authentication
try await adaptiveManager.upgradeToWebAuthentication(webAuthToken: webToken)

Examples

Check out the Examples/ directory for complete working examples:

  • MistDemo: Web-based CloudKit authentication demo with automatic token capture
  • BushelCloud: Server-to-Server auth demo syncing macOS restore images, Xcode, and Swift versions — backend for the Bushel app
  • CelestraCloud: RSS reader demonstrating CloudKit query filtering, sorting, and web etiquette patterns — backend for the Celestra app, built with SyndiKit

Documentation

Apple References

Related Swift Packages

License

MistKit is released under the MIT License. See LICENSE for details.

Acknowledgments

Roadmap

v1.0.0-alpha.1

v1.0.0-alpha.2

v1.0.0-alpha.3

v1.0.0-alpha.4

v1.0.0-alpha.5

v1.0.0-beta.1

Querying & Sync

  • Query pagination with continuation markers (#306) ✅
  • Operation classification & batch sync result tracking (#296) ✅

Authentication

  • AuthenticationMiddleware refactor — each Authenticator applies itself (#294) ✅
  • Strengthened environment & database configuration validation (#293) ✅

Error Handling

  • Typed TokenManagerError and safe RecordOperation conversion (#305) ✅
  • Move CloudKitResponseType defaults to protocol extension (#292) ✅

Concurrency

  • Replace custom AsyncChannel with swift-async-algorithms (#280) ✅

MistDemo

  • --database flag and demo-errors command (#282) ✅
  • Test split, CRUD commands, auth fix, native app (#271 / #273) ✅
  • IntegrationTestRunner refactored into protocol-based phase pipeline (#283) ✅

Tooling & CI

  • Test suite improvements (#286 / #287) ✅
  • CI updates for May 2026 (#277) ✅
  • Fail lint job when any command fails (#303) ✅

v1.0.0-beta.2

Backlog / Post-beta

v1.0.0

v1.1.0

Support


MistKit: Bringing CloudKit to every Swift platform 🌟