diff --git a/eslint.config.ts b/eslint.config.ts index e4b151080..2a4d219a9 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -60,8 +60,6 @@ export default defineConfig([ '**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*', 'packages/registry-types/src/comfyRegistryTypes.ts', - 'public/auth-dev-sw.js', - 'public/auth-sw.js', 'src/extensions/core/*', 'src/scripts/*', 'src/types/generatedManagerTypes.ts', diff --git a/knip.config.ts b/knip.config.ts index 6f5487eba..3bc025cad 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -41,10 +41,7 @@ const config: KnipConfig = { 'src/workbench/extensions/manager/types/generatedManagerTypes.ts', 'packages/registry-types/src/comfyRegistryTypes.ts', // Used by a custom node (that should move off of this) - 'src/scripts/ui/components/splitButton.ts', - // Service workers - registered at runtime via navigator.serviceWorker.register() - 'public/auth-sw.js', - 'public/auth-dev-sw.js' + 'src/scripts/ui/components/splitButton.ts' ], compilers: { // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 diff --git a/public/auth-dev-sw.js b/public/auth-dev-sw.js deleted file mode 100644 index c721ba2ac..000000000 --- a/public/auth-dev-sw.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * @fileoverview Authentication Service Worker (Development Version) - * Intercepts /api/view requests and rewrites them to a configurable base URL with auth token. - * Required for browser-native requests (img, video, audio) that cannot send custom headers. - * This version is used in development to proxy requests to staging/test environments. - * Default base URL: https://testcloud.comfy.org (configurable via SET_BASE_URL message) - */ - -/** - * @typedef {Object} AuthHeader - * @property {string} Authorization - Bearer token for authentication - */ - -/** - * @typedef {Object} CachedAuth - * @property {AuthHeader|null} header - * @property {number} expiresAt - Timestamp when cache expires - */ - -const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour) - -/** @type {CachedAuth|null} */ -let authCache = null - -/** @type {Promise|null} */ -let authRequestInFlight = null - -/** @type {string} */ -let baseUrl = 'https://testcloud.comfy.org' - -self.addEventListener('message', (event) => { - if (event.data.type === 'INVALIDATE_AUTH_HEADER') { - authCache = null - authRequestInFlight = null - } - - if (event.data.type === 'SET_BASE_URL') { - baseUrl = event.data.baseUrl - console.log('[Auth DEV SW] Base URL set to:', baseUrl) - } -}) - -self.addEventListener('fetch', (event) => { - const url = new URL(event.request.url) - - if ( - !url.pathname.startsWith('/api/view') && - !url.pathname.startsWith('/api/viewvideo') - ) { - return - } - - event.respondWith( - (async () => { - try { - // Rewrite URL to use configured base URL (default: stagingcloud.comfy.org) - const originalUrl = new URL(event.request.url) - const rewrittenUrl = new URL( - originalUrl.pathname + originalUrl.search, - baseUrl - ) - - const authHeader = await getAuthHeader() - - // With mode: 'no-cors', Authorization headers are stripped by the browser - // So we add the token to the URL as a query parameter instead - if (authHeader && authHeader.Authorization) { - const token = authHeader.Authorization.replace('Bearer ', '') - rewrittenUrl.searchParams.set('token', token) - } - - // Cross-origin request requires no-cors mode - // - mode: 'no-cors' allows cross-origin fetches without CORS headers - // - Returns opaque response, which works fine for images/videos/audio - // - Auth token is sent via query parameter since headers are stripped in no-cors mode - // - Server may return redirect to GCS, which will be followed automatically - return fetch(rewrittenUrl, { - method: 'GET', - redirect: 'follow', - mode: 'no-cors' - }) - } catch (error) { - console.error('[Auth DEV SW] Request failed:', error) - const originalUrl = new URL(event.request.url) - const rewrittenUrl = new URL( - originalUrl.pathname + originalUrl.search, - baseUrl - ) - return fetch(rewrittenUrl, { - mode: 'no-cors', - redirect: 'follow' - }) - } - })() - ) -}) - -/** - * Gets auth header from cache or requests from main thread - * @returns {Promise} - */ -async function getAuthHeader() { - // Return cached value if valid - if (authCache && authCache.expiresAt > Date.now()) { - return authCache.header - } - - // Clear expired cache - if (authCache) { - authCache = null - } - - // Deduplicate concurrent requests - if (authRequestInFlight) { - return authRequestInFlight - } - - authRequestInFlight = requestAuthHeaderFromMainThread() - const header = await authRequestInFlight - authRequestInFlight = null - - // Cache the result - if (header) { - authCache = { - header, - expiresAt: Date.now() + CACHE_TTL_MS - } - } - - return header -} - -/** - * Requests auth header from main thread via MessageChannel - * @returns {Promise} - */ -async function requestAuthHeaderFromMainThread() { - const clients = await self.clients.matchAll() - if (clients.length === 0) { - return null - } - - const messageChannel = new MessageChannel() - - return new Promise((resolve) => { - let timeoutId - - messageChannel.port1.onmessage = (event) => { - clearTimeout(timeoutId) - resolve(event.data.authHeader) - } - - timeoutId = setTimeout(() => { - console.error( - '[Auth DEV SW] Timeout waiting for auth header from main thread' - ) - resolve(null) - }, 1000) - - clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [ - messageChannel.port2 - ]) - }) -} - -self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()) -}) diff --git a/public/auth-sw.js b/public/auth-sw.js deleted file mode 100644 index 2f21da21a..000000000 --- a/public/auth-sw.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @fileoverview Authentication Service Worker - * Intercepts /api/view requests and adds Firebase authentication headers. - * Required for browser-native requests (img, video, audio) that cannot send custom headers. - */ - -/** - * @typedef {Object} AuthHeader - * @property {string} Authorization - Bearer token for authentication - */ - -/** - * @typedef {Object} CachedAuth - * @property {AuthHeader|null} header - * @property {number} expiresAt - Timestamp when cache expires - */ - -const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour) - -/** @type {CachedAuth|null} */ -let authCache = null - -/** @type {Promise|null} */ -let authRequestInFlight = null - -self.addEventListener('message', (event) => { - if (event.data.type === 'INVALIDATE_AUTH_HEADER') { - authCache = null - authRequestInFlight = null - } -}) - -self.addEventListener('fetch', (event) => { - const url = new URL(event.request.url) - - if ( - !url.pathname.startsWith('/api/view') && - !url.pathname.startsWith('/api/viewvideo') - ) { - return - } - - event.respondWith( - (async () => { - try { - const authHeader = await getAuthHeader() - - if (!authHeader) { - return fetch(event.request) - } - - const headers = new Headers(event.request.headers) - for (const [key, value] of Object.entries(authHeader)) { - headers.set(key, value) - } - - // Fetch with manual redirect to handle cross-origin redirects (e.g., GCS signed URLs) - const response = await fetch( - new Request(event.request.url, { - method: event.request.method, - headers: headers, - credentials: event.request.credentials, - cache: 'no-store', - redirect: 'manual', - referrer: event.request.referrer, - integrity: event.request.integrity - }) - ) - - // Handle redirects to external storage (e.g., GCS signed URLs) - if (response.type === 'opaqueredirect') { - // Opaqueredirect: redirect occurred but response is opaque (headers not accessible) - // Re-fetch the original /api/view URL with redirect: 'follow' and mode: 'no-cors' - // - mode: 'no-cors' allows cross-origin fetches without CORS headers (GCS doesn't have CORS) - // - Returns opaque response, which works fine for images/videos/audio - // - Browser will send auth headers to /api/view (same-origin) - // - Browser will receive 302 redirect to GCS - // - Browser will follow redirect using GCS signed URL authentication - return fetch(event.request.url, { - method: 'GET', - headers: headers, - redirect: 'follow', - mode: 'no-cors' - }) - } - - // Non-opaque redirect (status visible) - shouldn't normally happen with redirect: 'manual' - // but handle as fallback - if (response.status === 302 || response.status === 301) { - const location = response.headers.get('location') - if (location) { - // Follow redirect manually - do NOT include auth headers for external URLs - return fetch(location, { - method: 'GET', - redirect: 'follow' - }) - } - } - - return response - } catch (error) { - console.error('[Auth SW] Request failed:', error) - return fetch(event.request) - } - })() - ) -}) - -/** - * Gets auth header from cache or requests from main thread - * @returns {Promise} - */ -async function getAuthHeader() { - // Return cached value if valid - if (authCache && authCache.expiresAt > Date.now()) { - return authCache.header - } - - // Clear expired cache - if (authCache) { - authCache = null - } - - // Deduplicate concurrent requests - if (authRequestInFlight) { - return authRequestInFlight - } - - authRequestInFlight = requestAuthHeaderFromMainThread() - const header = await authRequestInFlight - authRequestInFlight = null - - // Cache the result - if (header) { - authCache = { - header, - expiresAt: Date.now() + CACHE_TTL_MS - } - } - - return header -} - -/** - * Requests auth header from main thread via MessageChannel - * @returns {Promise} - */ -async function requestAuthHeaderFromMainThread() { - const clients = await self.clients.matchAll() - if (clients.length === 0) { - return null - } - - const messageChannel = new MessageChannel() - - return new Promise((resolve) => { - let timeoutId - - messageChannel.port1.onmessage = (event) => { - clearTimeout(timeoutId) - resolve(event.data.authHeader) - } - - timeoutId = setTimeout(() => { - console.error( - '[Auth SW] Timeout waiting for auth header from main thread' - ) - resolve(null) - }, 1000) - - clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [ - messageChannel.port2 - ]) - }) -} - -self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()) -}) diff --git a/src/main.ts b/src/main.ts index b5b4b844f..8e7697148 100644 --- a/src/main.ts +++ b/src/main.ts @@ -82,10 +82,4 @@ app modules: [VueFireAuth()] }) -// Register auth service worker after Pinia is initialized (cloud-only) -// Wait for registration to complete before mounting to ensure SW controls the page -if (isCloud) { - await import('@/platform/auth/serviceWorker') -} - app.mount('#vue-app') diff --git a/src/platform/auth/serviceWorker/index.ts b/src/platform/auth/serviceWorker/index.ts deleted file mode 100644 index 518723fc7..000000000 --- a/src/platform/auth/serviceWorker/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isCloud } from '@/platform/distribution/types' - -/** - * Auth service worker registration (cloud-only). - * Tree-shaken for desktop/localhost builds via compile-time constant. - */ -if (isCloud) { - await import('./register') -} diff --git a/src/platform/auth/serviceWorker/register.ts b/src/platform/auth/serviceWorker/register.ts deleted file mode 100644 index c752721f7..000000000 --- a/src/platform/auth/serviceWorker/register.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { watch } from 'vue' - -import { useCurrentUser } from '@/composables/auth/useCurrentUser' -import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' - -/** - * Registers the authentication service worker for cloud distribution. - * Intercepts /api/view requests to add auth headers for browser-native requests. - */ -async function registerAuthServiceWorker(): Promise { - if (!('serviceWorker' in navigator)) { - return - } - - try { - // Use dev service worker in development mode (rewrites to configured backend URL with token in query param) - // Use production service worker in production (same-origin requests with Authorization header) - const swPath = import.meta.env.DEV ? '/auth-dev-sw.js' : '/auth-sw.js' - const registration = await navigator.serviceWorker.register(swPath) - - // Configure base URL for dev service worker - if (import.meta.env.DEV) { - console.warn('[Auth DEV SW] Registering development serviceworker') - // Use the same URL that Vite proxy is using - const baseUrl = __DEV_SERVER_COMFYUI_URL__ - navigator.serviceWorker.controller?.postMessage({ - type: 'SET_BASE_URL', - baseUrl - }) - - // Also set base URL when service worker becomes active - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing - newWorker?.addEventListener('statechange', () => { - if (newWorker.state === 'activated') { - navigator.serviceWorker.controller?.postMessage({ - type: 'SET_BASE_URL', - baseUrl - }) - } - }) - }) - } - - setupAuthHeaderProvider() - setupCacheInvalidation() - } catch (error) { - console.error('[Auth SW] Registration failed:', error) - } -} - -/** - * Listens for auth header requests from the service worker - */ -function setupAuthHeaderProvider(): void { - navigator.serviceWorker.addEventListener('message', async (event) => { - if (event.data.type === 'REQUEST_AUTH_HEADER') { - const firebaseAuthStore = useFirebaseAuthStore() - const authHeader = await firebaseAuthStore.getAuthHeader() - - event.ports[0].postMessage({ - type: 'AUTH_HEADER_RESPONSE', - authHeader - }) - } - }) -} - -/** - * Invalidates cached auth header when user logs in/out - */ -function setupCacheInvalidation(): void { - const { isLoggedIn } = useCurrentUser() - - watch(isLoggedIn, (newValue, oldValue) => { - if (newValue !== oldValue) { - navigator.serviceWorker.controller?.postMessage({ - type: 'INVALIDATE_AUTH_HEADER' - }) - } - }) -} - -await registerAuthServiceWorker() diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 3a8ce7ade..c38a27585 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -16,8 +16,6 @@ declare global { interface Window { __COMFYUI_FRONTEND_VERSION__: string } - - const __DEV_SERVER_COMFYUI_URL__: string } export {} diff --git a/vite.config.mts b/vite.config.mts index 88631efeb..b2424dc43 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -302,8 +302,7 @@ export default defineConfig({ __ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''), __ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''), __USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true', - __DISTRIBUTION__: JSON.stringify(DISTRIBUTION), - __DEV_SERVER_COMFYUI_URL__: JSON.stringify(DEV_SERVER_COMFYUI_URL) + __DISTRIBUTION__: JSON.stringify(DISTRIBUTION) }, resolve: {