From 514425b560b4b2db133d8e8a1fda10885d8cc3cf Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 23 Feb 2026 18:49:32 -0800 Subject: [PATCH] fix: use getAuthHeader for API key auth in subscription/billing (#9142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix "User not authenticated" errors when API key users (desktop/portable) trigger subscription status checks or billing operations. ## Changes - **What**: Replace `getFirebaseAuthHeader()` with `getAuthHeader()` in subscription and billing call sites (`fetchSubscriptionStatus`, `initiateSubscriptionCheckout`, `fetchBalance`, `addCredits`, `accessBillingPortal`, `performSubscriptionCheckout`). `getAuthHeader()` supports the full auth fallback chain (workspace token → Firebase token → API key), whereas `getFirebaseAuthHeader()` returns null for API key users since they bypass Firebase entirely. Also add an `isCloud` guard to the subscription status watcher so non-cloud environments skip subscription checks. ## Review Focus - The `isCloud` guard on the watcher ensures local/desktop users never hit the subscription endpoint. This was the originally intended design per code owner confirmation. - `getAuthHeader()` already exists in `firebaseAuthStore` with proper fallback logic — no new auth code was added. Fixes https://www.notion.so/comfy-org/Bug-Subscription-status-check-occurring-in-non-cloud-environments-causing-authentication-errors-3116d73d365081738b21db157e88a9ed ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9142-fix-use-getAuthHeader-for-API-key-auth-in-subscription-billing-3116d73d3650817fa345deaddc8c3fcd) by [Unito](https://www.unito.io) --- .../components/PricingTable.test.ts | 4 ++-- .../composables/useSubscription.test.ts | 23 ++++++++++++++++++- .../composables/useSubscription.ts | 8 +++---- .../utils/subscriptionCheckoutUtil.test.ts | 2 +- .../utils/subscriptionCheckoutUtil.ts | 2 +- src/stores/firebaseAuthStore.ts | 6 ++--- 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/platform/cloud/subscription/components/PricingTable.test.ts b/src/platform/cloud/subscription/components/PricingTable.test.ts index 1d24c7d130..cb6e590602 100644 --- a/src/platform/cloud/subscription/components/PricingTable.test.ts +++ b/src/platform/cloud/subscription/components/PricingTable.test.ts @@ -16,7 +16,7 @@ const mockAccessBillingPortal = vi.fn() const mockReportError = vi.fn() const mockTrackBeginCheckout = vi.fn() const mockUserId = ref('user-123') -const mockGetFirebaseAuthHeader = vi.fn(() => +const mockGetAuthHeader = vi.fn(() => Promise.resolve({ Authorization: 'Bearer test-token' }) ) const mockGetCheckoutAttribution = vi.hoisted(() => vi.fn(() => ({}))) @@ -58,7 +58,7 @@ vi.mock('@/composables/useErrorHandling', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: () => reactive({ - getFirebaseAuthHeader: mockGetFirebaseAuthHeader, + getAuthHeader: mockGetAuthHeader, userId: computed(() => mockUserId.value) }), FirebaseAuthStoreError: class extends Error {} diff --git a/src/platform/cloud/subscription/composables/useSubscription.test.ts b/src/platform/cloud/subscription/composables/useSubscription.test.ts index 48913f1dbd..f4375fce78 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.test.ts @@ -108,7 +108,7 @@ vi.mock('@/services/dialogService', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => ({ - getFirebaseAuthHeader: mockGetAuthHeader, + getAuthHeader: mockGetAuthHeader, get userId() { return mockUserId.value } @@ -363,6 +363,27 @@ describe('useSubscription', () => { }) }) + describe('non-cloud environments', () => { + it('should not fetch subscription status when not on cloud', async () => { + mockIsCloud.value = false + mockIsLoggedIn.value = true + + useSubscriptionWithScope() + + await vi.dynamicImportSettled() + + expect(global.fetch).not.toHaveBeenCalled() + }) + + it('should report isActiveSubscription as true when not on cloud', () => { + mockIsCloud.value = false + + const { isActiveSubscription } = useSubscriptionWithScope() + + expect(isActiveSubscription.value).toBe(true) + }) + }) + describe('action handlers', () => { it('should open usage history URL', () => { const windowOpenSpy = vi diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 5f026ce782..5189d13087 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -40,7 +40,7 @@ function useSubscriptionInternal() { const { showSubscriptionRequiredDialog } = useDialogService() const firebaseAuthStore = useFirebaseAuthStore() - const { getFirebaseAuthHeader } = firebaseAuthStore + const { getAuthHeader } = firebaseAuthStore const { wrapWithErrorHandlingAsync } = useErrorHandling() const { isLoggedIn } = useCurrentUser() @@ -184,7 +184,7 @@ function useSubscriptionInternal() { * @returns Subscription status or null if no subscription exists */ async function fetchSubscriptionStatus(): Promise { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } @@ -217,7 +217,7 @@ function useSubscriptionInternal() { watch( () => isLoggedIn.value, async (loggedIn) => { - if (loggedIn) { + if (loggedIn && isCloud) { try { await fetchSubscriptionStatus() } catch (error) { @@ -238,7 +238,7 @@ function useSubscriptionInternal() { const initiateSubscriptionCheckout = async (): Promise => { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError( t('toastMessages.userNotAuthenticated') diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.ts index 06f769ff88..8386a469ca 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.ts @@ -39,7 +39,7 @@ vi.mock('@/platform/telemetry', () => ({ vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => reactive({ - getFirebaseAuthHeader: mockGetAuthHeader, + getAuthHeader: mockGetAuthHeader, userId: computed(() => mockUserId.value) }) ), diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts index 3494a8c4a9..1982a4d4f9 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts @@ -54,7 +54,7 @@ export async function performSubscriptionCheckout( const firebaseAuthStore = useFirebaseAuthStore() const { userId } = storeToRefs(firebaseAuthStore) const telemetry = useTelemetry() - const authHeader = await firebaseAuthStore.getFirebaseAuthHeader() + const authHeader = await firebaseAuthStore.getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index c09380c237..bdc3569bcb 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -239,7 +239,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const fetchBalance = async (): Promise => { isFetchingBalance.value = true try { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError( t('toastMessages.userNotAuthenticated') @@ -435,7 +435,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const addCredits = async ( requestBodyContent: CreditPurchasePayload ): Promise => { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } @@ -475,7 +475,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const accessBillingPortal = async ( targetTier?: BillingPortalTargetTier ): Promise => { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) }