Skip to content

Refactor AuthenticationMiddleware so each Authenticator applies itself (#290) - #294

Merged
leogdion merged 5 commits into
v1.0.0-beta.1from
290-authenticationmiddleware-authenticator
May 7, 2026
Merged

Refactor AuthenticationMiddleware so each Authenticator applies itself (#290)#294
leogdion merged 5 commits into
v1.0.0-beta.1from
290-authenticationmiddleware-authenticator

Conversation

@leogdion

@leogdionleogdion commented May 7, 2026

Copy link
Copy Markdown
Member

Summary

Closes#290.

AuthenticationMiddleware.intercept previously owned all per-method knowledge of how to attach credentials — switching over TokenCredentials.method and reaching back into tokenManager as? ServerToServerAuthManager for signing. This PR pushes the request-mutation logic onto the credential itself.

Changes

  • New public Authenticator protocol with storageKey, authenticate(request:body:), encoded(), init(decoding:). Existential-friendly — no Self requirement (Equatable/Codable deliberately not inherited). Consumers can write custom auth (test fakes, future modes) without forking MistKit.
  • Three concrete authenticators: APITokenAuthenticator, WebAuthTokenAuthenticator, ServerToServerAuthenticator. Each owns format validation in its throws init and its on-disk wire format.
  • Middleware collapses to a one-liner: get authenticator → call authenticate → forward. No switch, no downcast, no assertionFailure.
  • TokenManager.getCurrentCredentials()currentAuthenticator() -> (any Authenticator)? on the protocol and all four managers (APITokenManager, WebAuthTokenManager, ServerToServerAuthManager, AdaptiveTokenManager).
  • ServerToServerAuthenticator owns signing — kills the manager downcast and the assertionFailure. Buffers the body once and reassigns it via body: inout HTTPBody? so downstream middleware sees the same bytes regardless of HTTPBody iteration behavior. Exposes configurable bodyBufferLimit (default 1 MiB).
  • TokenStorage now stores any Authenticator; InMemoryTokenStorage uses [storageKey: Data] with a factory map for routing decoding.
  • Deletes: AuthenticationMethod, TokenCredentials, ServerToServerAuthManager+RequestSigning, credentialsWithMetadata(_:) on each manager, validateAPITokenFormat/validateWebAuthTokenFormat static helpers, and the now-redundant InternalErrorReason.serverToServerRequiresSpecificManager / failedCredentialRetrieval{AfterUpgrade,AfterDowngrade} cases.
  • New tests: APITokenAuthenticatorTests, WebAuthTokenAuthenticatorTests, ServerToServerAuthenticatorTests cover per-method query/header construction, format validation throwing from init, and encoded()init(decoding:) round-trip. The S2S suite includes a regression test for body re-buffering on .single iteration bodies.

No deprecation cycle — pre-1.0, dropped in one PR per the issue's recommendation.

Test plan

  • swift test — 430 tests / 127 suites pass (up from 412)
  • cd Examples/MistDemo && swift test — 831 tests / 275 suites pass
  • Build succeeds on macOS

🤖 Generated with Claude Code


Perform an AI-assisted review on CodePeer.com

…290)
Replaces the per-method switch in AuthenticationMiddleware with a public
Authenticator protocol whose concrete types own both the credential payload
and the rules for attaching it to a request. The middleware now resolves to
a single delegation call.
- Adds public Authenticator protocol + APITokenAuthenticator,
WebAuthTokenAuthenticator, ServerToServerAuthenticator
- Replaces TokenManager.getCurrentCredentials() with currentAuthenticator()
- ServerToServerAuthenticator owns request signing, killing the manager
downcast and assertionFailure in the middleware. Buffers body once and
reassigns it so downstream sees the same bytes regardless of HTTPBody
iteration behavior. Exposes configurable bodyBufferLimit.
- Format validation moves into authenticator throwing inits
- Each authenticator owns its wire format via encoded()/init(decoding:);
InMemoryTokenStorage routes decoding by storageKey
- Deletes AuthenticationMethod, TokenCredentials,
ServerToServerAuthManager+RequestSigning, and obsolete tests
- Adds dedicated per-authenticator test suites
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 7, 2026

Copy link
Copy Markdown

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: 046da54f-9a64-469c-92d6-d87799908647

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 290-authenticationmiddleware-authenticator

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

claudeBot commented May 7, 2026

Copy link
Copy Markdown

Code Review — PR #294: Refactor AuthenticationMiddleware so each Authenticator applies itself

Overall: Strong architectural improvement. The middleware collapse to a one-liner, the elimination of assertionFailure, and the typed errors per authenticator are all the right direction. A few issues worth addressing, ranging from a naming bug to a material extensibility gap.


✅ What's good

  • Responsibility inversion done right: pushing authenticate(request:body:) onto each authenticator eliminates the switch+downcast in AuthenticationMiddleware and makes new auth schemes addable without touching the middleware.
  • ServerToServerAuthenticator owning signing kills the assertionFailure that was a latent crash path. That's a real reliability improvement.
  • Body buffering with bodyBufferLimit: the explicit re-buffering so downstream middleware sees the same bytes, plus the configurable limit, shows careful attention to the HTTP/2 body-iteration semantics documented in CLAUDE.md.
  • Test coverage: the new APITokenAuthenticatorTests, WebAuthTokenAuthenticatorTests, ServerToServerAuthenticatorTests including round-trip and body-rebuffer regression tests are exactly what this kind of cryptographic code needs.
  • Deleting failedCredentialRetrievalAfterUpgrade/Downgrade: removing guard statements that could never logically fail is correct cleanup.

🐛 Bug / Naming issue: appendingQueryItems is mutating

In HTTPRequest+QueryItems.swift:

internalmutatingfunc appendingQueryItems(_ items:[URLQueryItem]){
// modifies self.path in place
}

In Swift convention, the -ing suffix (appending, sorted, filtered) signals a non-mutating operation that returns a new value. The mutating form should be named appendQueryItems(_:). The current name causes confusion when reading call sites:

// In APITokenAuthenticator.authenticate:
request.appendingQueryItems([URLQueryItem(name:"ckAPIToken", value: token)])

This looks like a discarded return value — a common source of silent bugs. It actually mutates request correctly (since request is inout), but the name actively misleads readers. Rename to appendQueryItems (or append(queryItems:)) before merge.


🔧 Extensibility gap: InMemoryTokenStorage can't decode custom Authenticator types

The PR's stated goal is: "Consumers can write custom auth without forking MistKit." But InMemoryTokenStorage.factories is a hardcoded static dict of the three built-in types:

privatestaticletfactories:[String:@Sendable(Data)throws->anyAuthenticator]={varentries=[APITokenAuthenticator.storageKey:...,WebAuthTokenAuthenticator.storageKey:...,]
if #available(...){entries[ServerToServerAuthenticator.storageKey]=...}return entries
}()

A consumer implementing a custom Authenticator can call authenticate(request:body:) correctly, but InMemoryTokenStorage.retrieve will always throw TokenStorageError.corruptedStorage for their type because no factory is registered. The storage round-trip is effectively broken for any type not in this list.

Suggested fix: expose a register(factory:) method on InMemoryTokenStorage (or accept a factory map in init) so custom types can plug in. Alternatively, add a defaultIdentifier and factory requirement to the Authenticator protocol itself — since init(decoding:) is already required, the factory is just { try Self(decoding: $0) }.


🔧 Same issue: defaultIdentifier(for:) type-casts to concrete types

privatestaticfunc defaultIdentifier(for authenticator:anyAuthenticator)->String{iflet api = authenticator as?APITokenAuthenticator{...}iflet web = authenticator as?WebAuthTokenAuthenticator{...}if #available(...),let s2s = authenticator as?ServerToServerAuthenticator{...}returntype(of: authenticator).storageKey // fallback for custom types
}

This is the same OCP violation. Custom authenticators silently fall back to storageKey as their identifier (which may collide). A defaultIdentifier: String protocol requirement (with a default of type(of: self).storageKey via extension) would let each type own its identifier scheme.


⚠️ Error message drops context on unknown storage key

guardlet factory =Self.factories[entry.storageKey]else{throwTokenStorageError.corruptedStorage
}

When an unknown storageKey is encountered (e.g., a custom authenticator — see above), corruptedStorage is thrown with no indication of which key was unrecognized. At minimum include the key in the error:

throwTokenStorageError.unrecognizedStorageKey(entry.storageKey)

Or pass it as context in corruptedStorage if that type accepts a message.


Minor notes

  • upgradeToWebAuthentication, downgradeToAPIOnly, and updateWebAuthToken are now @discardableResult — good call, since callers often don't need the returned authenticator but shouldn't get a warning.
  • The mock in MockCommandTokenManager now uses a full 64-char hex token to satisfy APITokenAuthenticator's validation: "abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234" — this is correct but consider extracting a test constant so the magic string doesn't appear in multiple places.

@claude

claudeBot commented May 7, 2026

Copy link
Copy Markdown

Code Review — PR #294: Refactor AuthenticationMiddleware so each Authenticator applies itself

Overview

This is a well-executed architectural refactor. Pushing request-mutation logic from the middleware (which had to switch over TokenCredentials.method and downcast tokenManager) onto the credential itself eliminates the switch, the downcast, and the assertionFailure. The new Authenticator protocol is existential-friendly (no Self requirement), which is the right tradeoff for storage and TokenManager.currentAuthenticator().


Issues

1. Silent body-buffering failure in ServerToServerAuthenticator.authenticate (security / correctness)

do{
bodyData =tryawaitData(collecting: original, upTo: bodyBufferLimit)}catch{
bodyData =nil // ← swallows the error
}iflet bytes = bodyData {
body =HTTPBody(bytes)}

If buffering fails (body exceeds bodyBufferLimit or any other HTTPBody error), bodyData silently becomes nil, the body is not replaced, and signRequest is called with nil — producing a signature over an empty body while the actual request body is still being forwarded unchanged. CloudKit will reject the request with a signature mismatch, but only at the server. Worse, a future transport that retries might forward the un-buffered body with a stale signature.

Recommendation: propagate the error rather than swallowing it:

bodyData =tryawaitData(collecting: original, upTo: bodyBufferLimit)
body =HTTPBody(bodyData!) // or HTTPBody(bodyData)

If you want a distinct error for oversized bodies, throw a typed error here before reaching signRequest.


2. appendingQueryItems is mutating but named like a non-mutating factory (naming)

internalmutatingfunc appendingQueryItems(_ items:[URLQueryItem]){

Swift convention: names starting with appending / adding / inserting return a new value (appending returns a new collection). A mutating method that modifies self in-place should be named appendQueryItems (verb, no participle). The current name is actively misleading — a reader expects a return value.


3. defaultIdentifier in InMemoryTokenStorage+Convenience breaks the protocol abstraction (design)

privatestaticfunc defaultIdentifier(for authenticator:anyAuthenticator)->String{iflet api = authenticator as?APITokenAuthenticator{...}iflet web = authenticator as?WebAuthTokenAuthenticator{...}if #available(...),let s2s = authenticator as?ServerToServerAuthenticator{...}returntype(of: authenticator).storageKey
}

This is a type-switch disguised as if-chains — exactly the pattern the Authenticator protocol was designed to eliminate. Adding a new Authenticator concrete type won't cause a compiler error here; it will silently fall through to storageKey as the identifier.

Recommendation: add an optional defaultStorageIdentifier: String requirement to Authenticator with a protocol-extension default of storageKey, and let each type override if it needs a richer identifier (e.g., "api-\(token.prefix(8))"). This keeps the coupling inside each authenticator type, where it belongs.


4. ServerToServerAuthenticator.encoded() serialises the private key as base64 JSON (security / documentation)

The raw P-256 private key material is stored in plain base64 inside a JSON blob in InMemoryTokenStorage. InMemoryTokenStorage is documentation-scoped to "development and testing" and doesn't persist to disk, so this is low risk now. But the TokenStorage protocol is public, and any concrete TokenStorage that does persist could leak key material. A doc-comment on Authenticator.encoded() warning that the returned Data may contain sensitive key material would set expectations for implementors.


5. ServerToServerAuthenticator.init runs a test signature on every construction (performance, minor)

do{
_ =try privateKey.signature(for:Data("test".utf8))}catch{...}

This fires in currentAuthenticator(), which is called on every request. A P-256 signature is cheap (~microseconds), so this probably doesn't matter in practice, but it is unnecessary work given that the key was already successfully used (or validated) during ServerToServerAuthManager.init. Consider validating once at init time and caching a Bool isValid, or removing the runtime check from the authenticator init since the manager already validates at construction.


What Works Well

  • Middleware collapse to get-authenticator → authenticate → forward is clean and extensible.
  • TokenStorage serialisation pivot from TokenCredentials to any Authenticator + factory map is the right data model.
  • Body re-buffering design (assign body = HTTPBody(bytes) so downstream middleware sees the same bytes) is sound — assuming point Migrating code #1 above is fixed.
  • Deleting AuthenticationMethod, TokenCredentials, ServerToServerAuthManager+RequestSigning, and the InternalErrorReason cases that were only meaningful in the old design removes real dead weight.
  • Test count +18 (430 → 412 on swift test, +556 on MistDemo) with round-trip coverage of encoded()/init(decoding:) is solid.

Verdict

The design direction is correct. Fix the silent body-buffering failure (#1) before merging — it's a correctness/security issue, not just a style nit. Items #2 and #3 are lower priority but worth addressing to keep the protocol abstraction clean. Items #4 and #5 are documentation / minor performance notes.

@codecov

codecovBot commented May 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.15356% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.04%. Comparing base (2c3b43e) to head (e5f58c4).
⚠️ Report is 9 commits behind head on v1.0.0-beta.1.

Files with missing linesPatch %Lines
...t/Authentication/ServerToServerAuthenticator.swift69.44%22 Missing ⚠️
...hentication/AdaptiveTokenManager+Transitions.swift0.00%11 Missing ⚠️
...hentication/InMemoryTokenStorage+Convenience.swift0.00%10 Missing ⚠️
...s/CommandIntegration/MockCommandTokenManager.swift0.00%5 Missing ⚠️
.../MistKit/Authentication/InMemoryTokenStorage.swift91.83%4 Missing ⚠️
.../MistKit/Authentication/AdaptiveTokenManager.swift66.66%2 Missing ⚠️
Sources/MistKit/Authentication/Authenticator.swift0.00%2 Missing ⚠️
...Kit/Authentication/WebAuthTokenAuthenticator.swift92.59%2 Missing ⚠️
...t/Authentication/WebAuthTokenManager+Methods.swift0.00%2 Missing ⚠️
Sources/MistKit/MistKitClient.swift0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## v1.0.0-beta.1 #294 +/- ##
==================================================
+ Coverage 25.66% 66.04% +40.38% 
==================================================
Files 95 480 +385 Lines 8240 13513 +5273 ==================================================
+ Hits 2115 8925 +6810 + Misses 6125 4588 -1537 
FlagCoverage Δ
mistdemo-spm-macos56.49% <11.61%> (?)
mistdemo-swift-6.2-jammy56.50% <11.61%> (?)
mistdemo-swift-6.2-noble56.50% <11.61%> (?)
mistdemo-swift-6.3-jammy56.50% <11.61%> (?)
mistdemo-swift-6.3-noble56.50% <11.61%> (?)
spm45.21% <78.62%> (+19.55%)⬆️
swift-6.1-jammy45.17% <78.62%> (+19.62%)⬆️
swift-6.1-noble45.14% <78.24%> (+19.57%)⬆️
swift-6.2-jammy45.17% <78.24%> (+19.52%)⬆️
swift-6.2-noble45.14% <78.24%> (+19.49%)⬆️
swift-6.3-jammynightly?
swift-6.3-noble45.42% <78.62%> (?)
swift-6.3-noblenightly?

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

claudeBot commented May 7, 2026

Copy link
Copy Markdown

Code Review (Draft)

Overview

Large refactor (~1500 additions / ~1700 deletions) pushing per-method authentication logic from AuthenticationMiddleware into concrete Authenticator types. The middleware collapses from ~120 lines to ~15. The new Authenticator protocol is public-facing and extensible.


Code Quality

Positive:

  • Authenticator has no Self or associated-type requirements, so any Authenticator works as an existential without boxing — important for a public protocol.
  • Middleware simplification removes the switch + downcast + assertionFailure, which is the right direction.
  • Body buffering in ServerToServerAuthenticator to prevent double-read of HTTPBody is correct.
  • @discardableResult on upgradeToWebAuthentication and downgradeToAPIOnly is appropriate.
  • Token format validation moved into authenticator init — good fail-fast design.

Issues:

  1. Naming convention violation: appendingQueryItems. In Swift API Design Guidelines, the -ing suffix signals a non-mutating function returning a new value (e.g., sorted() vs sort()). But HTTPRequest.appendingQueryItems is defined as mutating func with void return — the opposite of the convention. It works correctly because authenticate takes request: inout HTTPRequest, but the name will mislead readers into thinking the result should be captured. Rename to appendQueryItems to match the mutating/void convention.

  2. bodyBufferLimit overflow behavior is undocumented. The 1 MiB default is stated but neither the rationale nor the overflow behavior is documented: does exceeding the limit throw, truncate, or log? This needs to be described on the property and exercised in a test.

  3. public import re-exports.APITokenAuthenticator.swift uses public import Foundation, public import HTTPTypes, and public import OpenAPIRuntime. This re-exports those modules to any client importing MistKit. Re-exporting OpenAPIRuntime in particular exposes generated internal types as part of MistKit's public surface — confirm this is intentional and consistent with other public-facing files.

  4. InMemoryTokenStorage factory map silent failure. If a new Authenticator type is added but its factory is not registered in the decoding map, storage silently returns nil rather than throwing. A comment noting that the factory map must stay in sync with all conformances (or a debug-mode assertionFailure) would catch this during development.


Test Coverage

Tests for APITokenAuthenticator, WebAuthTokenAuthenticator, and format validation appear present. ✓

A test verifying that authenticate(request:body:) actually mutates request to include the ckAPIToken query item in the outbound URL would guard against any future regression from the naming confusion noted above.


Summary

Architecture direction is correct. Before marking ready: fix the appendingQueryItems naming, document bodyBufferLimit overflow behavior, and confirm intent of public import re-exports.

@claude

claudeBot commented May 7, 2026

Copy link
Copy Markdown

Code Review — PR #294: Refactor AuthenticationMiddleware so each Authenticator applies itself

Summary: This is a significant, well-motivated architectural improvement. The middleware collapsing to a single dispatch call is exactly right, and removing AuthenticationMethod / TokenCredentials eliminates a whole class of downcast-based bugs. One likely bug and several design questions need attention before this leaves DRAFT.


Likely Bug — APITokenAuthenticator.authenticate discards its result

publicfunc authenticate(
request:inoutHTTPRequest,
body:inoutHTTPBody?)asyncthrows{
request.appendingQueryItems([URLQueryItem(name:"ckAPIToken", value: token)])}

HTTPRequest.appendingQueryItems returns a new HTTPRequest value — it does not mutate the receiver. Because request is inout, the idiomatic fix is:

request = request.appendingQueryItems([URLQueryItem(name:"ckAPIToken", value: token)])

As written, the ckAPIToken query item is silently dropped for every API-token authenticated request. This would cause 401 errors at runtime but would not be caught by the current test suite if it only inspects the mutated request via the inout parameter after the call.

The same pattern should be audited in WebAuthTokenAuthenticator and ServerToServerAuthenticator.


Design Concerns

1. storageKey as static let vs. instance property

The protocol (based on InMemoryTokenStorage's factory-map usage) appears to use storageKey as a type-level discriminator for routing decoding. Using static let is correct for that purpose, but making it public static let on each concrete type means a consumer who creates a custom Authenticator must pick a key that doesn't collide with the three built-in keys ("api-token", etc.). Consider documenting the key namespace or providing a registration mechanism.

2. bodyBufferLimit default of 1 MiB may be too small

ServerToServerAuthenticator buffers the entire request body to sign it. The default 1 MiB is mentioned as configurable, but CloudKitService constructs ServerToServerAuthManager internally — callers have no path to increase the limit for operations with large record payloads (e.g., modify requests with many large string fields). Verify that the buffer limit is surfaced at the CloudKitService or ServerToServerAuthManager initializer level.

3. InMemoryTokenStorage factory map is open to key collision

If InMemoryTokenStorage routes decoding via a [String: (Data) throws -> any Authenticator] map keyed on storageKey, two third-party Authenticator implementations with the same key would silently overwrite each other. This is fine for the library's three built-in types but worth documenting as a constraint for custom authenticators.


Positives

  • Middleware reducing to get-authenticator → authenticate → forward is a clear win. The previous switch + downcast was a maintenance liability.
  • Pushing format validation into each authenticator's throws init means invalid credentials are rejected at construction time, not at first use.
  • ServerToServerAuthenticator owning signing (and buffering) eliminates the assertionFailure downcast path — a correct and safe change.
  • The test plan explicitly counts passing tests before and after, which is good practice for a refactor this size.
  • Deleting AuthenticationMethod, TokenCredentials, and the related InternalErrorReason cases is the right call pre-1.0 rather than carrying a deprecation cycle.

Minor / nits

  • The mock in MockCommandTokenManager now uses a real 64-character hex string for the API token — that's better than a short placeholder for format validation to pass.
  • The encoded() / init(decoding:) round-trip tests are a good regression guard.
  • Consider a test that calls authenticate(request:body:) and then reads back request to verify the query item / header was actually applied, rather than only testing that the method doesn't throw.

Verdict: The direction is correct and the architecture is sound. Fix the appendingQueryItems result-discard bug before merging — it would cause silent authentication failures in production. The design concerns are worth addressing but can be done in follow-ups.

leogdionand others added 4 commits May 7, 2026 08:45
- ServerToServerAuthenticator.authenticate now propagates HTTPBody.collecting
errors instead of silently signing over an empty body. An oversized body
throws rather than producing a request that CloudKit will reject with a
signature mismatch.
- Add defaultStorageIdentifier requirement to Authenticator (default returns
storageKey). Each concrete type provides a richer identifier; deletes the
type-switch in InMemoryTokenStorage+Convenience that bypassed the
protocol abstraction.
- Document that Authenticator.encoded() may contain sensitive credential
material so TokenStorage implementors handle persistence accordingly.
- Drop the redundant test-sign in ServerToServerAuthenticator.init — a
successfully-parsed P256.Signing.PrivateKey is by definition capable of
signing, and this ran on every per-request currentAuthenticator() call.
- Rename HTTPRequest.appendingQueryItems → appendQueryItems (mutating
methods take the imperative form).
- Bump swift-crypto to 4.0.0 so P256.Signing.PrivateKey conforms to Sendable
on Linux (fixes the CI build failure on the Sendable-conforming class
diagnostic).
Adds tests for oversized-body throw and defaultStorageIdentifier per type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reorder authenticator types to match SwiftLint's type_contents_order
(subtype → type_property → instance_property → initializer →
other_method). Move private WireFormat to top of each struct, place
init(decoding:) with other initializers.
- Split ServerToServerAuthenticator.signRequest into its own +Signing.swift
extension to keep the main file under SwiftLint's 225-line limit.
- Move S2S manager's privateKeyData computed property up before its
initializers.
- Add doc comments to all protocol witnesses on the new authenticator
types and InMemoryTokenStorage's TokenStorage methods.
- Add - Throws sections to Authenticator.authenticate and
ServerToServerAuthenticator.signRequest.
- Drop redundant `as TokenManagerError` casts in catch clauses — the
inits use typed throws so the cast is always-true.
- Downgrade unused public Foundation import in WebAuthTokenManager+Methods
to internal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@leogdion
leogdion marked this pull request as ready for review May 7, 2026 14:24
@claude

claudeBot commented May 7, 2026

Copy link
Copy Markdown

Code Review: Authenticator Protocol Refactor (#294)

Overview

This PR pushes per-method authentication logic out of AuthenticationMiddleware and onto three concrete Authenticator types (APITokenAuthenticator, WebAuthTokenAuthenticator, ServerToServerAuthenticator). The middleware shrinks to a three-line delegation. The assertionFailure + manager downcast for server-to-server auth is eliminated. This is a meaningful correctness and maintainability improvement.


Strengths

  • Middleware collapse is clean. The old intercept had ~120 lines with a switch, private helpers, and a latent downcast bug. The new version is 3 lines and correct by construction.
  • Body-buffering moved to the right place. The old extractRequestBodyData silently swallowed errors (catch { return nil }), meaning a too-large body would produce an unsigned request rather than an error. The new ServerToServerAuthenticator.authenticate propagates buffering failures correctly.
  • bodyBufferLimit is configurable. Thoughtful addition for callers who need to sign larger bodies.
  • Body re-buffering regression test (bufferReplacesSingleIterationBody) is exactly what this kind of change needs — covers the .single-iteration HTTPBody edge case explicitly.
  • TokenCredentials / AuthenticationMethod removal is clean. No residual references were left behind (mock managers, test helpers all updated).

Issues

Medium

1. downgradeToAPIOnly() does not update storage.

upgradeToWebAuthentication stores the new WebAuthTokenAuthenticator in storage. downgradeToAPIOnly does not remove that entry or store the new APITokenAuthenticator. A caller who downgrades and then restores from InMemoryTokenStorage will get back stale web-auth credentials. The old code had the same gap, but with the storage protocol now typed to any Authenticator, the round-trip expectation is clearer. Consider either updating storage on downgrade or documenting that storage is write-only on downgrade.

2. InMemoryTokenStorage.factories is hardcoded — custom Authenticator types cannot be persisted.

The factory map is a static let with three entries:

privatestaticletfactories:[String:@Sendable(Data)throws->anyAuthenticator]={...}()

retrieve() throws corruptedStorage for any unknown storageKey. The Authenticator protocol is described as "existential-friendly — consumers can write custom auth," but those custom types can never be decoded from InMemoryTokenStorage. Either document this limitation explicitly, or provide a registration mechanism (e.g. register(factory:forKey:) on InMemoryTokenStorage).

3. Remaining manager downcast in MistKitClient.swift.

The middleware downcast was eliminated, but MistKitClient.swift still has:

if let serverManager = tokenManager as?ServerToServerAuthManager{
keyID = serverManager.keyID
privateKeyData = serverManager.privateKeyData

If the intent is to fully remove downcasts, this one was missed. If it's intentional (different code path), a comment explaining why the Authenticator protocol can't be used here would help.

Minor

4. WebAuthTokenManager.encodedWebAuthToken allocates a new CharacterMapEncoder on every call.

WebAuthTokenManager+Methods.swift changed from tokenEncoder.encode(webAuthToken) (field-cached) to CharacterMapEncoder().encode(webAuthToken). WebAuthTokenAuthenticator has a static encoder field. The two approaches are inconsistent. If CharacterMapEncoder is allocation-heavy, the manager extension should use a static or the authenticator's static instance.

5. Error-checking test pattern could use #expect(throws:).

The new authenticator tests check error types via do/catch + Issue.record:

do{
_ =tryAPITokenAuthenticator(token:"")Issue.record("Expected init to throw")}catch{if case .invalidCredentials(.apiTokenEmpty)= error {...}else{Issue.record("Unexpected error: \(error)")}}

Swift Testing's #expect(throws:) closure overload (or #expect { } throws:) makes the intent clearer and avoids the false-pass risk if the Issue.record call itself fails. Not a blocking issue, but worth standardising.

6. listIdentifiers() comment says expired entries are included, but that may surprise callers.

retrieve(identifier:) silently returns nil and removes expired entries. A caller who does listIdentifiers()retrieve(identifier:) for each result gets silent mismatches. The old code had the same behaviour, but it's worth a note in the protocol doc (or filtering expired entries from listIdentifiers).


Security

7. Raw P-256 key material in JSON.

ServerToServerAuthenticator.encoded() writes the private key as base64 in plain JSON. The Authenticator.encoded() doc has a warning. This is fine for InMemoryTokenStorage (in-process only), but any on-disk TokenStorage implementor who calls this without encryption exposes the key. The warning on the protocol is good; consider repeating the warning on ServerToServerAuthenticator.encoded() specifically, since it is the most sensitive of the three.


Summary

The refactor is well-executed. The middleware is genuinely simpler and the body-buffering bug fix is a real correctness win. The main actionable items are:

  1. downgradeToAPIOnly storage gap — decide on semantics and document or fix.
  2. Custom Authenticator storage — either document the limitation or add a registration hook.
  3. MistKitClient downcast — either remove it (using the Authenticator protocol) or comment why it exists.

The test count increase (18 new tests) and the explicit body-buffering regression test make this feel well-covered. Approve with the above for consideration.

🤖 Reviewed with Claude Code

@leogdion
leogdion merged commit afcae11 into v1.0.0-beta.1May 7, 2026
97 of 98 checks passed
@leogdion
leogdion deleted the 290-authenticationmiddleware-authenticator branch May 7, 2026 15:01
@claudeclaudeBot mentioned this pull request May 12, 2026
leogdion added a commit that referenced this pull request May 15, 2026
…cator refactor
PR #294 moved the credential-application logic from AuthenticationMiddleware
into per-type Authenticator implementations and replaced the TokenManager
seam from getCurrentCredentials() -> TokenCredentials? to
currentAuthenticator() -> (any Authenticator)?. The internals article and the
cloudkit-guide auth chapter were still describing the pre-refactor design;
this commit brings both in line with the code on v1.0.0-beta.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
leogdion added a commit that referenced this pull request May 16, 2026
…cator refactor
PR #294 moved the credential-application logic from AuthenticationMiddleware
into per-type Authenticator implementations and replaced the TokenManager
seam from getCurrentCredentials() -> TokenCredentials? to
currentAuthenticator() -> (any Authenticator)?. The internals article and the
cloudkit-guide auth chapter were still describing the pre-refactor design;
this commit brings both in line with the code on v1.0.0-beta.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
leogdion added a commit that referenced this pull request May 18, 2026
commit de82483
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:35 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "ea897c3"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "ea897c3"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 24c8719
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:31 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "5bb4490"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "5bb4490"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit eee0670
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:13 2026 +0100
docs: sync README/CLAUDE examples to v1.0.0-beta.1 API; pin BushelCloud CI; exclude internal Python from CodeFactor
- README.md, Examples/BushelCloud/{CLAUDE.md,.docc,.claude/s2s-auth-details.md},
Examples/CelestraCloud/{CLAUDE.md,README.md,.claude/IMPLEMENTATION_NOTES.md}:
drop `try CloudKitService(... database: .public)` from init examples (init is
non-throwing, `database:` moved per-call); rewrite Quick Start auth around
`Credentials` + `APICredentials` / `ServerToServerCredentials` and show
`database: .public(.prefers(.serverToServer))` at the call site.
- Examples/BushelCloud/.github/workflows/{BushelCloud.yml,bushel-cloud-build.yml}:
pin MISTKIT_BRANCH to v1.0.0-beta.1 (matches CelestraCloud) so the subrepo PR
builds against the branch that actually carries the new API. Revert to `main`
once #298 merges.
- .codefactor.yml: exclude Scripts/mermaid-to-pptx.py (internal-use helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 4d60b19
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:45 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "c44dc4f"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "c44dc4f"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 5bc403d
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:40 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "55f2092"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "55f2092"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit bce1f23
Author: leogdion <leogdion@brightdigit.com>
Date: Sun May 17 20:09:47 2026 +0100
refactor!: prep for talk — shrink API, refactor auth, split OpenAPI (#279)
commit 7023a31
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 15 12:56:58 2026 -0400
Fixed Nonisolated Web Auth Token (#347)
commit f799128
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 20:27:28 2026 -0400
Add MistDemo-Integration workflow for live CloudKit runs (#345)
commit 418e2e4
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 16:03:04 2026 -0400
Resolve#342: v1.0.0-beta.1 follow-ups (#341#327#321#317) + CI fixes (#343)
commit d65d20b
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 11:25:10 2026 -0400
Resolve#330: interactive MistDemo (web toggle + native app refresh) (#332)
commit a28ab3c
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 11 16:31:10 2026 -0400
Resolve#313: paginationLimitExceeded carries accumulated records (#326)
commit 7a5da7a
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 17:09:53 2026 -0400
Fix CI failures + Claude review nits on PR #298 (v1.0.0-beta.1) (#322)
commit b3626c0
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 16:06:20 2026 -0400
Resolve#312: public+web-auth user-identity endpoints (#310, #311, #27, #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>
commit 6f92a71
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 13:16:56 2026 -0400
Resolve#308: docs refresh + CI fixes + sub-issues #165, #285 (#309)
commit a1e2162
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 07:16:10 2026 -0400
Add query pagination support with continuation markers (#306)
commit c62bf44
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 15:52:45 2026 -0400
Improve error handling: typed TokenManagerError and safe RecordOperation conversion (#305)
commit 7c4b678
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:10 2026 -0400
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "4244497"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "4244497"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit f14e751
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:07 2026 -0400
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "123a732"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "123a732"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit a0f0af9
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:26:32 2026 -0400
updating example packages
commit 125dab5
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:01:18 2026 -0400
Refactor AuthenticationMiddleware so each Authenticator applies itself (#294)
commit f989fd1
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:23:23 2026 -0400
Strengthen environment and database configuration validation (#293)
commit b0f00a7
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:18:52 2026 -0400
Add operation classification and batch sync result tracking (#296)
commit 63a4e50
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:09:27 2026 -0400
Move CloudKitResponseType default implementations to protocol extension (#292)
commit ae1af15
Author: leogdion <leogdion@brightdigit.com>
Date: Wed May 6 20:20:44 2026 -0400
Test suite improvements for v1.0.0-beta.1 (#286) (#287)
commit 5475bfa
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:32 2026 -0400
MistDemo: --database flag + demo-errors command (closes#259, #269) (#282)
commit 8b21425
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:17 2026 -0400
Refactor IntegrationTestRunner into protocol-based phase pipeline (#254) (#283)
commit 9709f3d
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 08:54:16 2026 -0400
Replace custom AsyncChannel with swift-async-algorithms (#280)
commit d53467a
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 4 12:49:25 2026 -0400
CI Updates for May 2026 (#277)
commit d7b1a21
Author: Leo Dion <leogdion@brightdigit.com>
Date: Thu Apr 30 09:39:09 2026 -0400
MistDemo improvements: test split, CRUD, auth fix, native app (#271) (#273)
commit 0ab2ab6
Author: leogdion <leogdion@brightdigit.com>
Date: Wed Apr 29 15:49:34 2026 -0400
First Draft Revision of Docs (#268)
leogdion added a commit that referenced this pull request May 18, 2026
commit de82483
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:35 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "ea897c3"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "ea897c3"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 24c8719
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:31 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "5bb4490"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "5bb4490"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit eee0670
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:13 2026 +0100
docs: sync README/CLAUDE examples to v1.0.0-beta.1 API; pin BushelCloud CI; exclude internal Python from CodeFactor
- README.md, Examples/BushelCloud/{CLAUDE.md,.docc,.claude/s2s-auth-details.md},
Examples/CelestraCloud/{CLAUDE.md,README.md,.claude/IMPLEMENTATION_NOTES.md}:
drop `try CloudKitService(... database: .public)` from init examples (init is
non-throwing, `database:` moved per-call); rewrite Quick Start auth around
`Credentials` + `APICredentials` / `ServerToServerCredentials` and show
`database: .public(.prefers(.serverToServer))` at the call site.
- Examples/BushelCloud/.github/workflows/{BushelCloud.yml,bushel-cloud-build.yml}:
pin MISTKIT_BRANCH to v1.0.0-beta.1 (matches CelestraCloud) so the subrepo PR
builds against the branch that actually carries the new API. Revert to `main`
once #298 merges.
- .codefactor.yml: exclude Scripts/mermaid-to-pptx.py (internal-use helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 4d60b19
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:45 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "c44dc4f"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "c44dc4f"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 5bc403d
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:40 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "55f2092"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "55f2092"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit bce1f23
Author: leogdion <leogdion@brightdigit.com>
Date: Sun May 17 20:09:47 2026 +0100
refactor!: prep for talk — shrink API, refactor auth, split OpenAPI (#279)
commit 7023a31
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 15 12:56:58 2026 -0400
Fixed Nonisolated Web Auth Token (#347)
commit f799128
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 20:27:28 2026 -0400
Add MistDemo-Integration workflow for live CloudKit runs (#345)
commit 418e2e4
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 16:03:04 2026 -0400
Resolve#342: v1.0.0-beta.1 follow-ups (#341#327#321#317) + CI fixes (#343)
commit d65d20b
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 11:25:10 2026 -0400
Resolve#330: interactive MistDemo (web toggle + native app refresh) (#332)
commit a28ab3c
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 11 16:31:10 2026 -0400
Resolve#313: paginationLimitExceeded carries accumulated records (#326)
commit 7a5da7a
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 17:09:53 2026 -0400
Fix CI failures + Claude review nits on PR #298 (v1.0.0-beta.1) (#322)
commit b3626c0
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 16:06:20 2026 -0400
Resolve#312: public+web-auth user-identity endpoints (#310, #311, #27, #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>
commit 6f92a71
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 13:16:56 2026 -0400
Resolve#308: docs refresh + CI fixes + sub-issues #165, #285 (#309)
commit a1e2162
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 07:16:10 2026 -0400
Add query pagination support with continuation markers (#306)
commit c62bf44
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 15:52:45 2026 -0400
Improve error handling: typed TokenManagerError and safe RecordOperation conversion (#305)
commit 7c4b678
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:10 2026 -0400
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "4244497"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "4244497"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit f14e751
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:07 2026 -0400
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "123a732"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "123a732"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit a0f0af9
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:26:32 2026 -0400
updating example packages
commit 125dab5
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:01:18 2026 -0400
Refactor AuthenticationMiddleware so each Authenticator applies itself (#294)
commit f989fd1
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:23:23 2026 -0400
Strengthen environment and database configuration validation (#293)
commit b0f00a7
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:18:52 2026 -0400
Add operation classification and batch sync result tracking (#296)
commit 63a4e50
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:09:27 2026 -0400
Move CloudKitResponseType default implementations to protocol extension (#292)
commit ae1af15
Author: leogdion <leogdion@brightdigit.com>
Date: Wed May 6 20:20:44 2026 -0400
Test suite improvements for v1.0.0-beta.1 (#286) (#287)
commit 5475bfa
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:32 2026 -0400
MistDemo: --database flag + demo-errors command (closes#259, #269) (#282)
commit 8b21425
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:17 2026 -0400
Refactor IntegrationTestRunner into protocol-based phase pipeline (#254) (#283)
commit 9709f3d
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 08:54:16 2026 -0400
Replace custom AsyncChannel with swift-async-algorithms (#280)
commit d53467a
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 4 12:49:25 2026 -0400
CI Updates for May 2026 (#277)
commit d7b1a21
Author: Leo Dion <leogdion@brightdigit.com>
Date: Thu Apr 30 09:39:09 2026 -0400
MistDemo improvements: test split, CRUD, auth fix, native app (#271) (#273)
commit 0ab2ab6
Author: leogdion <leogdion@brightdigit.com>
Date: Wed Apr 29 15:49:34 2026 -0400
First Draft Revision of Docs (#268)
leogdion added a commit that referenced this pull request May 19, 2026
commit de82483
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:35 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "ea897c3"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "ea897c3"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 24c8719
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:31 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "5bb4490"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "5bb4490"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit eee0670
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 21:14:13 2026 +0100
docs: sync README/CLAUDE examples to v1.0.0-beta.1 API; pin BushelCloud CI; exclude internal Python from CodeFactor
- README.md, Examples/BushelCloud/{CLAUDE.md,.docc,.claude/s2s-auth-details.md},
Examples/CelestraCloud/{CLAUDE.md,README.md,.claude/IMPLEMENTATION_NOTES.md}:
drop `try CloudKitService(... database: .public)` from init examples (init is
non-throwing, `database:` moved per-call); rewrite Quick Start auth around
`Credentials` + `APICredentials` / `ServerToServerCredentials` and show
`database: .public(.prefers(.serverToServer))` at the call site.
- Examples/BushelCloud/.github/workflows/{BushelCloud.yml,bushel-cloud-build.yml}:
pin MISTKIT_BRANCH to v1.0.0-beta.1 (matches CelestraCloud) so the subrepo PR
builds against the branch that actually carries the new API. Revert to `main`
once #298 merges.
- .codefactor.yml: exclude Scripts/mermaid-to-pptx.py (internal-use helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 4d60b19
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:45 2026 +0100
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "c44dc4f"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "c44dc4f"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit 5bc403d
Author: Leo Dion <leogdion@brightdigit.com>
Date: Sun May 17 20:10:40 2026 +0100
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "55f2092"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "55f2092"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "6f293daa9f"
commit bce1f23
Author: leogdion <leogdion@brightdigit.com>
Date: Sun May 17 20:09:47 2026 +0100
refactor!: prep for talk — shrink API, refactor auth, split OpenAPI (#279)
commit 7023a31
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 15 12:56:58 2026 -0400
Fixed Nonisolated Web Auth Token (#347)
commit f799128
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 20:27:28 2026 -0400
Add MistDemo-Integration workflow for live CloudKit runs (#345)
commit 418e2e4
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 16:03:04 2026 -0400
Resolve#342: v1.0.0-beta.1 follow-ups (#341#327#321#317) + CI fixes (#343)
commit d65d20b
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 14 11:25:10 2026 -0400
Resolve#330: interactive MistDemo (web toggle + native app refresh) (#332)
commit a28ab3c
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 11 16:31:10 2026 -0400
Resolve#313: paginationLimitExceeded carries accumulated records (#326)
commit 7a5da7a
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 17:09:53 2026 -0400
Fix CI failures + Claude review nits on PR #298 (v1.0.0-beta.1) (#322)
commit b3626c0
Author: leogdion <leogdion@brightdigit.com>
Date: Sat May 9 16:06:20 2026 -0400
Resolve#312: public+web-auth user-identity endpoints (#310, #311, #27, #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>
commit 6f92a71
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 13:16:56 2026 -0400
Resolve#308: docs refresh + CI fixes + sub-issues #165, #285 (#309)
commit a1e2162
Author: leogdion <leogdion@brightdigit.com>
Date: Fri May 8 07:16:10 2026 -0400
Add query pagination support with continuation markers (#306)
commit c62bf44
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 15:52:45 2026 -0400
Improve error handling: typed TokenManagerError and safe RecordOperation conversion (#305)
commit 7c4b678
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:10 2026 -0400
git subrepo push Examples/CelestraCloud
subrepo:
subdir: "Examples/CelestraCloud"
merged: "4244497"
upstream:
origin: "git@github.com:brightdigit/CelestraCloud.git"
branch: "mistkit"
commit: "4244497"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit f14e751
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:27:07 2026 -0400
git subrepo push Examples/BushelCloud
subrepo:
subdir: "Examples/BushelCloud"
merged: "123a732"
upstream:
origin: "git@github.com:brightdigit/BushelCloud.git"
branch: "mistkit"
commit: "123a732"
git-subrepo:
version: "0.4.9"
origin: "https://github.com/Homebrew/brew"
commit: "b9763ee528"
commit a0f0af9
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:26:32 2026 -0400
updating example packages
commit 125dab5
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 11:01:18 2026 -0400
Refactor AuthenticationMiddleware so each Authenticator applies itself (#294)
commit f989fd1
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:23:23 2026 -0400
Strengthen environment and database configuration validation (#293)
commit b0f00a7
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:18:52 2026 -0400
Add operation classification and batch sync result tracking (#296)
commit 63a4e50
Author: leogdion <leogdion@brightdigit.com>
Date: Thu May 7 10:09:27 2026 -0400
Move CloudKitResponseType default implementations to protocol extension (#292)
commit ae1af15
Author: leogdion <leogdion@brightdigit.com>
Date: Wed May 6 20:20:44 2026 -0400
Test suite improvements for v1.0.0-beta.1 (#286) (#287)
commit 5475bfa
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:32 2026 -0400
MistDemo: --database flag + demo-errors command (closes#259, #269) (#282)
commit 8b21425
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 20:21:17 2026 -0400
Refactor IntegrationTestRunner into protocol-based phase pipeline (#254) (#283)
commit 9709f3d
Author: leogdion <leogdion@brightdigit.com>
Date: Tue May 5 08:54:16 2026 -0400
Replace custom AsyncChannel with swift-async-algorithms (#280)
commit d53467a
Author: leogdion <leogdion@brightdigit.com>
Date: Mon May 4 12:49:25 2026 -0400
CI Updates for May 2026 (#277)
commit d7b1a21
Author: Leo Dion <leogdion@brightdigit.com>
Date: Thu Apr 30 09:39:09 2026 -0400
MistDemo improvements: test split, CRUD, auth fix, native app (#271) (#273)
commit 0ab2ab6
Author: leogdion <leogdion@brightdigit.com>
Date: Wed Apr 29 15:49:34 2026 -0400
First Draft Revision of Docs (#268)
@claudeclaudeBot mentioned this pull request May 27, 2026
Sign up for freeto 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.

1 participant

@leogdion