Skip to content

Resolve #312: public+web-auth user-identity endpoints (#310, #311, #27, #28, #34, #35)#315

Merged
leogdion merged 11 commits into
v1.0.0-beta.1from
312-public-database-web-authuser-identity
May 9, 2026
Merged

Resolve #312: public+web-auth user-identity endpoints (#310, #311, #27, #28, #34, #35)#315
leogdion merged 11 commits into
v1.0.0-beta.1from
312-public-database-web-authuser-identity

Conversation

@leogdion
Copy link
Copy Markdown
Member

@leogdion leogdion commented May 8, 2026

Closes #310, #311, #27, #34, #35.
Defers #28 (kept open with live-verification findings — see #28 comment).

Summary

CloudKit's user-identity endpoints (`/users/caller`, `/users/discover`, `/users/lookup/*`) require public-database routing with web-auth credentials — a combination MistKit's convenience layer and MistDemo's `DatabaseCredentials` enum couldn't previously express. This PR unblocks the family.

Verified live (`mistdemo test-integration --verbose`, 12/12 phases green)

`mistdemo test-private --verbose` is also fully green (12/12). `FetchCurrentUserPhase` was removed from the private pipeline because CloudKit rejects `users/caller` against the private database.

Deferred: #28

The Swift wrapper for GET `/users/discover` (`discoverAllUserIdentities()`) ships behind `@available(*, unavailable, message: ...)` pointing at #28. Live testing returned HTTP 500 from Apple, and the GET form does not appear in Apple's REST documentation — only in CloudKitJS. The OpenAPI definition, generated client, path builder, response processor, and Output extension all remain in place; unblocking is a one-line `@available` removal once the correct REST shape is determined. Findings written up on #28.

MistDemo refactor

Replaced `DatabaseCredentials` enum (which baked in public⇒S2S/private⇒webAuth) with two orthogonal types:

  • `AuthenticationCredentials` — `serverToServer` / `webAuth`
  • `DatabaseConfiguration` — pairs a database with auth; `make(database:authentication:)` rejects invalid combinations (private+S2S, shared+S2S) at construction time

`PhaseContext.userContextService` threads an optional public+web-auth service through the integration runner. `PublicDatabaseTest` conditionally appends user-identity phases when the credential is available.

Test plan

🤖 Generated with Claude Code

leogdion and others added 2 commits May 8, 2026 14:13
…aller migration

Implements the library side of #312 — adding/renaming user-identity endpoints
that require public-database routing with web-auth (user-context) credentials,
and unblocking the convenience initializers from their hardcoded database/
environment defaults.

#310: `CloudKitService` convenience initializers now accept `database:` and
`environment:` parameters with defaults that preserve current behavior.

#311: `users/current` → `users/caller`. Renamed in openapi.yaml and the
generated client; added a hand-written `fetchCaller()` plus an
`@available(*, deprecated, renamed: "fetchCaller")` `fetchCurrentUser()`
shim that forwards to the new method.

#28: GET `/users/discover` (`discoverAllUserIdentities`).

#34: POST `/users/lookup/email` (`lookupUsersByEmail`).

#35: POST `/users/lookup/id` (`lookupUsersByRecordName`).

The three new endpoints reuse `DiscoverResponse` for parsing — Apple returns
`{ users: [UserIdentity] }` for all of them. Each ships with a 5-file
test suite mirroring the existing `DiscoverUserIdentities` pattern.

#33 (`users/lookup/contacts`) intentionally not implemented: Apple has marked
the endpoint deprecated. To be closed as not-planned with a pointer to #34/#35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…text phases

Refactors MistDemo's CloudKit configuration model and integration runner to
support the public+web-auth combination required by the user-identity
endpoints landed in the prior commit.

**Configuration refactor.** Replaces the `DatabaseCredentials` enum (which
coupled database choice to a single auth method per case, baking in a
public⇒S2S/private⇒webAuth assumption) with two orthogonal types:

  - `AuthenticationCredentials` — `serverToServer(keyID:privateKey:)` /
    `webAuth(apiToken:webAuthToken:)`
  - `DatabaseConfiguration` — pairs a `MistKit.Database` with an
    `AuthenticationCredentials`. The `make(database:authentication:)` factory
    rejects private+S2S and shared+S2S (which CloudKit rejects) so invalid
    combinations remain unrepresentable, while public+webAuth is now a valid
    construction.

`MistKitClientFactory.create(for:)` consumes `toPrimaryConfiguration()`;
the new `createUserContext(for:)` returns the optional public+web-auth
service from `toUserContextConfiguration()` when web-auth tokens are
configured.

