Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@ swift test --parallel
# Show test output
swift test --verbose

# Format code (requires swift-format installation)
swift-format -i -r Sources/ Tests/

# Lint code (requires swiftlint installation)
swiftlint

# Auto-fix linting issues
swiftlint --fix
# Format + lint
# swift-format, swiftlint, periphery, and swift-openapi-generator are pinned
# in mise.toml — do NOT invoke them from PATH directly. Run them THROUGH mise:
mise exec -- swift-format -i -r Sources/ Tests/
mise exec -- swiftlint # lint
mise exec -- swiftlint --fix # auto-fix

# Or run the full lint pipeline (build + swiftlint + header.sh + periphery):
./Scripts/lint.sh
```

### MistDemo Commands
Expand Down Expand Up @@ -167,7 +168,7 @@ MistKit/
| `CloudKitService+WriteOperations.swift` | `modifyRecords`, `createRecord`, `updateRecord`, `deleteRecord` |
| `CloudKitService+ZoneOperations.swift` | `listZones`, `lookupZones(zoneIDs:)`, `fetchZoneChanges(syncToken:)` |
| `CloudKitService+SyncOperations.swift` | `fetchRecordChanges(recordType:syncToken:)`, `fetchAllRecordChanges(recordType:syncToken:)` |
| `CloudKitService+UserOperations.swift` | `fetchCurrentUser()`, `discoverUserIdentities(lookupInfos:)` |
| `CloudKitService+UserOperations.swift` | `fetchCaller()`, `discoverUserIdentities(lookupInfos:)`, `discoverAllUserIdentities()` *(unavailable — pending #28)*, `lookupUsersByEmail(_:)`, `lookupUsersByRecordName(_:)`, `fetchCurrentUser()` (deprecated, forwards to `fetchCaller`) |
| `CloudKitService+AssetOperations.swift` | `uploadAssets`, `requestAssetUploadURL` |
| `CloudKitService+AssetUpload.swift` | `uploadAssetData` |
| `CloudKitService+RecordManaging.swift` | record-managing convenience surface |
Expand All @@ -179,7 +180,15 @@ MistKit/
- `fetchAllRecordChanges(recordType:syncToken:)` — convenience wrapper that auto-paginates using `moreComing`
- `fetchZoneChanges(syncToken:)` → `/zones/changes` — returns `ZoneChangesResult`
- `lookupZones(zoneIDs:)` → `/zones/lookup` — returns `[ZoneInfo]`
- `discoverUserIdentities(lookupInfos:)` → `/users/discover` — takes `[UserIdentityLookupInfo]`, returns `[UserIdentity]`
- `discoverUserIdentities(lookupInfos:)` → POST `/users/discover` — takes `[UserIdentityLookupInfo]`, returns `[UserIdentity]`

**User-Identity Operations (public DB + web-auth required):**
- `fetchCaller()` → `/users/caller` — returns `UserInfo`. Replaces deprecated `fetchCurrentUser()` / `users/current`. Only valid against the public database with web-auth credentials.
- `discoverAllUserIdentities()` → GET `/users/discover` — returns `[UserIdentity]` for every discoverable user in the caller's address book.
- `lookupUsersByEmail(_:)` → POST `/users/lookup/email` — returns `[UserIdentity]`.
- `lookupUsersByRecordName(_:)` → POST `/users/lookup/id` — returns `[UserIdentity]`.

In MistDemo, integration runs targeting these endpoints use `PhaseContext.userContextService` (a public+web-auth `CloudKitService`) which is built from `CLOUDKIT_API_TOKEN` + `CLOUDKIT_WEB_AUTH_TOKEN` regardless of the primary `--database` selection. The `DatabaseConfiguration` / `AuthenticationCredentials` types in `Examples/MistDemo/Sources/MistDemoKit/Configuration/` enforce valid database+auth combinations at construction time.

**Result Types (Sources/MistKit/Service/):**
- `QueryResult` — `records: [RecordInfo]`, `continuationMarker: String?`
Expand Down Expand Up @@ -344,7 +353,7 @@ Key endpoints documented in the OpenAPI spec:
- Records: `/records/query`, `/records/modify`, `/records/lookup`, `/records/changes`
- Zones: `/zones/list`, `/zones/lookup`, `/zones/modify`, `/zones/changes`
- Subscriptions: `/subscriptions/list`, `/subscriptions/lookup`, `/subscriptions/modify`
- Users: `/users/current`, `/users/discover`, `/users/lookup/contacts`
- Users: `/users/caller`, `/users/discover` (POST + GET), `/users/lookup/email`, `/users/lookup/id`
- Assets: `/assets/upload`
- Tokens: `/tokens/create`, `/tokens/register`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,36 @@
// OTHER DEALINGS IN THE SOFTWARE.
//

import Foundation
internal import Foundation
public import MistKit

/// Factory for creating MistKit CloudKitService instances from MistDemo configuration
/// Factory for creating MistKit `CloudKitService` instances from MistDemo
/// configuration.
public struct MistKitClientFactory: Sendable {
/// Create a CloudKitService for `config.database`, choosing auth method automatically.
/// Create a `CloudKitService` configured for `config.database`, choosing
/// auth material automatically based on the populated environment.
///
/// - `.public`: requires `CLOUDKIT_KEY_ID` + `CLOUDKIT_PRIVATE_KEY[_FILE]`
/// - `.private` / `.shared`: requires `CLOUDKIT_API_TOKEN` + `CLOUDKIT_WEB_AUTH_TOKEN`
/// - `.public`: requires `CLOUDKIT_KEY_ID` + `CLOUDKIT_PRIVATE_KEY[_FILE]`,
/// optionally augmented with `CLOUDKIT_API_TOKEN` + `CLOUDKIT_WEB_AUTH_TOKEN`
/// so the same service can also satisfy user-identity routes.
/// - `.private` / `.shared`: requires `CLOUDKIT_API_TOKEN` +
/// `CLOUDKIT_WEB_AUTH_TOKEN`. The resulting web-auth credentials cover
/// user-identity routes too (which CloudKit pins to `.public`).
///
/// When `config.badCredentials == true`, this short-circuits database-based auth
/// selection and returns a service backed by a deliberately invalid web-auth
/// `TokenManager` so the next CloudKit call yields a typed HTTP 401. Because
/// that path always uses web auth, it is **not** supported on `.public` (which
/// requires server-to-server signing) and will throw
/// The service is database-agnostic — operations pick their database at the
/// call site, and `Credentials` resolves the appropriate token manager per
/// call. A single returned service therefore covers every phase the
/// integration runner exercises, including the user-context routes that
/// previously required a second service.
///
/// When `config.badCredentials == true`, this short-circuits and returns a
/// service backed by a deliberately invalid web-auth `TokenManager` so the
/// next CloudKit call yields a typed HTTP 401. Because that path always uses
/// web auth, it is **not** supported on `.public` and will throw
/// `ConfigurationError.badCredentialsOnPublicDB`.
///
/// - Parameter config: The base MistDemo configuration.
/// - Throws: `ConfigurationError` if required credentials are
/// missing, or if `badCredentials` is requested with `.public`.
/// - Returns: A configured `CloudKitService` instance.
/// - Throws: `ConfigurationError` if required credentials are missing, or
/// if `badCredentials` is requested with `.public`.
public static func create(
for config: MistDemoConfig
) throws -> CloudKitService {
Expand All @@ -62,33 +71,28 @@ public struct MistKitClientFactory: Sendable {
}
return try create(from: config, tokenManager: makeBadCredentialsTokenManager())
}
let credentials = try config.toDatabaseCredentials()
let tokenManager = try credentials.makeTokenManager()
let credentials = try config.toPrimaryCredentials()
return try CloudKitService(
containerIdentifier: config.containerIdentifier,
tokenManager: tokenManager,
environment: config.environment,
database: credentials.database
credentials: credentials,
environment: config.environment
)
#endif
}

/// Build a `WebAuthTokenManager` whose tokens pass `validateCredentials()`'s
/// local format check (64-char hex API token, ≥10-char web-auth token) but are
/// guaranteed to be rejected by Apple's servers, producing a real HTTP 401.
///
/// Used by both the factory's `badCredentials` short-circuit and by
/// `DemoErrorsRunner.runUnauthorized` so the same definition feeds every
/// 401-demo path.
/// local format check (64-char hex API token, ≥10-char web-auth token) but
/// are guaranteed to be rejected by Apple's servers, producing a real HTTP
/// 401.
internal static func makeBadCredentialsTokenManager() -> WebAuthTokenManager {
WebAuthTokenManager(
apiToken: String(repeating: "0", count: 64),
webAuthToken: String(repeating: "a", count: 100)
)
}

/// Create a CloudKitService with a caller-supplied TokenManager, targeting
/// `config.database`.
/// Create a `CloudKitService` with a caller-supplied `TokenManager`. Used
/// by the `--bad-credentials` demo path.
public static func create(
from config: MistDemoConfig,
tokenManager: any TokenManager
Expand All @@ -101,8 +105,7 @@ public struct MistKitClientFactory: Sendable {
return try CloudKitService(
containerIdentifier: config.containerIdentifier,
tokenManager: tokenManager,
environment: config.environment,
database: config.database
environment: config.environment
)
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public struct CurrentUserCommand: MistDemoCommand, OutputFormatting {
let client = try MistKitClientFactory.create(for: config.base)

// Fetch current user information
let userInfo = try await client.fetchCurrentUser()
let userInfo = try await client.fetchCaller()

// Filter fields if requested
let filteredUser = filterUserFields(userInfo, fields: config.fields)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal struct DemoErrorsRunner {
from: config.with(database: .private),
tokenManager: badTokenManager
)
_ = try await service.fetchCurrentUser()
_ = try await service.fetchCaller()
print("⚠️ Expected 401 but call succeeded — credentials may not be validated server-side.")
} catch let error as CloudKitError {
printCloudKitError(error, expectedStatus: 401)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ public struct LookupZonesCommand: MistDemoCommand, OutputFormatting {
print(" - \(name)")
}

let zones = try await service.lookupZones(zoneIDs: zoneIDs)
let zones = try await service.lookupZones(
zoneIDs: zoneIDs,
database: config.base.database
)
print("\n✅ Found \(zones.count) zone(s):")
for zone in zones {
print(" - \(zone.zoneName)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ public struct TestIntegrationCommand: MistDemoCommand {
--asset-size <kb> Asset size in KB (default: 100)
--skip-cleanup Skip cleanup after test
--verbose Run in verbose mode
--lookup-email <email> Email for users/lookup/email phase
(CLOUDKIT_LOOKUP_EMAIL); must belong
to an iCloud account discoverable to
the caller, otherwise the phase skips

EXAMPLES:
mistdemo test-integration --verbose
mistdemo test-integration --skip-cleanup --verbose
mistdemo test-integration --lookup-email me@example.com

NOTES:
- Requires CLOUDKIT_KEY_ID and CLOUDKIT_PRIVATE_KEY
Expand All @@ -75,15 +80,22 @@ public struct TestIntegrationCommand: MistDemoCommand {
/// Executes the command.
public func execute() async throws {
let service = try MistKitClientFactory.create(for: config.base)
// A single service handles every phase: server-to-server signing on
// `.public` for record ops, plus web-auth for user-identity routes when
// the API/web-auth env vars are populated. The resolver picks the right
// token manager per call.
let supportsUserContextPhases = config.base.hasUserContextCredentials

let runner = IntegrationTestRunner(
service: service,
supportsUserContextPhases: supportsUserContextPhases,
containerIdentifier: config.base.containerIdentifier,
database: config.base.database,
recordCount: config.recordCount,
assetSizeKB: config.assetSizeKB,
skipCleanup: config.skipCleanup,
verbose: config.verbose
verbose: config.verbose,
lookupEmail: config.lookupEmail
)

try await runner.runBasicWorkflow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ public struct TestPrivateCommand: MistDemoCommand {
--asset-size <kb> Asset size in KB (default: 100)
--skip-cleanup Skip cleanup after test
--verbose Run in verbose mode
--lookup-email <email> Email for users/lookup/email phase
(CLOUDKIT_LOOKUP_EMAIL); must belong
to an iCloud account discoverable to
the caller, otherwise the phase skips

EXAMPLES:
mistdemo test-private --verbose
mistdemo test-private --skip-cleanup --verbose
mistdemo test-private --lookup-email me@example.com

NOTES:
- Requires CLOUDKIT_API_TOKEN and
Expand All @@ -76,15 +81,21 @@ public struct TestPrivateCommand: MistDemoCommand {
/// Executes the command.
public func execute() async throws {
let service = try MistKitClientFactory.create(for: config.base)
// Private-database flows always carry web-auth credentials, so the same
// service can also serve user-identity routes when this command needs
// them. Per-call resolution picks the right token manager.
let supportsUserContextPhases = config.base.hasUserContextCredentials

let runner = IntegrationTestRunner(
service: service,
supportsUserContextPhases: supportsUserContextPhases,
containerIdentifier: config.base.containerIdentifier,
database: .private,
recordCount: config.recordCount,
assetSizeKB: config.assetSizeKB,
skipCleanup: config.skipCleanup,
verbose: config.verbose
verbose: config.verbose,
lookupEmail: config.lookupEmail
)

try await runner.runPrivateWorkflow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import Foundation

/// Configuration errors.
internal enum ConfigurationError: LocalizedError {
case missingAPIToken
case invalidEnvironment(String)
case invalidDatabase(String)
case missingRequired(String, suggestion: String)
Expand All @@ -42,9 +41,6 @@ internal enum ConfigurationError: LocalizedError {

internal var errorDescription: String? {
switch self {
case .missingAPIToken:
"CloudKit API token is required. "
+ "Set CLOUDKIT_API_TOKEN environment variable or use --api-token"
case .invalidEnvironment(let env):
"Invalid environment '\(env)'. Must be 'development' or 'production'"
case .invalidDatabase(let database):
Expand Down

This file was deleted.

Loading
Loading