From 437c3b2da04336a16f2b7e979b48e862a45536cb Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 5 Nov 2025 12:45:21 -0800 Subject: [PATCH] set config via feature flags (#6590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In cloud environment, allow all the config values to be set by the feature flag endpoint and be updated dynamically (on 30s poll). Retain origianl behavior for OSS. On cloud, config changes shouldn't be changed via redeploy and the promoted image should match the staging image. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6590-set-config-via-feature-flags-2a26d73d3650819f8084eb2695c51f22) by [Unito](https://www.unito.io) --------- Co-authored-by: DrJKL --- global.d.ts | 14 ++++++ .../dialog/content/SignInContent.vue | 17 +++++-- .../dialog/content/signin/ApiKeyForm.test.ts | 4 +- .../dialog/content/signin/ApiKeyForm.vue | 15 +++++- src/config/comfyApi.ts | 48 ++++++++++++++++--- src/config/firebase.ts | 22 +++++++-- src/main.ts | 4 +- .../composables/useSubscription.ts | 10 ++-- src/platform/remoteConfig/remoteConfig.ts | 9 ++++ src/platform/remoteConfig/types.ts | 15 ++++++ src/platform/updates/common/releaseService.ts | 25 ++++++---- src/services/customerEventsService.ts | 13 +++-- src/stores/firebaseAuthStore.ts | 12 +++-- .../subscription/useSubscription.test.ts | 2 +- vitest.setup.ts | 33 ++++++++++++- 15 files changed, 200 insertions(+), 43 deletions(-) diff --git a/global.d.ts b/global.d.ts index 059e47732..7f7dd832f 100644 --- a/global.d.ts +++ b/global.d.ts @@ -8,7 +8,21 @@ declare const __USE_PROD_CONFIG__: boolean interface Window { __CONFIG__: { mixpanel_token?: string + require_whitelist?: boolean subscription_required?: boolean + max_upload_size?: number + comfy_api_base_url?: string + comfy_platform_base_url?: string + firebase_config?: { + apiKey: string + authDomain: string + databaseURL?: string + projectId: string + storageBucket: string + messagingSenderId: string + appId: string + measurementId?: string + } server_health_alert?: { message: string tooltip?: string diff --git a/src/components/dialog/content/SignInContent.vue b/src/components/dialog/content/SignInContent.vue index 19da83125..0cc9869c4 100644 --- a/src/components/dialog/content/SignInContent.vue +++ b/src/components/dialog/content/SignInContent.vue @@ -96,7 +96,7 @@ {{ t('auth.apiKey.helpText') }} @@ -145,11 +145,15 @@ import Button from 'primevue/button' import Divider from 'primevue/divider' import Message from 'primevue/message' -import { onMounted, onUnmounted, ref } from 'vue' +import { computed, onMounted, onUnmounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' -import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' +import { getComfyPlatformBaseUrl } from '@/config/comfyApi' +import { + configValueOrDefault, + remoteConfig +} from '@/platform/remoteConfig/remoteConfig' import type { SignInData, SignUpData } from '@/schemas/signInSchema' import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist' import { isInChina } from '@/utils/networkUtil' @@ -168,6 +172,13 @@ const isSecureContext = window.isSecureContext const isSignIn = ref(true) const showApiKeyForm = ref(false) const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname)) +const comfyPlatformBaseUrl = computed(() => + configValueOrDefault( + remoteConfig.value, + 'comfy_platform_base_url', + getComfyPlatformBaseUrl() + ) +) const toggleState = () => { isSignIn.value = !isSignIn.value diff --git a/src/components/dialog/content/signin/ApiKeyForm.test.ts b/src/components/dialog/content/signin/ApiKeyForm.test.ts index bf1ec2cdd..f5ba33411 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.test.ts +++ b/src/components/dialog/content/signin/ApiKeyForm.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createApp } from 'vue' import { createI18n } from 'vue-i18n' -import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' +import { getComfyPlatformBaseUrl } from '@/config/comfyApi' import ApiKeyForm from './ApiKeyForm.vue' @@ -111,7 +111,7 @@ describe('ApiKeyForm', () => { const helpText = wrapper.find('small') expect(helpText.text()).toContain('Need an API key?') expect(helpText.find('a').attributes('href')).toBe( - `${COMFY_PLATFORM_BASE_URL}/login` + `${getComfyPlatformBaseUrl()}/login` ) }) }) diff --git a/src/components/dialog/content/signin/ApiKeyForm.vue b/src/components/dialog/content/signin/ApiKeyForm.vue index 2e70345fc..027f35b6d 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.vue +++ b/src/components/dialog/content/signin/ApiKeyForm.vue @@ -48,7 +48,7 @@ {{ t('auth.apiKey.helpText') }} @@ -88,7 +88,11 @@ import Message from 'primevue/message' import { computed } from 'vue' import { useI18n } from 'vue-i18n' -import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' +import { getComfyPlatformBaseUrl } from '@/config/comfyApi' +import { + configValueOrDefault, + remoteConfig +} from '@/platform/remoteConfig/remoteConfig' import { apiKeySchema } from '@/schemas/signInSchema' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' @@ -96,6 +100,13 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' const authStore = useFirebaseAuthStore() const apiKeyStore = useApiKeyAuthStore() const loading = computed(() => authStore.loading) +const comfyPlatformBaseUrl = computed(() => + configValueOrDefault( + remoteConfig.value, + 'comfy_platform_base_url', + getComfyPlatformBaseUrl() + ) +) const { t } = useI18n() diff --git a/src/config/comfyApi.ts b/src/config/comfyApi.ts index 272708264..8efd651bf 100644 --- a/src/config/comfyApi.ts +++ b/src/config/comfyApi.ts @@ -1,7 +1,43 @@ -export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__ - ? 'https://api.comfy.org' - : 'https://stagingapi.comfy.org' +import { isCloud } from '@/platform/distribution/types' +import { + configValueOrDefault, + remoteConfig +} from '@/platform/remoteConfig/remoteConfig' -export const COMFY_PLATFORM_BASE_URL = __USE_PROD_CONFIG__ - ? 'https://platform.comfy.org' - : 'https://stagingplatform.comfy.org' +const PROD_API_BASE_URL = 'https://api.comfy.org' +const STAGING_API_BASE_URL = 'https://stagingapi.comfy.org' + +const PROD_PLATFORM_BASE_URL = 'https://platform.comfy.org' +const STAGING_PLATFORM_BASE_URL = 'https://stagingplatform.comfy.org' + +const BUILD_TIME_API_BASE_URL = __USE_PROD_CONFIG__ + ? PROD_API_BASE_URL + : STAGING_API_BASE_URL + +const BUILD_TIME_PLATFORM_BASE_URL = __USE_PROD_CONFIG__ + ? PROD_PLATFORM_BASE_URL + : STAGING_PLATFORM_BASE_URL + +export function getComfyApiBaseUrl(): string { + if (!isCloud) { + return BUILD_TIME_API_BASE_URL + } + + return configValueOrDefault( + remoteConfig.value, + 'comfy_api_base_url', + BUILD_TIME_API_BASE_URL + ) +} + +export function getComfyPlatformBaseUrl(): string { + if (!isCloud) { + return BUILD_TIME_PLATFORM_BASE_URL + } + + return configValueOrDefault( + remoteConfig.value, + 'comfy_platform_base_url', + BUILD_TIME_PLATFORM_BASE_URL + ) +} diff --git a/src/config/firebase.ts b/src/config/firebase.ts index e5fbd2af7..e976a2ef6 100644 --- a/src/config/firebase.ts +++ b/src/config/firebase.ts @@ -1,5 +1,8 @@ import type { FirebaseOptions } from 'firebase/app' +import { isCloud } from '@/platform/distribution/types' +import { remoteConfig } from '@/platform/remoteConfig/remoteConfig' + const DEV_CONFIG: FirebaseOptions = { apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE', authDomain: 'dreamboothy-dev.firebaseapp.com', @@ -22,7 +25,18 @@ const PROD_CONFIG: FirebaseOptions = { measurementId: 'G-3ZBD3MBTG4' } -// To test with prod config while using dev server, set USE_PROD_CONFIG=true in .env -export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_CONFIG__ - ? PROD_CONFIG - : DEV_CONFIG +const BUILD_TIME_CONFIG = __USE_PROD_CONFIG__ ? PROD_CONFIG : DEV_CONFIG + +/** + * Returns the Firebase configuration for the current environment. + * - Cloud builds use runtime configuration delivered via feature flags + * - OSS / localhost builds fall back to the build-time config determined by __USE_PROD_CONFIG__ + */ +export function getFirebaseConfig(): FirebaseOptions { + if (!isCloud) { + return BUILD_TIME_CONFIG + } + + const runtimeConfig = remoteConfig.value.firebase_config + return runtimeConfig ?? BUILD_TIME_CONFIG +} diff --git a/src/main.ts b/src/main.ts index e063bc18c..659198d53 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ import Tooltip from 'primevue/tooltip' import { createApp } from 'vue' import { VueFire, VueFireAuth } from 'vuefire' -import { FIREBASE_CONFIG } from '@/config/firebase' +import { getFirebaseConfig } from '@/config/firebase' import '@/lib/litegraph/public/css/litegraph.css' import router from '@/router' @@ -40,7 +40,7 @@ const ComfyUIPreset = definePreset(Aura, { } }) -const firebaseApp = initializeApp(FIREBASE_CONFIG) +const firebaseApp = initializeApp(getFirebaseConfig()) const app = createApp(App) const pinia = createPinia() diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 92588907f..b622cb97a 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -4,7 +4,7 @@ import { createSharedComposable } from '@vueuse/core' import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useErrorHandling } from '@/composables/useErrorHandling' -import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi' import { MONTHLY_SUBSCRIPTION_PRICE } from '@/config/subscriptionPricesConfig' import { t } from '@/i18n' import { isCloud } from '@/platform/distribution/types' @@ -74,6 +74,8 @@ function useSubscriptionInternal() { () => `$${MONTHLY_SUBSCRIPTION_PRICE.toFixed(0)}` ) + const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + const fetchStatus = wrapWithErrorHandlingAsync( fetchSubscriptionStatus, reportError @@ -114,7 +116,7 @@ function useSubscriptionInternal() { } const handleViewUsageHistory = () => { - window.open('https://platform.comfy.org/profile/usage', '_blank') + window.open(`${getComfyPlatformBaseUrl()}/profile/usage`, '_blank') } const handleLearnMore = () => { @@ -136,7 +138,7 @@ function useSubscriptionInternal() { } const response = await fetch( - `${COMFY_API_BASE_URL}/customers/cloud-subscription-status`, + buildApiUrl('/customers/cloud-subscription-status'), { headers: { ...authHeader, @@ -181,7 +183,7 @@ function useSubscriptionInternal() { } const response = await fetch( - `${COMFY_API_BASE_URL}/customers/cloud-subscription-checkout`, + buildApiUrl('/customers/cloud-subscription-checkout'), { method: 'POST', headers: { diff --git a/src/platform/remoteConfig/remoteConfig.ts b/src/platform/remoteConfig/remoteConfig.ts index a20eb7ebd..7d8f28c5f 100644 --- a/src/platform/remoteConfig/remoteConfig.ts +++ b/src/platform/remoteConfig/remoteConfig.ts @@ -21,6 +21,15 @@ import type { RemoteConfig } from './types' */ export const remoteConfig = ref({}) +export function configValueOrDefault( + remoteConfig: RemoteConfig, + key: K, + defaultValue: NonNullable +): NonNullable { + const configValue = remoteConfig[key] + return configValue || defaultValue +} + /** * Loads remote configuration from the backend /api/features endpoint * and updates the reactive remoteConfig ref diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index 939c6bbf1..43bfbd70f 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -8,6 +8,17 @@ type ServerHealthAlert = { badge?: string } +type FirebaseRuntimeConfig = { + apiKey: string + authDomain: string + databaseURL?: string + projectId: string + storageBucket: string + messagingSenderId: string + appId: string + measurementId?: string +} + /** * Remote configuration type * Configuration fetched from the server at runtime @@ -16,4 +27,8 @@ export type RemoteConfig = { mixpanel_token?: string subscription_required?: boolean server_health_alert?: ServerHealthAlert + max_upload_size?: number + comfy_api_base_url?: string + comfy_platform_base_url?: string + firebase_config?: FirebaseRuntimeConfig } diff --git a/src/platform/updates/common/releaseService.ts b/src/platform/updates/common/releaseService.ts index 189421678..6b9bfceed 100644 --- a/src/platform/updates/common/releaseService.ts +++ b/src/platform/updates/common/releaseService.ts @@ -1,18 +1,11 @@ import type { AxiosError, AxiosResponse } from 'axios' import axios from 'axios' -import { ref } from 'vue' +import { ref, watch } from 'vue' -import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { getComfyApiBaseUrl } from '@/config/comfyApi' import type { components, operations } from '@/types/comfyRegistryTypes' import { isAbortError } from '@/utils/typeGuardUtil' -const releaseApiClient = axios.create({ - baseURL: COMFY_API_BASE_URL, - headers: { - 'Content-Type': 'application/json' - } -}) - // Use generated types from OpenAPI spec export type ReleaseNote = components['schemas']['ReleaseNote'] type GetReleasesParams = operations['getReleaseNotes']['parameters']['query'] @@ -20,11 +13,25 @@ type GetReleasesParams = operations['getReleaseNotes']['parameters']['query'] // Use generated error response type type ErrorResponse = components['schemas']['ErrorResponse'] +const releaseApiClient = axios.create({ + baseURL: getComfyApiBaseUrl(), + headers: { + 'Content-Type': 'application/json' + } +}) + // Release service for fetching release notes export const useReleaseService = () => { const isLoading = ref(false) const error = ref(null) + watch( + () => getComfyApiBaseUrl(), + (url) => { + releaseApiClient.defaults.baseURL = url + } + ) + // No transformation needed - API response matches the generated type // Handle API errors with context diff --git a/src/services/customerEventsService.ts b/src/services/customerEventsService.ts index 04d0f1d66..830b2fc2d 100644 --- a/src/services/customerEventsService.ts +++ b/src/services/customerEventsService.ts @@ -1,9 +1,9 @@ import type { AxiosError, AxiosResponse } from 'axios' import axios from 'axios' -import { ref } from 'vue' +import { ref, watch } from 'vue' import { useI18n } from 'vue-i18n' -import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { getComfyApiBaseUrl } from '@/config/comfyApi' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import type { components, operations } from '@/types/comfyRegistryTypes' import { isAbortError } from '@/utils/typeGuardUtil' @@ -24,7 +24,7 @@ type CustomerEventsResponseQuery = export type AuditLog = components['schemas']['AuditLog'] const customerApiClient = axios.create({ - baseURL: COMFY_API_BASE_URL, + baseURL: getComfyApiBaseUrl(), headers: { 'Content-Type': 'application/json' } @@ -35,6 +35,13 @@ export const useCustomerEventsService = () => { const error = ref(null) const { d } = useI18n() + watch( + () => getComfyApiBaseUrl(), + (url) => { + customerApiClient.defaults.baseURL = url + } + ) + const handleRequestError = ( err: unknown, context: string, diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 08e441925..1c73539a9 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -21,7 +21,7 @@ import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { useFirebaseAuth } from 'vuefire' -import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { getComfyApiBaseUrl } from '@/config/comfyApi' import { t } from '@/i18n' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' @@ -65,6 +65,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { // Token refresh trigger - increments when token is refreshed const tokenRefreshTrigger = ref(0) + const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + // Providers const googleProvider = new GoogleAuthProvider() googleProvider.addScope('email') @@ -163,7 +165,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { ) } - const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, { + const response = await fetch(buildApiUrl('/customers/balance'), { headers: { ...authHeader, 'Content-Type': 'application/json' @@ -199,7 +201,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } - const createCustomerRes = await fetch(`${COMFY_API_BASE_URL}/customers`, { + const createCustomerRes = await fetch(buildApiUrl('/customers'), { method: 'POST', headers: { ...authHeader, @@ -367,7 +369,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { customerCreated.value = true } - const response = await fetch(`${COMFY_API_BASE_URL}/customers/credit`, { + const response = await fetch(buildApiUrl('/customers/credit'), { method: 'POST', headers: { ...authHeader, @@ -401,7 +403,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } - const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, { + const response = await fetch(buildApiUrl('/customers/billing'), { method: 'POST', headers: { ...authHeader, diff --git a/tests-ui/tests/platform/cloud/subscription/useSubscription.test.ts b/tests-ui/tests/platform/cloud/subscription/useSubscription.test.ts index ec356cfc3..e113e626e 100644 --- a/tests-ui/tests/platform/cloud/subscription/useSubscription.test.ts +++ b/tests-ui/tests/platform/cloud/subscription/useSubscription.test.ts @@ -283,7 +283,7 @@ describe('useSubscription', () => { handleViewUsageHistory() expect(windowOpenSpy).toHaveBeenCalledWith( - 'https://platform.comfy.org/profile/usage', + 'https://stagingplatform.comfy.org/profile/usage', '_blank' ) diff --git a/vitest.setup.ts b/vitest.setup.ts index e3427e7c6..137b50db6 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -6,8 +6,27 @@ declare global { interface Window { __CONFIG__: { mixpanel_token?: string + require_whitelist?: boolean subscription_required?: boolean - server_health_alert?: string + max_upload_size?: number + comfy_api_base_url?: string + comfy_platform_base_url?: string + firebase_config?: { + apiKey: string + authDomain: string + databaseURL?: string + projectId: string + storageBucket: string + messagingSenderId: string + appId: string + measurementId?: string + } + server_health_alert?: { + message: string + tooltip?: string + severity?: 'info' | 'warning' | 'error' + badge?: string + } } } } @@ -24,7 +43,17 @@ globalThis.__DISTRIBUTION__ = 'localhost' // Define runtime config for tests window.__CONFIG__ = { subscription_required: true, - mixpanel_token: 'test-token' + mixpanel_token: 'test-token', + comfy_api_base_url: 'https://stagingapi.comfy.org', + comfy_platform_base_url: 'https://stagingplatform.comfy.org', + firebase_config: { + apiKey: 'test', + authDomain: 'test.firebaseapp.com', + projectId: 'test', + storageBucket: 'test.appspot.com', + messagingSenderId: '123', + appId: '123' + } } // Mock Worker for extendable-media-recorder