**Phase plumbing.** `PhaseContext` and `IntegrationTestRunner` now thread an
optional `userContextService: CloudKitService?`. `PublicDatabaseTest` takes
`includeUserContextPhases:` and conditionally appends the new user-identity
phases:

  - `FetchCallerPhase` (renamed from `FetchCurrentUserPhase`)
  - `DiscoverUserIdentitiesPhase` (existed; updated to use userContextService)
  - `DiscoverAllUserIdentitiesPhase` (#28)
  - `LookupUsersByEmailPhase` (#34)
  - `LookupUsersByRecordNamePhase` (#35)

`PrivateDatabaseTest` no longer includes `FetchCurrentUserPhase`: CloudKit
rejects `users/caller` against the private database, matching the rest of
the user-identity family.

**Call-site updates.** `CurrentUserCommand` and `DemoErrorsRunner` swap
`fetchCurrentUser()` → `fetchCaller()`. `TestIntegrationCommand` and
`TestPrivateCommand` now build and pass `userContextService`.

Tests for `AuthenticationCredentials`, `DatabaseConfiguration.make`
validation, and `MistDemoConfig.toPrimaryConfiguration` /
`toUserContextConfiguration` ship alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: feb943b7-4b66-47f3-8b29-bd3790ae588b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 312-public-database-web-authuser-identity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Code Review — PR #315: public+web-auth user-identity endpoints

Overview

This PR correctly identifies and implements that CloudKit's user-identity endpoints require a public DB + web-auth combination that the previous DatabaseCredentials enum couldn't express. The refactor into orthogonal AuthenticationCredentials + DatabaseConfiguration types is architecturally clean and the new endpoints follow existing patterns well.


Code Quality & Conventions

Positives:

  • The @available(*, deprecated, renamed: "fetchCaller", message: ...) shim on fetchCurrentUser() is correct Swift API evolution — source-compatible with a clear migration path.
  • DatabaseConfiguration.make(database:authentication:) factory that throws on invalid combinations is the right approach. Making invalid states unrepresentable is better than defensive guards scattered across call sites.
  • The 5-file test pattern (Suite + Helpers + SuccessCases + Validation + main) is consistently applied to all three new endpoint suites.
  • Splitting AuthenticationCredentials from DatabaseConfiguration cleans up the awkward ".publicDatabase vs .privateDatabase" naming that was really encoding two orthogonal concepts.

Suggestions:

  1. createXxxPath(containerIdentifier:) doc comments — These comments just restate what the function name already says ("Create a standard path for getCaller requests"). Per project convention (CLAUDE.md), comments should only appear when the WHY is non-obvious. The four new path helpers repeat this pattern; consider dropping the inline comments on these helpers.

  2. toUserContextConfiguration() silently swallows errors:

    // Current — errors from resolveWebAuth() are silently discarded
    internal func toUserContextConfiguration() -> DatabaseConfiguration? {
      guard let auth = try? resolveWebAuth() else { return nil }
      return try? DatabaseConfiguration.make(database: .public, authentication: auth)
    }

    If a user has CLOUDKIT_API_TOKEN set but CLOUDKIT_WEB_AUTH_TOKEN missing, this returns nil with no indication why. The user-context phases then silently skip with a log message. A misconfigured-but-present token set is easier to debug if it surfaces a warning. Even logging the caught error before returning nil would help.

  3. DemoErrorsRunner error expectation mismatch — After the call-site swap to fetchCaller(), the test expects HTTP 401:

    _ = try await service.fetchCaller()
    print("⚠️  Expected 401 but call succeeded...")

    But fetchCaller()'s own docstring says calling against the private database returns BAD_REQUEST: endpoint not applicable in the database type. The test configures database: .private with bad credentials. Apple may return 421/400 before even evaluating auth, making the "Expected 401" comment misleading. Worth verifying the actual error code during live testing.

  4. unsupportedDatabaseAuthCombination hardcoded string — The error case uses a hardcoded "serverToServer" string rather than one derived from AuthenticationCredentials:

    throw ConfigurationError.unsupportedDatabaseAuthCombination(
      database: database.rawValue,
      authentication: "serverToServer"  // <-- hardcoded
    )

    If a new auth case is added later, the error message won't automatically reflect it. Giving AuthenticationCredentials a description or rawValue would be more maintainable. (Low priority since the switch exhausts cases.)


Potential Bugs / Risks

  1. API-only initializer default changed — The original init(containerIdentifier:apiToken:transport:) had a comment // API-only supports public database and hardcoded .public. After this PR the default is still .public, but callers can now override to .private. The API-token-only path (no webAuthToken) against a private database will fail at runtime. This is a valid combination to allow (per the PR's design goals), but the docstring on that initializer should state the constraint explicitly to prevent confusion.

Security

No concerns. The refactor reduces surface area by eliminating the impossible private/shared + serverToServer combination at construction time rather than relying on server-side rejection.


Test Coverage

  • +12 unit tests across 3 new endpoint suites — good.
  • DatabaseConfigurationMakeTests validates all 6 database × auth combinations (4 valid, 2 rejected) — thorough.
  • ToUserContextConfigurationTests covers the nil-on-missing-creds path.
  • Integration phases (FetchCallerPhase, DiscoverAllUserIdentitiesPhase, LookupUsersByEmailPhase, LookupUsersByRecordNamePhase) follow the phased test pattern.

Gap: There are no unit tests for toUserContextConfiguration() with a partially present web-auth config (e.g., apiToken set but webAuthToken empty). Given the silent-nil behavior noted above, a test documenting that case would be useful.


Summary

Solid PR. The architecture is correct and the implementation is clean. The main actionable items are: (1) the silent error swallow in toUserContextConfiguration(), and (2) verifying the expected error code in DemoErrorsRunner. Everything else is minor or stylistic.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 64.21471% with 180 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.31%. Comparing base (73f13d5) to head (2f66115).

Files with missing lines Patch % Lines
...t/Integration/Phases/LookupUsersByEmailPhase.swift 0.00% 26 Missing ⚠️
...DemoKit/Integration/Tests/PublicDatabaseTest.swift 0.00% 17 Missing ⚠️
...egration/Phases/LookupUsersByRecordNamePhase.swift 0.00% 12 Missing ⚠️
...ources/MistKit/Service/CloudKitError+OpenAPI.swift 40.00% 9 Missing ⚠️
.../MistDemoKit/Commands/TestIntegrationCommand.swift 0.00% 8 Missing ⚠️
...it/Service/CloudKitResponseProcessor+Changes.swift 69.23% 8 Missing ⚠️
Sources/MistKit/Service/CloudKitService.swift 65.21% 8 Missing ⚠️
...rces/MistDemoKit/Commands/TestPrivateCommand.swift 0.00% 7 Missing ⚠️
...istDemoKit/Integration/IntegrationTestRunner.swift 0.00% 7 Missing ⚠️
Sources/MistKit/Service/CloudKitError.swift 22.22% 7 Missing ⚠️
... and 22 more
Additional details and impacted files
@@                Coverage Diff                @@
##           v1.0.0-beta.1     #315      +/-   ##
=================================================
+ Coverage          67.16%   67.31%   +0.15%     
=================================================
  Files                495      503       +8     
  Lines              13869    14062     +193     
=================================================
+ Hits                9315     9466     +151     
- Misses              4554     4596      +42     
Flag Coverage Δ
mistdemo-spm-macos 55.46% <22.24%> (-1.25%) ⬇️
mistdemo-swift-6.2-jammy 55.45% <22.24%> (-1.21%) ⬇️
mistdemo-swift-6.2-noble 55.42% <22.24%> (-1.30%) ⬇️
mistdemo-swift-6.3-jammy 55.47% <22.24%> (-1.25%) ⬇️
mistdemo-swift-6.3-noble 55.49% <22.24%> (-1.21%) ⬇️
spm 51.00% <69.90%> (+2.79%) ⬆️
swift-6.1-jammy 51.54% <73.04%> (+3.36%) ⬆️
swift-6.1-noble 51.63% <74.29%> (+3.45%) ⬆️
swift-6.2-jammy 51.20% <72.06%> (+2.96%) ⬆️
swift-6.2-noble 51.03% <70.79%> (+2.64%) ⬆️
swift-6.3-jammy 51.12% <72.06%> (+2.94%) ⬆️
swift-6.3-noble 51.00% <70.79%> (+2.79%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

test ping

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Code Review: PR #315 — Public + Web-Auth User-Identity Endpoints

Overview

This PR fixes a real gap: CloudKit's user-identity endpoints require public-database routing with web-auth credentials — a combination the old DatabaseCredentials enum could not express. The core fix (splitting DatabaseCredentials into orthogonal AuthenticationCredentials + DatabaseConfiguration) is architecturally sound and the new API surface is clean.


Strengths

Architecture refactor (AuthenticationCredentials + DatabaseConfiguration)
The separation of the database axis from the auth axis is the right model. The DatabaseConfiguration.make(database:authentication:) factory that rejects invalid combinations at construction time is a good use of type-system enforcement. The old DatabaseCredentials was starting to duplicate state (private vs shared had identical auth code), and this cleans that up entirely.

Deprecation shim
@available(*, deprecated, renamed: "fetchCaller", message:) forwarding to fetchCaller() is exactly the right approach — source-compatible with a clear migration path.

Initializer defaults
The new database: + environment: parameters on CloudKitService.init preserve backwards-compatibility (same defaults as the previous hardcoded values) while making the types much more useful for user-context flows.

Test coverage
The 5-file suite pattern (Suite + Helpers + SuccessCases + Validation) is applied consistently to all three new endpoints. DatabaseConfigurationMakeTests is a good addition — it covers all valid and invalid pairings.


Issues

Bug: DemoErrorsRunner demos the wrong error for fetchCaller

The service in DemoErrorsRunner is constructed against the private database. users/caller rejects private-database requests with 400 BAD_REQUEST: endpoint not applicable in the database type — the server evaluates the DB type before checking credentials. So the demo will now surface a 400, not a 401, making the error output misleading. The fix is to use a public-database service with a bad token instead, and update the print statement from "Expected 401" accordingly.

Minor: generated files vs. CLAUDE.md claim
CLAUDE.md says Sources/MistKit/Generated/ code is "not committed", but the PR correctly updates those files (the test plan even verifies generate-openapi.sh produces no diff). CLAUDE.md should be updated to say "auto-generated — regenerate from openapi.yaml, do not manually edit" rather than "not committed".


Nits

PublicDatabaseTest boolean flagincludeUserContextPhases: Bool works but can drift silently if a new user-context phase is added. At minimum, a comment above the if includeUserContextPhases block noting that append order matters (since subsequent phases depend on FetchCallerPhase's output) would help.

discoverAllUserIdentities nil coalescediscoverData.users?.map(...) ?? [] is fine for now. Worth a future note: if CloudKit always returns the key (even empty), a missing key could indicate a parse failure rather than zero users.


Summary

The core logic is correct and well-tested. The DemoErrorsRunner issue is worth fixing before merge since it changes the observable behavior of mistdemo demo-errors (400 instead of 401). Everything else is non-blocking.

Reviewed with Claude Code

…igation

Live verification on 2026-05-08 against iCloud.com.brightdigit.MistDemo
returned HTTP 500 from Apple's GET /users/discover. The first 12 phases
of mistdemo test-integration --verbose run green (the 8 base public+S2S
phases plus FetchCallerPhase, DiscoverUserIdentitiesPhase,
LookupUsersByEmailPhase, LookupUsersByRecordNamePhase) — only
discoverAllUserIdentities fails, blocking phases beyond it.

The endpoint is referenced in CloudKitJS but does not appear in Apple's
CloudKit Web Services REST documentation. The actual REST shape is still
under investigation under #28.

Changes:
- Marked `CloudKitService.discoverAllUserIdentities()`
  `@available(*, unavailable, message: ...)` with a pointer to #28.
- Removed `DiscoverAllUserIdentitiesPhase` from MistDemo and from
  `PublicDatabaseTest.phases`.
- Removed the `CloudKitServiceDiscoverAllUserIdentities` test directory
  (the unavailable method cannot be called from Swift code).

The OpenAPI definition, generated client, path builder, response
processor, Output extension, and Swift wrapper are all retained.
Unblocking is a one-line `@available` removal once the correct REST
shape is determined under #28.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Code Review: PR #315 — Public+web-auth user-identity endpoints

Overview

This PR correctly models CloudKit's orthogonal database/authentication axes and unblocks the family of user-identity endpoints (users/caller, users/discover, users/lookup/*) that require public DB + web-auth credentials. The design is sound and the live-test results are compelling.


Strengths

  • AuthenticationCredentials + DatabaseConfiguration split is the right model. The old DatabaseCredentials enum baked in a 1:1 mapping (public→S2S, private→webAuth) that CloudKit doesn't actually enforce. The new types correctly express the orthogonal axes and validate only the genuinely invalid combinations at construction time.
  • Backward-compatible deprecation shim on fetchCurrentUser() is done cleanly — no behavior change for existing callers.
  • @available(*, unavailable) on discoverAllUserIdentities() is the right choice for a live-verified broken endpoint. Shipping the infrastructure and leaving a one-liner removal path is better than reverting.
  • Test coverage is solid: DatabaseConfiguration.make validation has explicit tests for all four valid combinations and both invalid ones. The new toUserContextConfiguration suite tests both the happy path and the nil case.
  • Initializer extensions — adding environment: and database: defaulted parameters is source-compatible and the right generalization.

Issues and Suggestions

1. Dead try? in toUserContextConfiguration()

DatabaseConfiguration.make(.public, .webAuth(...)) cannot throw — the validation only rejects private/shared + serverToServer. The try? silently swallows any future validation errors if the logic changes. Consider try! with a comment explaining the invariant, or restructure so the return is non-optional via DatabaseConfiguration(database: .public, authentication: auth) directly since the combination is known-valid.

2. Variable name discoverData in lookup operations

discoverData is used as the local variable name in lookupUsersByEmail and lookupUsersByRecordName. Since these aren't "discover" operations at the domain level (they just reuse the DiscoverResponse schema), naming it lookupResult or result would be clearer.

3. database: default in API-token-only initializer

Before this PR the comment read "API-only supports public database". The initializer now accepts .private or .shared without a web-auth token, which CloudKit will reject at runtime. A doc comment noting that without webAuthToken only .public is valid, or a runtime assertion, would prevent callers from silently misconfiguring this.

4. Integration phase ordering in PublicDatabaseTest

User-context phases are appended after CleanupPhase() in the order:

FetchCallerPhase()            // Output = UserInfo
DiscoverUserIdentitiesPhase() // Input = UserInfo, Output = NoState
LookupUsersByEmailPhase()     // Input = UserInfo
LookupUsersByRecordNamePhase()// Input = UserInfo

After DiscoverUserIdentitiesPhase emits NoState, how does the pipeline feed UserInfo to the two lookup phases? If the runner uses a keyed state store rather than strict linear chaining this is fine — but a brief comment explaining the threading mechanism would help future maintainers, since it's non-obvious from the phase signature types alone.

5. openapi.yaml: request body properties lack required

Neither users/lookup/email (emailAddress) nor users/lookup/id (userRecordName) marks the lookup key as required in their request body schemas. Adding required: [emailAddress] / required: [userRecordName] would let the generator validate inputs and surface clearer errors on missing fields.

6. Minor: misleading comment in TestPrivateCommand.swift

The comment says "private-database pipeline already authenticates with web-auth, so the primary service is sufficient", but the real reason for userContextService: nil is that CloudKit rejects user-identity endpoints on the private DB entirely. Worth aligning with the comment in PrivateDatabaseTest.swift which is accurate.


Test Coverage

The unit tests for the new lookup methods follow the project's established pattern well (success/empty/auth-error). One gap: no test for lookupUsersByEmail([]) with an empty input array — it should return [] harmlessly, but a test would make that contract explicit.


Summary

Well-executed PR. The credentials split is the right long-term design, the backward compatibility story is clean, and the live-test evidence is thorough. The items above are mostly polish (naming, a dead try?, one comment) with one design question about the API-token-only initializer's new database: parameter.

🤖 Reviewed with Claude Code

Comment on lines +62 to +65
case .unsupportedDatabaseAuthCombination(let database, let authentication):
"Database '\(database)' does not accept '\(authentication)' authentication. "
+ "CloudKit allows server-to-server only against the public database; "
+ "private and shared databases require web-auth credentials."
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this incorrect now?

apiToken: String,
webAuthToken: String,
environment: Environment = .development,
database: Database = .private,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should database even be part of the CloudKitServer since calls could be for either

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be an optional argument for various calls

containerIdentifier: String,
apiToken: String,
webAuthToken: String
webAuthToken: String,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should allow any set of authentication values (private key, id, API Token, web authentication, etc...)

// OTHER DEALINGS IN THE SOFTWARE.
//

extension Operations.discoverAllUserIdentities.Output: CloudKitResponseType {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be marked unavailable?

… unavailable

Addresses all four review threads on PR #315:

- Comment #1 (error wording): removed `unsupportedDatabaseAuthCombination`
  along with `MistDemo.DatabaseConfiguration`; invalid combos now surface as
  `CloudKitError.missingCredentials` from the library.
- Comment #2 (per-call database): user-identity ops in
  `CloudKitService+UserOperations` hardcode `.public`; record/zone/asset/sync
  ops accept `database: Database? = nil` falling back to a service-level
  default.
- Comment #3 (unified credentials): new `Credentials` /
  `ServerToServerCredentials` / `APICredentials` value types replace the
  legacy `apiToken:`/`webAuthToken:` initializers. The token manager is
  selected based on the target database (S2S for `.public`, web-auth for
  `.private`/`.shared`). Lifted `PrivateKeyMaterial` into the library.
- Comment #4 (cascade unavailable): removed
  `Operations.discoverAllUserIdentities.Output: CloudKitResponseType`
  conformance entirely; `processDiscoverAllUserIdentitiesResponse` is now
  `@available(*, unavailable)` with a `fatalError` body.

Also migrates ~15 MistKit test helpers and the MistDemo factory to the new
Credentials API.

Breaking changes (pre-1.0): removed legacy `CloudKitService` initializers
taking `apiToken:`/`webAuthToken:`; `CloudKitService.apiToken` is removed,
`.database` is now `internal`.

Out of scope: per-call `TokenManager` dispatch (would let one service mix
S2S-for-public and web-auth-for-user-context). MistDemo still constructs a
separate `userContextService` for that scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

Code Review — PR #315: Public+web-auth user-identity endpoints

Overview

This is a well-structured PR that addresses a real architectural gap: CloudKit's user-identity endpoints require a public database + web-auth credential combination that MistKit couldn't previously express. The new Credentials type, fetchCaller() rename, and the MistDemo AuthenticationCredentials/DatabaseConfiguration split are all correct design moves. Live integration tests passing (12/12) is strong evidence the wiring is right.


Issues

🔴 Breaking API changes (needs acknowledgement / migration note)

Two previously public properties on CloudKitService are removed or made internal:

  • public let apiToken: Stringremoved entirely
  • public let database: Databasemade internal

Any consumer that reads service.apiToken or service.database after this PR will fail to compile. Given this is pre-1.0 these changes are acceptable, but they should be called out explicitly in the PR description and/or changelog so downstream adopters (including MistDemo users) know what to update. The deprecation shim on fetchCurrentUser() is a great model — consider the same for the removed properties if any user might be reading them.

🟡 openapi.yaml: structural ambiguity on discoverAllUserIdentities

The new get: block for discoverAllUserIdentities is inserted immediately after the closing responses of the /users/discover POST block — inside the same path item. That means the generated client sees GET /users/discover, which is intentional. However the diff reads ambiguously because the indentation level of the new get: matches the existing POST's child keys rather than the path-item level. If generate-openapi.sh produces no diff this is fine in practice, but a structural comment (# GET /users/discover — see #28) would reduce future confusion for someone reading the raw YAML.

🟡 fetchCaller() / lookupUsers*() hardcode .public database in the path but use self.database nowhere

// CloudKitService+UserOperations.swift
path: createGetCallerPath(
    containerIdentifier: containerIdentifier,
    database: .public          // always .public, ignores self.database
)

This is correct by design (CloudKit rejects these endpoints against any other database), but it silently bypasses the caller's configured database. A caller who constructs a CloudKitService(database: .private) and calls fetchCaller() will get an unexpected response from the public database, with no compile- or run-time indication. Consider throwing CloudKitError.missingCredentials (or a new CloudKitError.invalidDatabaseForOperation) early if self.database != .public so the misconfiguration surfaces immediately.

🟡 Credentials.init uses precondition — crash on programming error

precondition(
    serverToServer != nil || apiAuth != nil,
    "Credentials must include at least one of serverToServer or apiAuth"
)

precondition crashes in release builds for this class of invalid input. Failable init (init?) or throwing init would let callers handle the error gracefully without a trap. If the intent is "this is a programmer error and should never happen," document that clearly with a comment; if there's any chance it comes from dynamic config (JSON deserialization), a throwing init is safer.

🟡 Test gap: makeTokenManager routing paths not unit-tested

CloudKitService.makeTokenManager(credentials:database:) now encodes the credential-selection logic that determines which token manager is used for each database type. This is important enough that it warrants direct unit tests:

  • .public + S2S credentials → ServerToServerAuthManager
  • .public + apiAuth with webAuthTokenWebAuthTokenManager
  • .public + apiAuth without webAuthTokenAPITokenManager
  • .public + neither → throws .missingCredentials
  • .private + apiAuth with webAuthTokenWebAuthTokenManager
  • .private + S2S only → throws .missingCredentials
  • .shared + same as .private

These are currently only covered indirectly through service-level tests.

🟡 Test gap: fetchCaller() has no dedicated unit test suite

lookupUsersByEmail and lookupUsersByRecordName each have SuccessCases, Validation, and Helpers test files. fetchCaller() has no equivalent — the integration test covers the live path but a unit test with a mock transport would catch regressions faster.


Minor / Nits

internal import cleanup — switching bare import Foundation / import OpenAPIRuntime to internal import is a good Swift 5.10+ practice for reducing ABI exposure. Consistent and correct.

Path builder signature change — adding database: Database to every createXxxPath function is the right call: it removes the hidden dependency on self.database and makes each builder's scope explicit. Slightly verbose but much clearer.

discoverAllUserIdentities inline investigation notes — the @available(*, unavailable, message:) doc-comment contains a detailed investigation log (dates, HTTP status codes, live test findings). This context belongs in issue #28, not permanently in the source tree. Shorten it to: "Not yet ready: GET /users/discover returns HTTP 500 in live testing. See #28." and keep the detailed findings in the issue.

@available(macOS 11.0, ...) guard pattern in tests — every test body duplicates:

guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else {
    Issue.record("CloudKitService is not available on this operating system.")
    return
}

Consider a helper or wrapping the suite's condition: in .enabled(if: Platform.isCryptoAvailable) (as LookupUsersByRecordName already does at the suite level) instead of per-test guards.


Summary

The core architecture is sound and the live integration results are convincing. The main asks before merging are:

  1. Acknowledge the two breaking API removals (apiToken, database) with a migration note
  2. Add unit tests for makeTokenManager routing paths
  3. Add a fetchCaller unit test suite parallel to the lookup suites
  4. Consider a more defensive stance (early error) when fetchCaller/lookupUsers* are called on a non-public database service

🤖 Generated with Claude Code

Comment thread Sources/MistKit/Authentication/Credentials.swift
Comment thread Sources/MistKit/Service/CloudKitService+Initialization.swift Outdated
Comment thread Sources/MistKit/Service/CloudKitService.swift Outdated
…ip ci]

Resolves the architectural feedback in the PR-315 review:

* CloudKitService no longer carries `database` — operations take
  `database:` per call (defaulting to `.public`); user-identity routes
  drop the parameter since CloudKit pins them to `.public`. Subsumes
  Claude's "fetchCaller bypasses self.database" finding.
* Credentials.makeTokenManager(for:requiresUserContext:) resolves the
  appropriate token manager at dispatch time. A single service can now
  serve public-database S2S record ops and user-identity web-auth
  routes from one fully-populated `Credentials`. MistKitClient.swift is
  obsolete and removed; per-call dispatch lives in
  CloudKitService+ClientDispatch.
* Credentials.swift split per SwiftLint one_file_per_declaration into
  ServerToServerCredentials.swift + APICredentials.swift +
  Credentials.swift. New typed CredentialsValidationError; init asserts
  in debug, throws in release (no more precondition crash for dynamic
  config).
* MistDemo: userContextService workaround collapsed — single service
  handles all phases via per-call resolution.
* CI hotfix: 11 unused `public import` lines demoted to `internal`
  (the warnings-as-errors regression flagged in the review).
* Tests: 12-case routing-matrix unit suite for makeTokenManager and a
  fetchCaller suite parallel to LookupUsers* (success + validation).
  Obsolete MistKitClient tests removed.
* Polish: shorter @available message on discoverAllUserIdentities,
  structural comment for GET /users/discover in openapi.yaml,
  ConfigurationError.missingAPIToken (unused) removed.

475/475 tests pass. Library + MistDemo build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread Sources/MistKit/Service/CloudKitService+ZoneOperations.swift Outdated
leogdion added a commit that referenced this pull request May 9, 2026
Per review on PR #315: listZones, lookupZones, fetchZoneChanges now
default to .private since the public database only contains
_defaultZone, making .public a degenerate default. MistDemo callers
pass context.database / config.base.database explicitly so the
--database flag still drives the test runs.

Also repairs MistDemo test breakage from 7debe8d ([skip ci]):
toUserContextCredentials() was removed but tests still referenced it;
rewritten against the replacement surface (toPrimaryCredentials embeds
apiAuth on .public, plus the new hasUserContextCredentials boolean).
The CredentialsValidationTests suite was deleted — it asserted
init-time validation that no longer exists under per-call credential
resolution; the equivalent .missingCredentials behavior is covered in
MistKitTests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
default to .private since the public database only contains
_defaultZone, making .public a degenerate default. MistDemo callers
pass context.database / config.base.database explicitly so the
--database flag still drives the test runs.

Also repairs MistDemo test breakage from 7debe8d:
toUserContextCredentials() was removed but tests still referenced it;
rewritten against the replacement surface (toPrimaryCredentials embeds
apiAuth on .public, plus the new hasUserContextCredentials boolean).
The CredentialsValidationTests suite was deleted — it asserted
init-time validation that no longer exists under per-call credential
resolution; the equivalent .missingCredentials behavior is covered in
MistKitTests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@leogdion leogdion force-pushed the 312-public-database-web-authuser-identity branch from a9bdb37 to 22a1760 Compare May 9, 2026 17:29
@claude
Copy link
Copy Markdown

claude Bot commented May 9, 2026

Code Review — PR #315: public+web-auth user-identity endpoints

Overview

This is a well-scoped PR that unblocks the family of user-identity issues (#27, #34, #35, #310, #311) by addressing a real CloudKit API constraint: user-identity endpoints only work on the public database with web-auth credentials. The architectural response — moving from a fixed database on CloudKitService to per-call dispatch via a new client(for:requiresUserContext:) method — is the right design. The deprecation of fetchCurrentUser() is handled cleanly, and deferred #28 is documented honestly.


Strengths

  • Per-call client dispatch (CloudKitService+ClientDispatch.swift): resolving the token manager per-operation rather than once at construction is elegant and correctly models CloudKit's asymmetric routing rules. The three-branch logic (fixed manager → credentials → throw) is clear and exhaustive.
  • Deprecation pattern: fetchCurrentUser() forwards to fetchCaller() with @available(*, deprecated, renamed:) — exactly the right approach for source compatibility.
  • discoverAllUserIdentities() strategy: keeping the implementation behind @available(*, unavailable) is a reasonable way to preserve the work while being honest about the API state. The pointer to File Apple Feedback Assistant report: discoverAllUserIdentities returns HTTP 500 #28 in the message is helpful.
  • Documentation: docstrings throughout consistently explain why routing is constrained (e.g., "CloudKit rejects users/caller against the private database"), not just what.
  • Module import visibility: converting bare import Foundation / OpenAPIRuntime to internal import is a good hygiene win — prevents those types leaking into the public interface.
  • Credentials validation: the assert-in-debug + typed-throw-in-release pattern on the Credentials initializer is the right dual-mode approach.
  • Test count: +8 new tests, all following the established @Suite / @Test / #expect conventions.

Issues / Suggestions

Medium — client(for:requiresUserContext:) invariant not asserted at construction

CloudKitService+ClientDispatch.swift has a third branch:

} else {
    throw CloudKitError.missingCredentials(
        database: database,
        reason: "service has neither credentials nor a fixed token manager"
    )
}

By reading the initializers, this state should be unreachable (every init sets one or the other), but there is no assert or precondition enforcing it at construction time. If a future initializer path omits both, callers get a runtime throw from an unexpected place. Consider adding an assert(credentials != nil || fixedTokenManager != nil) in the base initializer to surface this early in debug builds.

Medium — Validation test coverage for new lookup methods is thin

LookupUsersByEmail and LookupUsersByRecordName each have exactly one validation test (auth error). The FetchCaller suite has two (auth error + missing web-auth token check). The lookup methods are missing:

  • A test that missingCredentials is thrown when webAuthToken is absent (parallel to FetchCaller's throwsWhenWebAuthMissing)
  • An empty-array input scenario (does the service short-circuit before a network call, or pass an empty body to CloudKit?)

These would round out coverage without much effort.

Minor — discoverAllUserIdentities() has an unreachable implementation body

The @available(*, unavailable) annotation means the Swift compiler will never call this function, but the body is ~25 lines of live code. It builds and compiles fine, but future editors may not realize it is intentionally preserved. A brief inline note (or a // Implementation intact for easy re-enablement — see #28) would prevent confusion.

Minor — apiToken: "" in MistKitConfiguration+ConvenienceInitializers.serverToServer()

The serverToServer factory sets apiToken: "" in MistKitConfiguration. createTokenManager() should pick the S2S path first and never reach the APITokenManager branch with the empty string. But this is fragile: if the priority chain in createTokenManager() is ever reordered, an empty string would silently produce a broken API token manager. Consider a sentinel like apiToken: nil if the type allows, or document the invariant with a comment adjacent to the assignment.


Correctness Check

  • The users/currentusers/caller rename in openapi.yaml and generated Client.swift / Types.swift aligns with Apple deprecating the former endpoint.
  • FetchCurrentUserPhase removed from the private integration pipeline is correct — CloudKit rejects users/caller against the private database.
  • PublicDatabaseTest properly guards with a precondition that database == .public before running user-identity phases.
  • LookupUsersByEmailPhase and LookupUsersByRecordNamePhase gracefully skip when the caller's email/record-name is unavailable — good defensive design.
  • PrivateKeyMaterial.swift moving from MistDemo to the main MistKit package is a good consolidation.

Summary

The core architecture is sound, well-tested, and well-documented. The two medium items (construction-time invariant assertion, expanded validation tests for lookup methods) would make this more robust but are not blockers. The minor items are polish. Overall this is merge-ready after consideration of the above.

🤖 Generated with Claude Code

…iesResponse to Swift 6.2+

Swift 6.1 rejects calls to an unavailable function from within another
unavailable function; 6.2 relaxed that rule. The internal helper
processDiscoverAllUserIdentitiesResponse is unavailable in lockstep with
its only caller — the also-unavailable CloudKitService.discoverAllUserIdentities() —
which built fine on 6.2+ but failed on Swift 6.1 with:

    error: 'processDiscoverAllUserIdentitiesResponse' is unavailable:
           Pending #28: discoverAllUserIdentities is not yet ready.

Wrap just the attribute in `#if swift(>=6.2)` so the body is shared and
6.1 compiles. Inline doc records the intent and the one-line cleanup
(delete the #if/#endif) once 6.1 is dropped from the matrix.

A `swiftlint:disable:next unavailable_function` is required because
swiftlint does not evaluate #if and otherwise sees a fatalError-only
function without the attribute.

Verified: swift build + swift test pass on Swift 6.1.3 (Linux container)
and on macOS Swift 6.2+ (475/475 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 9, 2026

Code Review — PR #315: public+web-auth user-identity endpoints

Verified against CLAUDE.md conventions and the CloudKit Web Services reference. This is a substantial, well-scoped PR. Notes below are roughly ordered by severity.


Architecture / Design

Credentials + per-call dispatch — solid approach.
Credentials cleanly separates the two orthogonal auth concerns; CloudKitService+ClientDispatch.swift's per-call Client construction is the right way to select the correct token manager without forcing callers to hold multiple services. The assert (debug) + typed throws(CredentialsValidationError) (release) pattern in Credentials.init is good layered validation.

PrivateKeyMaterial promotion to MistKit core — good call.
Keeping key-material handling in the library rather than the demo makes ServerToServerCredentials a first-class SDK type. The only behavioral change worth flagging is below.


Issues

1. PrivateKeyMaterial.loadPEM() no longer wraps file I/O errors (behavior change)

Before:

// file(path:) errors were caught and re-thrown as ConfigurationError.missingRequired with a
// helpful hint pointing at the path and the underlying error message.

After:

return try String(contentsOfFile: path, encoding: .utf8)
// Callers now receive raw Foundation NSError (e.g. POSIX file-not-found)

The ConfigurationError type is MistDemo-internal so the old wrapping couldn't move to the library unchanged — but the raw NSError surface is unfriendly to callers who need to display useful diagnostics. A light PrivateKeyMaterialError in the library, or at minimum a throw-site re-wrap in MistKitClientFactory, would recover that UX. This is a new footgun for library consumers.

2. Removed public properties break source compatibility

CloudKitService previously exposed:

  • public let apiToken: String
  • public let database: Database

Both are gone without a deprecation shim. Any caller who read service.database or service.apiToken will get a compile error on upgrade. The PR description doesn't call this out explicitly. At minimum, a @available(*, deprecated, message:) computed property bridging the old names would keep the semver story cleaner heading into beta.

3. processDiscoverAllUserIdentitiesResponsefatalError in reachable code

// swiftlint:disable:next unavailable_function
internal func processDiscoverAllUserIdentitiesResponse(...) async throws(CloudKitError) -> ... {
    fatalError("discoverAllUserIdentities is not yet ready (pending #28)")
}

The @available(*, unavailable) on the caller prevents normal call sites from reaching this, and the Swift 6.2 caveat is correctly documented. However, the function itself is not marked unavailable — it's callable from tests, from @testable imports, or via withUnsafeContinuation tricks. A safer pattern is to also mark the function @available(*, unavailable, message: "...") unconditionally (this works in Swift 6.1 because the caller is in the same module), or to have the body throw CloudKitError.unsupportedOperationType(...) rather than a fatalError so a rogue caller gets a recoverable error rather than a crash.

4. supportsUserContextPhases: true hardcoded in TestPrivateCommand

let runner = IntegrationTestRunner(
  service: service,
  supportsUserContextPhases: true,  // ← always true
  ...
)

The comment explains the rationale (private → web-auth → user-context), which is correct. But if credentials are somehow incomplete (missing webAuthToken) the runner will schedule user-identity phases and they will fail at runtime rather than being gracefully skipped. Consider deriving the flag from config.base.hasUserContextCredentials for consistency with TestIntegrationCommand, even if it always evaluates to true for the private-database path in practice.

5. CLAUDE.md lists discoverAllUserIdentities() as available

The table entry in CLAUDE.md now lists discoverAllUserIdentities() as part of the public API without noting it is @available(*, unavailable). This will mislead future contributors (and AI tooling). Suggest adding an (unavailable — see #28) annotation in the table cell.


Minor / Style

  • resolveAPICredentials() silent swallow in toPrimaryCredentials: The let webAuth = try? resolveAPICredentials() is intentional for the optional augmentation case and is documented, but a missing CLOUDKIT_API_TOKEN on a public-only setup will now be silently ignored rather than surfaced (the error only appears if someone exercises a user-context route). Acceptable given the design intent, but worth a log line so operators know they're running without user-context capability.

  • internal import Foundation sweep — correct and consistent with the library's access-level hygiene. Good cleanup.

  • @available(*, deprecated, renamed: "fetchCaller") on fetchCurrentUser() — correct pattern, correct placement.

  • discoverUserIdentities(lookupInfos:) in CloudKitService+UserOperations.swift — the diff shows only a trailing comma / formatting change. Worth confirming the function body also picks up self.client(for: .public, requiresUserContext: true) the same way the new functions do (the truncated diff makes it hard to verify).

  • lookupZones(zoneIDs:database:) default .public — the LookupZonesCommand now passes database: config.base.database explicitly, which is correct. The internal change is source-compatible for callers that relied on the old fixed database.


Tests

The PR adds 8 new unit tests and confirms 469 pass. It would be valuable to add unit tests for:

  • Credentials.makeTokenManager(for:requiresUserContext:) covering all 6 meaningful paths (requiresUserContext × database variants, missing credentials → error)
  • Credentials(serverToServer: nil, apiAuth: nil)CredentialsValidationError.empty

The AuthenticationCredentialsTests rename suggests existing tests were adapted. If the new dispatch logic in Credentials+TokenManager.swift isn't covered by unit tests, a logic error there would only surface in live integration tests.


Summary

Approve with minor requests. The core design is sound and well-tested via live integration. The two items that should not slip through to a beta release:

  1. The raw file I/O error surface from PrivateKeyMaterial.loadPEM() (or a re-wrap at the factory call site)
  2. The missing deprecation shims for apiToken and database if this is a semver-public surface

The fatalError in the unavailable response processor and the CLAUDE.md annotation are lower-priority but easy to fix now.

🤖 Generated with Claude Code

leogdion and others added 4 commits May 9, 2026 14:48
…ng (type/status only)

CodeQL's swift/cleartext-logging flagged the existing warning logs
because lookupUsersByEmail(_:) propagates email-PII taint through the
response object. Move full \(response) interpolation to .debug so the
detail stays available for development without flowing into ops logs;
keep .warning at type(of:) + HTTP status code only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…okup/email

LookupUsersByEmailPhase previously skipped whenever fetchCaller() didn't
return an email (which is the common case). Plumb a configurable lookup
email through TestIntegrationConfig / TestPrivateConfig → PhaseContext so
the phase can be driven against a known-discoverable iCloud account.
Falls back to caller email, then to a clearer skip message naming the
flag/env var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
swift-format / swiftlint / periphery are pinned in mise.toml; the
previous "requires swiftlint installation" wording led to PATH lookups
that fail in this repo. Replace with `mise exec --` invocations and
flag the full ./Scripts/lint.sh pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ailable response, supportsUserContextPhases derivation

- CloudKitError: add invalidPrivateKey(path:underlying:) so PEM-load failures
  carry the file path + original error instead of bare Foundation NSError.
  Wrap loadPEM() at the single call site in Credentials+TokenManager. Add
  PrivateKeyMaterial.filePath accessor for the diagnostic.

- processDiscoverAllUserIdentitiesResponse: replace fatalError with
  throw CloudKitError.unsupportedOperationType so a stray Swift 6.1 caller
  (where the @available cascade does not apply) gets a recoverable error
  instead of a crash.

- TestPrivateCommand: derive supportsUserContextPhases from
  config.base.hasUserContextCredentials, mirroring TestIntegrationCommand,
  so user-identity phases skip cleanly when web-auth env vars are absent.

- toPrimaryCredentials: replace try? with do/catch + stderr INFO line so
  operators see when web-auth is missing on a .public setup.

- CLAUDE.md: annotate discoverAllUserIdentities() as unavailable pending #28.

- CredentialsTokenManagerTests: fill the missing routing-matrix branches
  (user-context × .private/.shared, .shared + token-only) and cover the new
  .invalidPrivateKey path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 9, 2026

Code Review — PR #315: public+web-auth user-identity endpoints

Overview

This PR delivers a clean, well-thought-out solution to a genuinely tricky CloudKit constraint: user-identity endpoints live on the public database but require web-auth credentials, which the existing CloudKitService couldn't express. The core change — replacing a single flat credential set with an orthogonal Credentials type that holds serverToServer? and apiAuth? and resolves the right token manager per call — is a solid architectural improvement.

Live-tested with 12/12 integration phases passing and 469 unit tests green.


Strengths

  • Orthogonal credential types (APICredentials, ServerToServerCredentials, Credentials) clearly model the distinct auth modes and the "both at once" case.
  • Per-call token resolution in CloudKitService+ClientDispatch.swift correctly dispatches based on (database, requiresUserContext) — the routing matrix in Credentials+TokenManager.swift is explicit and easy to audit.
  • CredentialsTokenManagerTests covers every cell of the routing matrix with clear test names. The S2S-prefers-over-webAuth and user-context-ignores-S2S cases are both tested explicitly.
  • API lifecycle management via @available(*, deprecated, renamed:) on fetchCurrentUser() and @available(*, unavailable) on discoverAllUserIdentities() is the right approach.
  • internal import cleanup throughout the auth and service layers is good hygiene for a library package.
  • Breaking MistKitClient intermediary class removal simplifies the initialization path significantly — the old MistKitClient.init(container:environment:database:tokenManager:transport:) with its introspection-based credential extraction was fragile.

Issues

1. Per-call Client construction — performance concern

Every operation now calls self.client(for:requiresUserContext:) which allocates a new Client(serverURL:transport:middlewares:) on every request:

// CloudKitService+ClientDispatch.swift
return Client(
    serverURL: URL.MistKit.cloudKitAPI,
    transport: transport,
    middlewares: [
        AuthenticationMiddleware(tokenManager: tokenManager),
        LoggingMiddleware(),
    ]
)

The old design reused a single Client across the lifetime of the service. For low-frequency CLI or demo usage this is fine, but in server-side / batch scenarios (e.g. fetchAllRecordChanges iterating hundreds of pages) this allocates a middleware array, an AuthenticationMiddleware, and a Client struct on every page turn.

Suggestion: Lazily cache the two common Client instances — one for S2S (no user context) and one for web-auth (user context). A Dictionary<(Database, Bool), Client> stored on CloudKitService won't work for Sendable, but two nonisolated(unsafe) lazy var or a small struct ClientCache: Sendable with a nonisolated(unsafe) var would work. Alternatively, document this as a known trade-off and leave it as a future optimisation — just worth flagging.


2. discoverUserIdentities is now a silent breaking change for S2S-credentialed callers

Before this PR discoverUserIdentities(lookupInfos:) used whatever client the service was constructed with. After this PR it calls self.client(for: .public, requiresUserContext: true), which throws CloudKitError.missingCredentials if the service has no apiAuth.webAuthToken.

Any S2S-credentialed service that previously called discoverUserIdentities (and got a CloudKit-level rejection) will now get a Swift-level missingCredentials error instead. The behaviour change is correct — CloudKit does require web-auth here — but it's a silent breaking change for anyone upgrading. A note in the deprecation/migration section of CLAUDE.md or a comment on the function would help.


3. OpenAPI users/lookup/email and users/lookup/id request schemas missing required

# openapi.yaml — lookupUsersByEmail requestBody
schema:
  type: object
  properties:
    users:
      type: array
      ...

Neither schema declares required: [users]. The generated types consequently make users optional (usersPayload?), which means an empty lookupUsersByEmail(.init(users: nil)) is representable and will be sent to CloudKit with a body of {}. CloudKit will likely reject this, but the error will come back as a CloudKit-level bad request rather than a local validation failure.

Suggestion: Add required: [users] to both request schemas, or at minimum validate non-empty input in the Swift wrapper (throw CloudKitError.invalidRequest for an empty emails/recordNames array before dispatching).


4. discoverAllUserIdentities function body contains live implementation behind unavailable

The function body calls client.discoverAllUserIdentities(...) against the live CloudKit endpoint. Since the method is @available(*, unavailable), no call site can reach it at compile time. But the body exists, compiles, and runs if the annotation is removed. This is intentional and the right approach for "one-line unblock" — just noting it so reviewers are aware the body isn't dead code by mistake.

One minor concern: if the @available annotation is ever accidentally changed to deprecated instead of unavailable, the live HTTP 500 would resurface without a guard. A // MARK: - #28: remove @available unavailable to unblock comment on the annotation itself would make the intent explicit.


5. FileHandle.standardError.write in toPrimaryCredentials() bypasses structured logging

// MistDemoConfig+DatabaseConfiguration.swift
FileHandle.standardError.write(Data(line.utf8))

This is a demo-only file and FileHandle.standardError is acceptable in a CLI context. Minor style note: the project uses MistKitLogger / swift-log elsewhere; if MistDemoKit has a logger available, using it would keep log output consistent and controllable. If not, fputs(line, stderr) is slightly less verbose. Not a blocker.


6. Test coverage gap: MistKitClientTests+ServerToServer validation deleted without equivalent

MistKitClientTests+Initialization.swift contained a test serverToServerWithPrivateDatabase that asserted S2S + private DB throws TokenManagerError.invalidCredentials. That test is deleted. The new equivalent is CredentialsTokenManagerTests.privateRejectsServerToServerOnly — which is correctly written — but it tests through Credentials.makeTokenManager (unit level), not through the full CloudKitService.init → operation dispatch path.

There is no test that constructs a CloudKitService with S2S-only credentials, calls an operation requiring a private DB, and verifies CloudKitError.missingCredentials is thrown end-to-end. This is a minor gap rather than a blocker, especially since the unit coverage is thorough.


Minor nits

  • CloudKitService.init(containerIdentifier:credentials:environment:transport:) no longer throws — good simplification. The Credentials.init throws(CredentialsValidationError) typed-throw is a clean Swift 6 use.
  • hasUserContextCredentials using try? resolveAPICredentials() silently swallows resolution errors; the silent false is acceptable for a guard property, but consider a comment explaining that errors mean "no web-auth present."
  • CLAUDE.md table for CloudKitService+UserOperations.swift is updated correctly.

Summary

Approve with suggested follow-up:

The architecture is solid and the live-test evidence is convincing. The per-call Client construction (#1) and missing required in the request schemas (#3) are the most actionable items. Everything else is minor polish or future work. The discoverUserIdentities behavioral change (#2) should at minimum be called out in release notes.

🤖 Generated with Claude Code

Comment thread Sources/MistKit/Service/CloudKitError+OpenAPI.swift Dismissed
Comment thread Sources/MistKit/Service/CloudKitError+OpenAPI.swift Dismissed
Comment thread Sources/MistKit/Service/CloudKitError+OpenAPI.swift Dismissed
@leogdion leogdion merged commit a97d233 into v1.0.0-beta.1 May 9, 2026
74 of 76 checks passed
@leogdion leogdion deleted the 312-public-database-web-authuser-identity branch May 10, 2026 15:29
leogdion added a commit that referenced this pull request May 11, 2026
#28, #34, #35) (#315)

* #312 library: add public+web-auth user-identity endpoints and users/caller migration

Implements the library side of #312 — adding/renaming user-identity endpoints
that require public-database routing with web-auth (user-context) credentials,
and unblocking the convenience initializers from their hardcoded database/
environment defaults.

#310: `CloudKitService` convenience initializers now accept `database:` and
`environment:` parameters with defaults that preserve current behavior.

#311: `users/current` → `users/caller`. Renamed in openapi.yaml and the
generated client; added a hand-written `fetchCaller()` plus an
`@available(*, deprecated, renamed: "fetchCaller")` `fetchCurrentUser()`
shim that forwards to the new method.

#28: GET `/users/discover` (`discoverAllUserIdentities`).

#34: POST `/users/lookup/email` (`lookupUsersByEmail`).

#35: POST `/users/lookup/id` (`lookupUsersByRecordName`).

The three new endpoints reuse `DiscoverResponse` for parsing — Apple returns
`{ users: [UserIdentity] }` for all of them. Each ships with a 5-file
test suite mirroring the existing `DiscoverUserIdentities` pattern.

#33 (`users/lookup/contacts`) intentionally not implemented: Apple has marked
the endpoint deprecated. To be closed as not-planned with a pointer to #34/#35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #312 MistDemo: separate database from authentication and add user-context phases

Refactors MistDemo's CloudKit configuration model and integration runner to
support the public+web-auth combination required by the user-identity
endpoints landed in the prior commit.

**Configuration refactor.** Replaces the `DatabaseCredentials` enum (which
coupled database choice to a single auth method per case, baking in a
public⇒S2S/private⇒webAuth assumption) with two orthogonal types:

  - `AuthenticationCredentials` — `serverToServer(keyID:privateKey:)` /
    `webAuth(apiToken:webAuthToken:)`
  - `DatabaseConfiguration` — pairs a `MistKit.Database` with an
    `AuthenticationCredentials`. The `make(database:authentication:)` factory
    rejects private+S2S and shared+S2S (which CloudKit rejects) so invalid
    combinations remain unrepresentable, while public+webAuth is now a valid
    construction.

`MistKitClientFactory.create(for:)` consumes `toPrimaryConfiguration()`;
the new `createUserContext(for:)` returns the optional public+web-auth
service from `toUserContextConfiguration()` when web-auth tokens are
configured.

**Phase plumbing.** `PhaseContext` and `IntegrationTestRunner` now thread an
optional `userContextService: CloudKitService?`. `PublicDatabaseTest` takes
`includeUserContextPhases:` and conditionally appends the new user-identity
phases:

  - `FetchCallerPhase` (renamed from `FetchCurrentUserPhase`)
  - `DiscoverUserIdentitiesPhase` (existed; updated to use userContextService)
  - `DiscoverAllUserIdentitiesPhase` (#28)
  - `LookupUsersByEmailPhase` (#34)
  - `LookupUsersByRecordNamePhase` (#35)

`PrivateDatabaseTest` no longer includes `FetchCurrentUserPhase`: CloudKit
rejects `users/caller` against the private database, matching the rest of
the user-identity family.

**Call-site updates.** `CurrentUserCommand` and `DemoErrorsRunner` swap
`fetchCurrentUser()` → `fetchCaller()`. `TestIntegrationCommand` and
`TestPrivateCommand` now build and pass `userContextService`.

Tests for `AuthenticationCredentials`, `DatabaseConfiguration.make`
validation, and `MistDemoConfig.toPrimaryConfiguration` /
`toUserContextConfiguration` ship alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #312: mark discoverAllUserIdentities() unavailable pending #28 investigation

Live verification on 2026-05-08 against iCloud.com.brightdigit.MistDemo
returned HTTP 500 from Apple's GET /users/discover. The first 12 phases
of mistdemo test-integration --verbose run green (the 8 base public+S2S
phases plus FetchCallerPhase, DiscoverUserIdentitiesPhase,
LookupUsersByEmailPhase, LookupUsersByRecordNamePhase) — only
discoverAllUserIdentities fails, blocking phases beyond it.

The endpoint is referenced in CloudKitJS but does not appear in Apple's
CloudKit Web Services REST documentation. The actual REST shape is still
under investigation under #28.

Changes:
- Marked `CloudKitService.discoverAllUserIdentities()`
  `@available(*, unavailable, message: ...)` with a pointer to #28.
- Removed `DiscoverAllUserIdentitiesPhase` from MistDemo and from
  `PublicDatabaseTest.phases`.
- Removed the `CloudKitServiceDiscoverAllUserIdentities` test directory
  (the unavailable method cannot be called from Swift code).

The OpenAPI definition, generated client, path builder, response
processor, Output extension, and Swift wrapper are all retained.
Unblocking is a one-line `@available` removal once the correct REST
shape is determined under #28.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #315: resolve PR review — Credentials API, per-call database, cascade unavailable

Addresses all four review threads on PR #315:

- Comment #1 (error wording): removed `unsupportedDatabaseAuthCombination`
  along with `MistDemo.DatabaseConfiguration`; invalid combos now surface as
  `CloudKitError.missingCredentials` from the library.
- Comment #2 (per-call database): user-identity ops in
  `CloudKitService+UserOperations` hardcode `.public`; record/zone/asset/sync
  ops accept `database: Database? = nil` falling back to a service-level
  default.
- Comment #3 (unified credentials): new `Credentials` /
  `ServerToServerCredentials` / `APICredentials` value types replace the
  legacy `apiToken:`/`webAuthToken:` initializers. The token manager is
  selected based on the target database (S2S for `.public`, web-auth for
  `.private`/`.shared`). Lifted `PrivateKeyMaterial` into the library.
- Comment #4 (cascade unavailable): removed
  `Operations.discoverAllUserIdentities.Output: CloudKitResponseType`
  conformance entirely; `processDiscoverAllUserIdentitiesResponse` is now
  `@available(*, unavailable)` with a `fatalError` body.

Also migrates ~15 MistKit test helpers and the MistDemo factory to the new
Credentials API.

Breaking changes (pre-1.0): removed legacy `CloudKitService` initializers
taking `apiToken:`/`webAuthToken:`; `CloudKitService.apiToken` is removed,
`.database` is now `internal`.

Out of scope: per-call `TokenManager` dispatch (would let one service mix
S2S-for-public and web-auth-for-user-context). MistDemo still constructs a
separate `userContextService` for that scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #315: drop service-level database, per-call credential resolution [skip ci]

Resolves the architectural feedback in the PR-315 review:

* CloudKitService no longer carries `database` — operations take
  `database:` per call (defaulting to `.public`); user-identity routes
  drop the parameter since CloudKit pins them to `.public`. Subsumes
  Claude's "fetchCaller bypasses self.database" finding.
* Credentials.makeTokenManager(for:requiresUserContext:) resolves the
  appropriate token manager at dispatch time. A single service can now
  serve public-database S2S record ops and user-identity web-auth
  routes from one fully-populated `Credentials`. MistKitClient.swift is
  obsolete and removed; per-call dispatch lives in
  CloudKitService+ClientDispatch.
* Credentials.swift split per SwiftLint one_file_per_declaration into
  ServerToServerCredentials.swift + APICredentials.swift +
  Credentials.swift. New typed CredentialsValidationError; init asserts
  in debug, throws in release (no more precondition crash for dynamic
  config).
* MistDemo: userContextService workaround collapsed — single service
  handles all phases via per-call resolution.
* CI hotfix: 11 unused `public import` lines demoted to `internal`
  (the warnings-as-errors regression flagged in the review).
* Tests: 12-case routing-matrix unit suite for makeTokenManager and a
  fetchCaller suite parallel to LookupUsers* (success + validation).
  Obsolete MistKitClient tests removed.
* Polish: shorter @available message on discoverAllUserIdentities,
  structural comment for GET /users/discover in openapi.yaml,
  ConfigurationError.missingAPIToken (unused) removed.

475/475 tests pass. Library + MistDemo build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Per review on PR #315: listZones, lookupZones, fetchZoneChanges now
default to .private since the public database only contains
_defaultZone, making .public a degenerate default. MistDemo callers
pass context.database / config.base.database explicitly so the
--database flag still drives the test runs.

Also repairs MistDemo test breakage from 7debe8d:
toUserContextCredentials() was removed but tests still referenced it;
rewritten against the replacement surface (toPrimaryCredentials embeds
apiAuth on .public, plus the new hasUserContextCredentials boolean).
The CredentialsValidationTests suite was deleted — it asserted
init-time validation that no longer exists under per-call credential
resolution; the equivalent .missingCredentials behavior is covered in
MistKitTests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #312: gate @available(*,unavailable) on processDiscoverAllUserIdentitiesResponse to Swift 6.2+

Swift 6.1 rejects calls to an unavailable function from within another
unavailable function; 6.2 relaxed that rule. The internal helper
processDiscoverAllUserIdentitiesResponse is unavailable in lockstep with
its only caller — the also-unavailable CloudKitService.discoverAllUserIdentities() —
which built fine on 6.2+ but failed on Swift 6.1 with:

    error: 'processDiscoverAllUserIdentitiesResponse' is unavailable:
           Pending #28: discoverAllUserIdentities is not yet ready.

Wrap just the attribute in `#if swift(>=6.2)` so the body is shared and
6.1 compiles. Inline doc records the intent and the one-line cleanup
(delete the #if/#endif) once 6.1 is dropped from the matrix.

A `swiftlint:disable:next unavailable_function` is required because
swiftlint does not evaluate #if and otherwise sees a fatalError-only
function without the attribute.

Verified: swift build + swift test pass on Swift 6.1.3 (Linux container)
and on macOS Swift 6.2+ (475/475 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #315: split unhandled-response logging into debug (full body) + warning (type/status only)

CodeQL's swift/cleartext-logging flagged the existing warning logs
because lookupUsersByEmail(_:) propagates email-PII taint through the
response object. Move full \(response) interpolation to .debug so the
detail stays available for development without flowing into ops logs;
keep .warning at type(of:) + HTTP status code only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #312: add --lookup-email / CLOUDKIT_LOOKUP_EMAIL to exercise users/lookup/email

LookupUsersByEmailPhase previously skipped whenever fetchCaller() didn't
return an email (which is the common case). Plumb a configurable lookup
email through TestIntegrationConfig / TestPrivateConfig → PhaseContext so
the phase can be driven against a known-discoverable iCloud account.
Falls back to caller email, then to a clearer skip message naming the
flag/env var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: point CLAUDE.md lint section at mise (and Scripts/lint.sh)

swift-format / swiftlint / periphery are pinned in mise.toml; the
previous "requires swiftlint installation" wording led to PATH lookups
that fail in this repo. Replace with `mise exec --` invocations and
flag the full ./Scripts/lint.sh pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #315: address review punch list — invalidPrivateKey, recoverable unavailable response, supportsUserContextPhases derivation

- CloudKitError: add invalidPrivateKey(path:underlying:) so PEM-load failures
  carry the file path + original error instead of bare Foundation NSError.
  Wrap loadPEM() at the single call site in Credentials+TokenManager. Add
  PrivateKeyMaterial.filePath accessor for the diagnostic.

- processDiscoverAllUserIdentitiesResponse: replace fatalError with
  throw CloudKitError.unsupportedOperationType so a stray Swift 6.1 caller
  (where the @available cascade does not apply) gets a recoverable error
  instead of a crash.

- TestPrivateCommand: derive supportsUserContextPhases from
  config.base.hasUserContextCredentials, mirroring TestIntegrationCommand,
  so user-identity phases skip cleanly when web-auth env vars are absent.

- toPrimaryCredentials: replace try? with do/catch + stderr INFO line so
  operators see when web-auth is missing on a .public setup.

- CLAUDE.md: annotate discoverAllUserIdentities() as unavailable pending #28.

- CredentialsTokenManagerTests: fill the missing routing-matrix branches
  (user-context × .private/.shared, .shared + token-only) and cover the new
  .invalidPrivateKey path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants