Skip to content

fix(auth): bind OAuth callback to loopback + harden keychain storage#6

Merged
atom2ueki merged 4 commits into
mainfrom
security/fix-loopback-bind
May 1, 2026
Merged

fix(auth): bind OAuth callback to loopback + harden keychain storage#6
atom2ueki merged 4 commits into
mainfrom
security/fix-loopback-bind

Conversation

@atom2ueki
Copy link
Copy Markdown
Owner

Summary

Fixes three security findings against the OAuth login path and the keychain-backed token store. All three came out of the security review on the SDK as it ships to third-party iOS/macOS app developers — anything weak here propagates into every consumer app.

1. OAuth callback server bound to all interfaces (High)

The local callback server inherited an INADDR_ANY / in6addr_any bind from SwiftWebServer 0.1.0, so during a login flow the server was reachable from any host on the same network. On a hostile LAN that's a reliable login-denial primitive (forge a callback faster than the browser, the single-shot server tears down before the legit callback lands) and a stretched code-injection race.

Fix: Bumped the dependency to SwiftWebServer 0.2.0 (released today: https://github.com/atom2ueki/SwiftWebServer/releases/tag/0.2.0), which adds a host: parameter to listen(). Now passing host: "localhost" in LocalCallbackServer.startListening() so the bind is constrained to dual-stack loopback (127.0.0.1 + ::1) per RFC 8252 §7.3. The browser can still reach the callback whether it resolves localhost to v4 or v6 first.

Defense in depth: the route handler now compares the incoming state query parameter against the value generated for the in-flight handshake. Mismatched callbacks return HTTP 400 without burning the single-shot resume — a forged or stale callback can no longer kill the legitimate one. The legit browser callback (which carries the genuine state) still wins.

2. KeychainTokenStorage silent fallback (Low)

KeychainTokenStorage.init silently fell back to a hardcoded global service name "com.codingplan.auth" when both servicePrefix and Bundle.main.bundleIdentifier were nil. In CLI tools, test harnesses, and app-extension hosts where Bundle.main is the host shell, two unrelated tools could end up sharing each other's credentials.

Fix: KeychainTokenStorage.init now throws AuthError.storageError when neither a prefix nor a bundle id is available. The failure surface moves from "first save/load returns the wrong creds" to "construction fails fast." AuthService.init lost its default storage so the failure is visible at the call site rather than buried in a default expression.

3. Adjacent improvements

  • isResolvedPortBindFailure now matches both IPv4 and IPv6 bind-failure messages so the existing port-race retry loop still triggers when the dual-stack listen loses the race on either family after the INADDR_ANY reservation closed.
  • LocalCallbackServerTests.reserveLocalPort now binds the conflicting reservation to INADDR_LOOPBACK instead of INADDR_ANY. With SO_REUSEADDR, an INADDR_ANY reservation does not prevent a more-specific loopback bind at the same port, so the test would otherwise stop observing the conflict it asserts on.

Breaking changes

This is a 0.x SDK; both breaks are intentional security posture improvements.

  • KeychainTokenStorage.init(servicePrefix:accessGroup:) is now throws. Existing callers add try. Apps with a Bundle.main.bundleIdentifier (the normal case) keep working unchanged once they add the keyword.
  • AuthService.init(storage:) no longer has a default value. Callers must pass storage explicitly. The DocC example was already showing explicit construction, so this matches the documented usage.

Test plan

  • swift build clean.
  • swift test — 49 tests, 0 failures, against the published SwiftWebServer 0.2.0.
  • Bind-failure regression test continues to fail-the-bind as intended.
  • Manual smoke: actual OAuth login on a real iOS/macOS app (recommended before tagging a release of this SDK).

Notes

This is one clean commit on top of the latest main (1c6a72f). No Package.resolved change is committed (gitignored in this repo). Ready to merge.

…rage

Addresses three security findings against the OAuth login path and the
keychain-backed token store.

OAuth callback server (was: bound to all interfaces during login)

Bumps SwiftWebServer to 0.2.0 and passes host: "localhost" to listen()
so the callback socket is constrained to dual-stack loopback (127.0.0.1
+ ::1) per RFC 8252 §7.3. Without this, the server was reachable from
any host on the same network while a login was in flight, giving a LAN
peer a reliable login-denial primitive (and a stretched code-injection
race) against any consumer app.

Defense in depth: the callback handler now compares the incoming state
query parameter against the value generated for this flow and returns
HTTP 400 on mismatch *without* burning the single-shot resume — a
forged callback can no longer kill the legitimate browser one.

Updated isResolvedPortBindFailure to also match IPv6 bind failures so
the existing port-race retry loop still triggers when ::1 loses the
race after the INADDR_ANY reservation closed.

KeychainTokenStorage init (was: silent fallback to a global service name)

KeychainTokenStorage.init now throws when both servicePrefix and
Bundle.main.bundleIdentifier are nil, so a CLI/test host can't end up
with a globally-shared "com.codingplan.auth" keychain service name and
inadvertently share credentials between unrelated tools sharing a
process bundle.

AuthService.init no longer has a default storage. Callers must
construct storage explicitly so the failure surface is at construction
time, not first save/load.

Tests
- All 49 existing tests pass against SwiftWebServer 0.2.0.
- Bind-failure regression test reserves the conflicting port via
  INADDR_LOOPBACK rather than INADDR_ANY (with SO_REUSEADDR a more-
  specific loopback bind can succeed against an INADDR_ANY reservation
  at the same port — the test would otherwise no longer observe the
  conflict it asserts on).
@atom2ueki atom2ueki requested a review from Copilot May 1, 2026 09:23
@atom2ueki
Copy link
Copy Markdown
Owner Author

@claude review this PR

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @atom2ueki's task in 1m 59s —— View job


PR Review: fix(auth): bind OAuth callback to loopback + harden keychain storage

  • Gather context and understand request
  • Read changed files
  • Analyze security claims and correctness
  • Post review feedback

Summary: The three security fixes are sound and the implementation is correct. A few items worth addressing before merging — one medium gap (no tests for the new state validation), one stale comment, and a minor docs nit. Nothing that blocks merge if the manual smoke test is green, but the test gap in particular is worth closing for a security-critical path.


Findings

Medium — State validation logic has no test coverage

LocalCallbackServer.swift:185-188 — the new expectedState defense-in-depth is the most important addition in this PR, but LocalCallbackServerTests.swift has no tests for it. Three cases should be covered:

  • Request with missing state → HTTP 400, continuation NOT consumed
  • Request with wrong state → HTTP 400, continuation NOT consumed
  • Request with correct state → continues normally

Without these, a future refactor of the makeServer() handler could silently break the "doesn't consume the continuation on mismatch" guarantee, defeating the whole point of the fix. Fix this →

Minor — Stale version in WebServerBox comment

LocalCallbackServer.swift:19:

// Reason: SwiftWebServer 0.1.0 does not yet declare itself Sendable.

This should read 0.2.0 (or be removed if 0.2.0 adds Sendable conformance — worth checking before tagging the release).

Minor (Docs nit) — try! in the overview example

CodingPlanAuth.md:29:

private let service = AuthService(storage: try! KeychainTokenStorage())

Using try! in a stored property initializer in an @Observable class will crash at startup if Bundle.main.bundleIdentifier is nil. Since the property is the very first thing shown to every new adopter, consider using a lazy var + try or adding a prose note explaining why try! is acceptable here (iOS/macOS apps always have a bundle id). Fix this →

Informational — resolveListenPort reserve socket still uses INADDR_ANY

LocalCallbackServer.swift:244-245 — the port-discovery socket briefly binds to INADDR_ANY:0. Since listen() is never called on it, nothing can actually connect, so the attack surface is essentially zero. Just noting it for completeness; the PR description doesn't call this out and it's a standard port-reservation idiom.


What's correct

  • Loopback bind (host: "localhost") is correctly placed and the dual-stack rationale (v4 + v6 in one call) is accurate. RFC 8252 §7.3 ✓
  • State check semantics — returning 400 without calling resume or Task { await target?.resume(with:) } correctly preserves the single-shot continuation so the real browser callback still wins. ✓
  • IPv6 bind-failure prefix addition in isResolvedPortBindFailure is correct; dual-stack listen can lose the race on either family after the reservation closes. ✓
  • reserveLocalPort test fix to INADDR_LOOPBACK is technically sound. The comment explains the SO_REUSEADDR / more-specific-bind interaction correctly. ✓
  • KeychainTokenStorage.init now throws — fail-fast at construction is the right design. The previous silent fallback to a hardcoded global name was the root cause of the credential-sharing risk. ✓
  • AuthService.init no longer has a default — forces callers to construct storage explicitly; matches what the DocC example was already showing. ✓
  • Breaking changes are explicitly documented in the PR body with migration guidance. ✓

Checklist

Loopback bind (RFC 8252 §7.3) ✅ Correct
State CSRF guard ✅ Logic correct, ⚠️ no tests
Retry loop dual-stack fix ✅ Correct
Test port reservation fix ✅ Correct
KeychainTokenStorage fail-fast ✅ Correct
AuthService storage default removed ✅ Correct
Manual smoke test ⬜ Not yet (noted in PR)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the OAuth local callback server and keychain-backed token storage to address security findings in the SDK’s login and credential persistence paths.

Changes:

  • Bind the local OAuth callback server to loopback only (host: "localhost") and add state validation to reject forged/stale callbacks without consuming the single-shot resume.
  • Make KeychainTokenStorage fail fast by throwing when neither an explicit servicePrefix nor a bundle identifier is available; require explicit storage construction in AuthService.
  • Update bind-failure detection for both IPv4/IPv6 and adjust the port-conflict test to reserve loopback rather than INADDR_ANY.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Tests/CodingPlanAuthTests/Infrastructure/LocalCallbackServerTests.swift Ensures the bind-conflict test reserves loopback to match the server’s new loopback-only bind behavior.
Sources/CodingPlanAuth/Infrastructure/Storage/KeychainTokenStorage.swift Makes the keychain storage initializer throwing to prevent silent global-service fallback in bundle-id-less hosts.
Sources/CodingPlanAuth/Infrastructure/Server/LocalCallbackServer.swift Binds callback server to loopback and adds expected-state checking; expands bind-failure detection to IPv6.
Sources/CodingPlanAuth/Infrastructure/OAuth/OAuth2PKCEFlow.swift Plumbs OAuth state into the callback server for defense-in-depth validation.
Sources/CodingPlanAuth/Documentation.docc/CodingPlanAuth.md Updates DocC sample to compile with the new throwing KeychainTokenStorage initializer.
Sources/CodingPlanAuth/Application/AuthService.swift Removes default storage parameter and updates docs to reflect explicit, throwing storage construction.
Package.swift Bumps SwiftWebServer dependency to a version that supports host-bound listening.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/CodingPlanAuth/Application/AuthService.swift Outdated
Comment thread Sources/CodingPlanAuth/Infrastructure/Storage/KeychainTokenStorage.swift Outdated
Comment thread Sources/CodingPlanAuth/Documentation.docc/CodingPlanAuth.md
- Add 4 unit tests for the new expectedState defense-in-depth check on
  LocalCallbackServer (Claude review medium / Copilot 4):
    - matching state completes the flow normally,
    - mismatched state returns 400 and the legitimate browser callback
      can still win the resume,
    - missing state with expectedState set is rejected,
    - no expectedState configured = back-compat (any state accepted).
  Tests drive the actual local server with URLSession so the route
  handler logic is exercised end to end.

- Update WebServerBox comment to reference SwiftWebServer 0.2.0
  (Claude review minor). The @unchecked Sendable workaround is still
  needed against 0.2.0; a follow-up will remove it once SwiftWebServer
  0.3.0 ships with @mainactor.

- Replace try! KeychainTokenStorage() in the DocC overview with a
  throwing init {} pattern so the snippet doesn't model a
  crash-on-misconfiguration that copy-paste readers might inherit
  (Claude minor / Copilot 3).

- Wrap the doc-comment snippets in AuthService and KeychainTokenStorage
  in do/catch so the new throwing init is shown with explicit error
  handling rather than a bare try in an implied throwing context
  (Copilot 1, 2).
@atom2ueki
Copy link
Copy Markdown
Owner Author

Re: @claude review — fix-up pushed in 02327cc.

Finding Status Where
Medium — no tests for expectedState Fixed 4 new unit tests in LocalCallbackServerTests.swift driving the actual local server with URLSession. The forge-then-genuine test directly proves the genuine callback wins after a 400-rejected forgery.
Minor — stale WebServerBox 0.1.0 comment Partly fixed Updated to read 0.2.0. The wrapper itself will be removed in a follow-up commit on this branch once SwiftWebServer 0.3.0 lands (PR atom2ueki/SwiftWebServer#5@MainActor for honest Sendable conformance).
Minor — try! in DocC overview Fixed Replaced with a throwing init on the SwiftUI sample so misconfigured hosts surface a clear failure instead of a startup crash.
Informational — resolveListenPort reserves on INADDR_ANY Acknowledged No action — the reservation socket is closed before listen() runs, so the brief INADDR_ANY bind has no exposure.

Test count: 49 → 53. Will follow up with the WebServerBox removal commit once SwiftWebServer 0.3.0 ships.

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @atom2ueki's task in 1m 52s —— View job


Re-review: fix-up commit 02327cc

  • Gather context and understand request
  • Read fix-up changes (02327cc)
  • Verify: 4 new expectedState unit tests
  • Verify: stale WebServerBox comment updated to 0.2.0
  • Verify: try! replaced in DocC overview
  • Verify: doc snippet do/catch wrapping in AuthService + KeychainTokenStorage
  • Post findings

All four findings from the previous review are correctly addressed. Details below.

Medium — expectedState test coverage ✅ Fully resolved

Four tests added in LocalCallbackServerTests.swift:40-135, all driving the actual server via URLSession:

Test What it proves
callbackWithMatchingStateCompletesFlow Happy path: correct state + code → 200, start() returns the right params
callbackWithMismatchedStateRejectsAndKeepsResumeAlive The critical security test. Forged callback → 400, genuine callback immediately after → 200, params.code == "genuine" (not "stolen") — directly proves the single-shot resume is preserved
callbackWithMissingStateRejectsWhenExpectedStateSet No state query param → 400; server correctly stopped to avoid a hang
callbackWithoutExpectedStateAcceptsAnyState Back-compat: unconfigured expectedState → any state accepted

Logic and cleanup in each test is correct. The callbackGET helper targets 127.0.0.1 directly, which is correct since the server is bound to localhost (IPv4 loopback).

One cosmetic note: the callbackGET comment says "Don't follow redirects" but no redirect-prevention delegate is configured on URLSession.shared. This is harmless because the tested responses are 200/400 (not 3xx), but the comment could mislead a future reader. Worth a one-line fix at some point, not a blocker.

Minor — Stale WebServerBox comment ✅ Resolved

LocalCallbackServer.swift:19 now reads SwiftWebServer 0.2.0 still does not declare itself Sendable. The @unchecked Sendable justification is accurate for 0.2.0 and the follow-up plan (remove wrapper once 0.3.0 ships @MainActor) is documented in the commit message.

Minor — try! in DocC overview ✅ Resolved

CodingPlanAuth.md:28-40: the stored-property try! pattern is replaced with an init() throws that propagates the error. The inline comment explaining why the throw is safe for normal app targets (but still surfaces for CLI/test hosts) is a nice touch for copy-paste readers.

Minor (Copilot) — do/catch in doc-comment snippets ✅ Resolved

Both AuthService.swift and KeychainTokenStorage.swift doc snippets now wrap the throwing call in do { ... } catch { ... }. Syntactically valid Swift and models the correct error-handling pattern.


Verdict

All review items resolved. The PR is clean from a security-correctness and test-coverage perspective. The only remaining item before tagging a release is the manual smoke test called out in the PR checklist.
| Branch: security/fix-loopback-bind

SwiftWebServer 0.3.0 ships @mainactor isolation on the SwiftWebServer
class, which makes it Sendable on its own — the WebServerBox
@unchecked Sendable wrapper this SDK was carrying since 0.1.0 is no
longer needed. Remove it; store SwiftWebServer directly on
LocalCallbackServer.

The constructor and route registration are now @mainactor, so
makeServer() is annotated @mainactor and called via the LocalCallback
actor's await hop. The immutable configuration fields the closure reads
(responseHTML, redirectBaseURL, expectedState) are now `nonisolated
private let` so the @mainactor helper can capture them without further
hops.

Mark the registered route handler closure @sendable. Without this,
Swift 6 strict concurrency infers the closure as @mainactor (since
server.get is @mainactor) and the runtime traps with
"BUG IN CLIENT OF libdispatch" when SwiftWebServer's Connection
invokes the handler from its background queue. @sendable forces
non-isolation, matching how Connection actually calls it.

53/53 tests pass.
@atom2ueki
Copy link
Copy Markdown
Owner Author

Follow-up: SwiftWebServer 0.3.0 just shipped (https://github.com/atom2ueki/SwiftWebServer/releases/tag/0.3.0) with @MainActor isolation for honest Sendable conformance, so the WebServerBox @unchecked Sendable wrapper this SDK was carrying since 0.1.0 is no longer needed.

Pushed 9bc5530:

  • Bumped Package.swift dep to from: "0.3.0".
  • Deleted WebServerBox; private var server: SwiftWebServer? directly.
  • makeServer() is now @MainActor (since SwiftWebServer.init/get are @MainActor); the immutable config fields it reads (responseHTML, redirectBaseURL, expectedState) are now nonisolated private let.
  • Marked the registered route handler closure @Sendable — without this, Swift 6 strict concurrency was inferring it as @MainActor (since server.get is @MainActor), and SwiftWebServer's Connection traps with "BUG IN CLIENT OF libdispatch" when it invokes the handler from its background queue. @Sendable forces non-isolation, matching how Connection actually calls it. Caught this via a SIGTRAP in callbackWithoutExpectedStateAcceptsAnyState; full backtrace pointed to _swift_task_checkIsolatedSwiftdispatch_assert_queue from inside the closure.

53/53 tests pass.

@atom2ueki
Copy link
Copy Markdown
Owner Author

@claude review this PR

@atom2ueki
Copy link
Copy Markdown
Owner Author

@codex review

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @atom2ueki's task in 2m 6s —— View job


PR Review: fix(auth): bind OAuth callback to loopback + harden keychain storage

  • Gather context and understand request
  • Read all changed files
  • Analyze security + correctness of changes
  • Verify 9bc5530 (@mainactor / @sendable / WebServerBox removal)
  • Post review feedback

All prior findings have been resolved. This review covers the final commit 9bc5530 (SwiftWebServer 0.3.0 + WebServerBox removal) which hadn't been reviewed yet.

9bc5530: @mainactor / @sendable / WebServerBox removal ✅

private var server: SwiftWebServer? — The WebServerBox @unchecked Sendable shim is gone. SwiftWebServer 0.3.0's @MainActor isolation gives it honest Sendable conformance, so the wrapper's entire purpose is moot. ✅

nonisolated private let responseHTML/redirectBaseURL/expectedState — Promoting the immutable config fields to nonisolated is the correct solution for reading actor-owned data inside @MainActor makeServer(). The compiler enforces the immutability guarantee; no locks needed. ✅

@MainActor private func makeServer() -> SwiftWebServer — Required because SwiftWebServer.init and server.get are @MainActor in 0.3.0. The callsite (let server = await makeServer()) hops correctly from the actor to the main actor and back. ✅

server.get(callbackPath) { @Sendable [weak self] ... } — This is the critical correctness fix in the commit. Without @Sendable, Swift 6 infers @MainActor isolation on the closure (it inherits from server.get's @MainActor context). SwiftWebServer invokes route handlers from a per-connection background dispatch queue, so the mismatch triggers dispatch_assert_queue → SIGTRAP at runtime. @Sendable forces non-isolation, matching how Connection actually calls it. The comment documenting this reasoning is valuable. ✅

stop()await MainActor.run { currentServer.close() }SwiftWebServer.close() is @MainActor, so the explicit hop is required. server = nil is cleared before the await, so no other actor-isolated code can observe the stale reference during the hop. ✅

callbackPath in makeServer()callbackPath is nonisolated let (line 22), so reading it from the @MainActor method is safe. ✅


Remaining cosmetic item (not a blocker)

LocalCallbackServerTests.swift:141 — the callbackGET comment still says "Don't follow redirects" but URLSession.shared has no redirect-prevention delegate configured. The tested responses are 200/400 (not 3xx), so this is harmless, but the comment could mislead a future reader who wonders what mechanism prevents redirect-following. Either remove the comment or configure a URLSession with redirectMaximum: 0 (or similar). This was flagged in the previous review pass and remains unchanged — fine to address in a follow-up. Fix this →


Full checklist

Loopback bind (RFC 8252 §7.3) ✅ Correct
State CSRF guard ✅ Logic correct, 4 tests covering all cases
Retry loop dual-stack fix ✅ Correct
Test port reservation fix (INADDR_LOOPBACK) ✅ Correct
KeychainTokenStorage fail-fast ✅ Correct
AuthService storage default removed ✅ Correct
@unchecked Sendable wrapper removed ✅ Correct (0.3.0 has honest @MainActor isolation)
@MainActor makeServer() ✅ Correct
@Sendable route handler closure ✅ Critical fix; correctly prevents libdispatch assert
nonisolated config fields ✅ Correct
DocC examples updated ✅ No try!, proper do/catch shown
Manual smoke test ⬜ Not yet (noted in PR checklist)

Verdict: All review items resolved across the three-commit series. The PR is clean — security-correct, test-covered, and Swift 6 strict-concurrency-clean. Ready to merge once the manual smoke test is green.
| Branch: security/fix-loopback-bind

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Claude review noted the comment is inaccurate — `URLSession.shared` has
no redirect-prevention delegate configured, so it would follow a 3xx if
the route ever returned one. The tested responses are 200/400 only, so
the comment was never load-bearing; just removing it rather than
configuring a redirect-blocking session, which would be ceremony with
no behavior change.
@atom2ueki
Copy link
Copy Markdown
Owner Author

Cosmetic fix pushed in 5e73c10 — dropped the misleading 'Don't follow redirects' comment in the test helper.

PR is ready from my side. Awaiting your manual smoke test of a real OAuth login on iOS/macOS before merging — that's the one thing I can't validate from here. Once you've confirmed the live flow still works, give the green light and I'll merge + tag the SDK release.

@atom2ueki atom2ueki merged commit 172434e into main May 1, 2026
3 of 4 checks passed
@atom2ueki atom2ueki deleted the security/fix-loopback-bind branch May 1, 2026 11:35
atom2ueki added a commit that referenced this pull request May 1, 2026
- README Acknowledgements: list SwiftWebServer (built on) and the four
  projects that inspired this work — openai/codex, sugarforever/rn-ai-kit,
  Vercel AI SDK, badlogic/pi-mono.

- README Quick Start: the example previously showed
  `AuthService(storage: KeychainTokenStorage())`, which no longer compiles
  after #6 made `KeychainTokenStorage.init` throwing and removed
  `AuthService.init`'s default storage. Move construction into a throwing
  `init() throws` on the SignIn class with an explanatory comment
  matching the DocC sample.

- llms.txt: tighten the KeychainTokenStorage description to mention the
  throwing init and why it traps misconfigured hosts.
atom2ueki added a commit that referenced this pull request May 1, 2026
#7)

- README Acknowledgements: list SwiftWebServer (built on) and the four
  projects that inspired this work — openai/codex, sugarforever/rn-ai-kit,
  Vercel AI SDK, badlogic/pi-mono.

- README Quick Start: the example previously showed
  `AuthService(storage: KeychainTokenStorage())`, which no longer compiles
  after #6 made `KeychainTokenStorage.init` throwing and removed
  `AuthService.init`'s default storage. Move construction into a throwing
  `init() throws` on the SignIn class with an explanatory comment
  matching the DocC sample.

- llms.txt: tighten the KeychainTokenStorage description to mention the
  throwing init and why it traps misconfigured hosts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants