mirror of
https://github.com/Comfy-Org/comfy-swift-sdk.git
synced 2026-11-04 23:47:26 +00:00
[PR #32] [MERGED] feat(auth): ComfyTokenStore protocol + ComfyAuth.restoreClient / signOut #34
Labels
No labels
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-swift-sdk#34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-swift-sdk/pull/32
Author: @mattmillerai
Created: 7/6/2026
Status: ✅ Merged
Merged: 7/7/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-2450-token-store-restore-client📝 Commits (4)
c2ae295feat(auth): parameterize OAuth client config + add callback state-verification helperc1ca10cfix(auth): thread OAuthClientConfig through refresh + tighten callback checks5a005b2feat(auth): add ComfyTokenStore + ComfyAuth.restoreClient/signOut337be8afix(auth): normalize store read errors + redact token secrets📊 Changes
15 files changed (+991 additions, -69 deletions)
View changed files
📝
Sources/ComfySwiftSDK/Internal/Auth/OAuthConfiguration.swift(+4 -12)📝
Sources/ComfySwiftSDK/Internal/Auth/OAuthExchanger.swift(+7 -3)📝
Sources/ComfySwiftSDK/Internal/Auth/OAuthTokenRefreshExecutor.swift(+9 -2)📝
Sources/ComfySwiftSDK/Internal/PollingFallback.swift(+2 -2)📝
Sources/ComfySwiftSDK/Internal/SDKLog.swift(+2 -0)📝
Sources/ComfySwiftSDK/Internal/Transport.swift(+9 -2)➕
Sources/ComfySwiftSDK/Public/ComfyAuth.swift(+246 -0)📝
Sources/ComfySwiftSDK/Public/ComfyCloudClient.swift(+42 -17)📝
Sources/ComfySwiftSDK/Public/ComfyError.swift(+11 -0)📝
Sources/ComfySwiftSDK/Public/OAuthAuthorizationRequest.swift(+43 -1)➕
Sources/ComfySwiftSDK/Public/OAuthClientConfig.swift(+78 -0)➕
Tests/ComfySwiftSDKTests/ComfyAuthTests.swift(+323 -0)➕
Tests/ComfySwiftSDKTests/OAuthClientConfigTests.swift(+176 -0)📝
Tests/ComfySwiftSDKTests/OAuthConfigurationTests.swift(+2 -29)📝
Tests/ComfySwiftSDKTests/OAuthExchangeTests.swift(+37 -1)📄 Description
ELI-5
Signing an app into Comfy Cloud means holding three things — an access token, a refresh token, and when the access token expires — and re-wiring four little callbacks so the SDK can quietly refresh them. Every app was copy-pasting that same four-callback boilerplate over its own keychain. This PR moves it into the SDK: an app just hands the SDK a tiny "here's where I keep my tokens" object, and gets back a ready, self-refreshing client. Signing out becomes one call.
What & why
Replaces the copy-paste four-closure
.oauthRefreshablewiring with a single injectable token-store protocol. Generalizes the pattern that Comfy-iOS'sHomeViewModel.resolveCredential()hand-builds today.New public surface (all Foundation-only, DocC'd):
ComfyStoredTokens— aSendablestorage view of a token pair with an absoluteexpiresAt(vs. the wire shape's relativeexpiresIn), so a token reloaded on a later launch still knows when it lapses.ComfyTokenStore— aSendableload/save/clearpersistence protocol. Implement it over Keychain, an encrypted file, or (in tests) an in-memory box.ComfyAuth.restoreClient(store:config:)— returnsnilwhen the store holds no usable session (nil, or an empty access token); otherwise returns aComfyCloudClientin.oauthRefreshablemode whose four closures adapt the store:.authInvalidif missing/empty.authExpiredif missing/empty.unknown(underlying:), never.authInvalid(matches the existingOAuthTokenStorecontract, so an infra write error isn't misreported as an invalid credential)ComfyAuth.signOut(store:)— clears the store.Plus an internal
ComfyStoredTokens(response:now:)mapper (OAuthTokenResponse.expiresIn+ a reference instant → absoluteexpiresAt), so the expiry arithmetic lives in one tested place.Judgment call — sync
expiryProviderover an async storeThe SDK's
OAuthExpiryProvideris synchronous (() throws -> Date?), butComfyTokenStore.load()is async, so the expiry can't be read from the store on the proactive-refresh check. I bridge it with a small lock-guarded in-memory expiry cache: seeded from theload()that gatedrestoreClient, and updated by the token-store closure on every successful refresh-save. The alternative — returningnil— makes the SDK treat the token as always-expired and refresh before every request (a refresh storm). A stale cache only ever mis-times the proactive refresh; the 401-triggered refresh path remains the backstop, so it's safe either way.Stacked on the OAuth-client-config PR
This depends on the parameterized
OAuthClientConfig/.comfyIOSfrom #31 and is opened with #31 as its base, so the diff here is just the token-store glue. GitHub will retarget it tomainonce #31 merges.Testing
ComfyAuthTests(15 tests): an in-memoryComfyTokenStorefake;restoreClient→ nil when empty / nil on empty access token / rethrows a load failure / builds a client when populated; each of the four adapter closures (including the.unknown-not-.authInvalidsave-failure contract); the expiry cache seeds + updates after a refresh-save; theexpiresIn→expiresAtmapping (incl. zero);signOutclears the store.swift build, fullswift test(198 tests, all pass), andImportBoundaryTestsgreen. Foundation-only (noSecurity).🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.