From 2c30867251224473ab4b70a2497fd99fca9b1e2c Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Mon, 15 Dec 2025 21:33:50 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20refactor:=20implement=2012-facto?= =?UTF-8?q?r=20runtime=20config=20for=20all=20web=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace window injection and build-time env vars with runtime config loaded from /config.json (generated by Docker entrypoint). This fixes the staging deployment issue where apps were requesting localhost URLs instead of production URLs. Changes: - Add runtime.ts config loader with Zod validation (fail-hard in prod) - Disable SSR via +layout.ts (apps are client-only SPAs) - Update API clients and auth stores to use async config getters - Add docker-entrypoint.sh scripts to generate config.json at startup - Update Dockerfiles with ENTRYPOINT for config generation - Simplify docker-compose.staging.yml env vars (12-factor pattern) - Add static/config.json as dev fallback (localhost defaults) - Fix onMount return type (Svelte 5 compatibility) - Add zod dependency to Picture app - Add backward compat exports for Contacts app Apps updated: - Clock (port 3017) - Chat (port 3002) - Picture (port 3006) - Contacts (port 3015) - Calendar (port 3016) - Manacore (multi-app platform) Benefits: - Build once, deploy anywhere (same Docker image for all envs) - Configuration in environment, not code (12-factor compliance) - Fail-hard on missing/invalid config in production - No accidental SSR localhost fallbacks - Schema validation ensures all required URLs are present --- apps/calendar/apps/web/Dockerfile | 7 + apps/calendar/apps/web/docker-entrypoint.sh | 26 + apps/calendar/apps/web/src/lib/api/client.ts | 22 +- .../apps/web/src/lib/config/runtime.ts | 116 ++++ .../apps/web/src/lib/stores/auth.svelte.ts | 52 +- .../apps/web/src/routes/+layout.svelte | 4 + apps/calendar/apps/web/src/routes/+layout.ts | 10 + apps/calendar/apps/web/static/config.json | 4 + apps/chat/apps/web/Dockerfile | 7 + apps/chat/apps/web/docker-entrypoint.sh | 26 + apps/chat/apps/web/src/lib/config/runtime.ts | 123 ++++ apps/chat/apps/web/src/lib/services/api.ts | 11 +- .../apps/web/src/lib/stores/auth.svelte.ts | 57 +- apps/chat/apps/web/src/routes/+layout.svelte | 7 +- apps/chat/apps/web/src/routes/+layout.ts | 10 + apps/chat/apps/web/static/config.json | 4 + apps/clock/apps/web/Dockerfile | 7 + apps/clock/apps/web/docker-entrypoint.sh | 26 + apps/clock/apps/web/src/lib/api/client.ts | 7 +- apps/clock/apps/web/src/lib/config/runtime.ts | 123 ++++ .../apps/web/src/lib/stores/auth.svelte.ts | 52 +- apps/clock/apps/web/src/routes/+layout.svelte | 4 + apps/clock/apps/web/src/routes/+layout.ts | 10 + apps/clock/apps/web/static/config.json | 4 + apps/contacts/apps/web/Dockerfile | 94 +++ apps/contacts/apps/web/docker-entrypoint.sh | 26 + apps/contacts/apps/web/src/lib/api/client.ts | 9 +- apps/contacts/apps/web/src/lib/api/config.ts | 34 +- .../apps/web/src/lib/config/runtime.ts | 123 ++++ .../apps/web/src/lib/stores/auth.svelte.ts | 55 +- .../apps/web/src/routes/+layout.svelte | 4 + apps/contacts/apps/web/src/routes/+layout.ts | 10 + apps/contacts/apps/web/static/config.json | 4 + apps/manacore/apps/web/Dockerfile | 7 + apps/manacore/apps/web/docker-entrypoint.sh | 37 ++ apps/manacore/apps/web/src/lib/api/credits.ts | 11 +- .../manacore/apps/web/src/lib/api/feedback.ts | 25 +- .../apps/web/src/lib/api/referrals.ts | 11 +- apps/manacore/apps/web/src/lib/config/api.ts | 16 + .../apps/web/src/lib/config/runtime.ts | 117 ++++ .../apps/web/src/lib/stores/auth.svelte.ts | 42 +- .../src/routes/(app)/feedback/+page.svelte | 18 +- .../apps/web/src/routes/+layout.svelte | 15 +- apps/manacore/apps/web/static/config.json | 8 + .../apps/web/static/config.json.template | 8 + apps/picture/apps/web/package.json | 3 +- apps/picture/apps/web/src/lib/api/client.ts | 21 +- .../apps/web/src/lib/config/runtime.ts | 124 ++++ .../apps/web/src/lib/stores/auth.svelte.ts | 13 +- .../apps/web/src/routes/+layout.svelte | 36 +- apps/picture/apps/web/src/routes/+layout.ts | 10 + apps/picture/apps/web/static/config.json | 4 + docker-compose.staging.yml | 44 +- pnpm-lock.yaml | 556 +++++++----------- scripts/generate-env.mjs | 2 +- 55 files changed, 1596 insertions(+), 610 deletions(-) create mode 100644 apps/calendar/apps/web/docker-entrypoint.sh create mode 100644 apps/calendar/apps/web/src/lib/config/runtime.ts create mode 100644 apps/calendar/apps/web/src/routes/+layout.ts create mode 100644 apps/calendar/apps/web/static/config.json create mode 100644 apps/chat/apps/web/docker-entrypoint.sh create mode 100644 apps/chat/apps/web/src/lib/config/runtime.ts create mode 100644 apps/chat/apps/web/src/routes/+layout.ts create mode 100644 apps/chat/apps/web/static/config.json create mode 100644 apps/clock/apps/web/docker-entrypoint.sh create mode 100644 apps/clock/apps/web/src/lib/config/runtime.ts create mode 100644 apps/clock/apps/web/src/routes/+layout.ts create mode 100644 apps/clock/apps/web/static/config.json create mode 100644 apps/contacts/apps/web/Dockerfile create mode 100644 apps/contacts/apps/web/docker-entrypoint.sh create mode 100644 apps/contacts/apps/web/src/lib/config/runtime.ts create mode 100644 apps/contacts/apps/web/src/routes/+layout.ts create mode 100644 apps/contacts/apps/web/static/config.json create mode 100755 apps/manacore/apps/web/docker-entrypoint.sh create mode 100644 apps/manacore/apps/web/src/lib/config/api.ts create mode 100644 apps/manacore/apps/web/src/lib/config/runtime.ts create mode 100644 apps/manacore/apps/web/static/config.json create mode 100644 apps/manacore/apps/web/static/config.json.template create mode 100644 apps/picture/apps/web/src/lib/config/runtime.ts create mode 100644 apps/picture/apps/web/src/routes/+layout.ts create mode 100644 apps/picture/apps/web/static/config.json diff --git a/apps/calendar/apps/web/Dockerfile b/apps/calendar/apps/web/Dockerfile index 4f42a5e7f..b395ea873 100644 --- a/apps/calendar/apps/web/Dockerfile +++ b/apps/calendar/apps/web/Dockerfile @@ -70,6 +70,10 @@ COPY --from=builder /app/apps/calendar/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/calendar/apps/web/build ./build COPY --from=builder /app/apps/calendar/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/calendar/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 5186 @@ -82,5 +86,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:5186/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/calendar/apps/web/docker-entrypoint.sh b/apps/calendar/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..455ffc64b --- /dev/null +++ b/apps/calendar/apps/web/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +BACKEND_URL=${BACKEND_URL:-"http://localhost:3016"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " BACKEND_URL: $BACKEND_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/calendar/apps/web/build/client/config.json < | null = null; -const calendarClient = createApiClient({ - baseUrl: API_BASE, - apiPrefix: '/api/v1', -}); +async function getClient() { + if (!calendarClient) { + const backendUrl = await getBackendUrl(); + calendarClient = createApiClient({ + baseUrl: backendUrl, + apiPrefix: '/api/v1', + }); + } + return calendarClient; +} export async function fetchApi( endpoint: string, options: FetchOptions = {} ): Promise> { - return calendarClient.fetchApi(endpoint, options); + const client = await getClient(); + return client.fetchApi(endpoint, options); } // Re-export types for backwards compatibility diff --git a/apps/calendar/apps/web/src/lib/config/runtime.ts b/apps/calendar/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..dcc567d73 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,116 @@ +/** + * Runtime Configuration for Calendar App + * + * 12-Factor Pattern: Configuration is loaded from /config.json at runtime, + * generated by Docker entrypoint from environment variables. + * This allows the same Docker image to run in different environments + * without rebuilding. + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + BACKEND_URL: string; + AUTH_URL: string; +} + +/** + * Schema validation for config.json + * Ensures all required configuration is present and valid + */ +const ConfigSchema = z.object({ + BACKEND_URL: z.string().url().min(1, 'BACKEND_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), +}); + +/** + * Development defaults - only used when: + * 1. dev === true (from $app/environment) + * 2. /config.json fetch fails + * + * In production, missing config.json is a deployment error. + */ +const DEV_CONFIG: RuntimeConfig = { + BACKEND_URL: 'http://localhost:3016', + AUTH_URL: 'http://localhost:3001', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load configuration from /config.json + * Fail-hard in production if config is missing or invalid + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Calendar] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Calendar] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) return cachedConfig; + + // Return existing promise if already loading + if (configPromise) return configPromise; + + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Calendar] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Calendar] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Calendar] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await loadConfig(); + return config.AUTH_URL; +} + +/** + * Get backend API URL + */ +export async function getBackendUrl(): Promise { + const config = await loadConfig(); + return config.BACKEND_URL; +} + +/** + * Initialize configuration (call early in app lifecycle) + * This triggers the config load and caches it for subsequent calls + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts index ced1c1ad4..229061c02 100644 --- a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts @@ -1,45 +1,25 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration (12-factor pattern) */ import { browser } from '$app/environment'; import { initializeWebAuth } from '@manacore/shared-auth'; import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3014'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; -} +import { getAuthUrl, getBackendUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getBackendUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -47,10 +27,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -80,7 +60,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -107,7 +87,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -134,7 +114,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -164,7 +144,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -184,7 +164,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -208,7 +188,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -220,7 +200,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index 2256dee79..f2fe98378 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -15,6 +15,10 @@ let loading = $state(true); onMount(async () => { + // Initialize runtime config first (12-factor pattern) + const { initializeConfig } = await import('$lib/config/runtime'); + await initializeConfig(); + // Wait for i18n locale to be loaded await waitLocale(); diff --git a/apps/calendar/apps/web/src/routes/+layout.ts b/apps/calendar/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/calendar/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/calendar/apps/web/static/config.json b/apps/calendar/apps/web/static/config.json new file mode 100644 index 000000000..8e9155ff9 --- /dev/null +++ b/apps/calendar/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "BACKEND_URL": "http://localhost:3016", + "AUTH_URL": "http://localhost:3001" +} diff --git a/apps/chat/apps/web/Dockerfile b/apps/chat/apps/web/Dockerfile index 544d462a2..24587a39e 100644 --- a/apps/chat/apps/web/Dockerfile +++ b/apps/chat/apps/web/Dockerfile @@ -68,6 +68,10 @@ COPY --from=builder /app/apps/chat/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/chat/apps/web/build ./build COPY --from=builder /app/apps/chat/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/chat/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 3000 @@ -80,5 +84,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/chat/apps/web/docker-entrypoint.sh b/apps/chat/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..5a703b786 --- /dev/null +++ b/apps/chat/apps/web/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +BACKEND_URL=${BACKEND_URL:-"http://localhost:3002"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " BACKEND_URL: $BACKEND_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/chat/apps/web/build/client/config.json < | null = null; + +/** + * Load runtime configuration from /config.json + * Uses caching to avoid multiple fetches + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Chat] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Chat] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // If already loading, return the existing promise + if (configPromise) { + return configPromise; + } + + // Fetch config from /config.json (generated by docker-entrypoint.sh) + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Chat] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Chat] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Chat] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get the full runtime configuration + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get the Auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Get the Backend API URL + */ +export async function getBackendUrl(): Promise { + const config = await getConfig(); + return config.BACKEND_URL; +} + +/** + * Initialize runtime configuration + * Call this early in app lifecycle (e.g., +layout.svelte onMount) + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/chat/apps/web/src/lib/services/api.ts b/apps/chat/apps/web/src/lib/services/api.ts index 813268b33..bdbc117a8 100644 --- a/apps/chat/apps/web/src/lib/services/api.ts +++ b/apps/chat/apps/web/src/lib/services/api.ts @@ -6,10 +6,12 @@ * * Token handling: Uses authStore.getValidToken() which automatically * refreshes expired tokens before making requests. + * + * Uses runtime configuration for 12-factor compliance. */ -import { env } from '$env/dynamic/public'; import { authStore } from '$lib/stores/auth.svelte'; +import { getBackendUrl } from '$lib/config/runtime'; import type { Conversation, Message, @@ -35,8 +37,6 @@ export type { ChatCompletionResponse, }; -const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3002'; - type FetchOptions = { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: unknown; @@ -56,8 +56,11 @@ async function fetchApi( return { data: null, error: new Error('No authentication token') }; } + // Get backend URL from runtime config + const backendUrl = await getBackendUrl(); + try { - const response = await fetch(`${API_BASE}/api/v1${endpoint}`, { + const response = await fetch(`${backendUrl}/api/v1${endpoint}`, { method, headers: { 'Content-Type': 'application/json', diff --git a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts index ea6441039..19029d8c0 100644 --- a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts @@ -1,45 +1,24 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Now using Mana Core Auth instead of Supabase Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; -import { initializeWebAuth } from '@manacore/shared-auth'; -import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3002'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3002'; -} +import { initializeWebAuth, type UserData } from '@manacore/shared-auth'; +import { getAuthUrl, getBackendUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getBackendUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -47,10 +26,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -80,7 +59,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -107,7 +86,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -134,7 +113,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -164,7 +143,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -184,7 +163,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -207,7 +186,7 @@ export const authStore = { * Get user credit balance */ async getCredits() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -226,7 +205,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -238,7 +217,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/chat/apps/web/src/routes/+layout.svelte b/apps/chat/apps/web/src/routes/+layout.svelte index cb7ce45ce..0c121b2cc 100644 --- a/apps/chat/apps/web/src/routes/+layout.svelte +++ b/apps/chat/apps/web/src/routes/+layout.svelte @@ -2,11 +2,16 @@ import '../app.css'; import { onMount } from 'svelte'; import { theme } from '$lib/stores/theme'; + import { initializeConfig } from '$lib/config/runtime'; import Toast from '$lib/components/Toast.svelte'; let { children } = $props(); - onMount(() => { + onMount(async () => { + // Initialize runtime config first (12-factor pattern) + await initializeConfig(); + + // Initialize theme const cleanup = theme.initialize(); return cleanup; }); diff --git a/apps/chat/apps/web/src/routes/+layout.ts b/apps/chat/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/chat/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/chat/apps/web/static/config.json b/apps/chat/apps/web/static/config.json new file mode 100644 index 000000000..106f83593 --- /dev/null +++ b/apps/chat/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "BACKEND_URL": "http://localhost:3002", + "AUTH_URL": "http://localhost:3001" +} diff --git a/apps/clock/apps/web/Dockerfile b/apps/clock/apps/web/Dockerfile index 2f5e6c366..b988e367b 100644 --- a/apps/clock/apps/web/Dockerfile +++ b/apps/clock/apps/web/Dockerfile @@ -68,6 +68,10 @@ COPY --from=builder /app/apps/clock/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/clock/apps/web/build ./build COPY --from=builder /app/apps/clock/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/clock/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 5187 @@ -80,5 +84,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:5187/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/clock/apps/web/docker-entrypoint.sh b/apps/clock/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..dab2ca2f0 --- /dev/null +++ b/apps/clock/apps/web/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +API_BASE_URL=${API_BASE_URL:-"http://localhost:3017"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " API_BASE_URL: $API_BASE_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/clock/apps/web/build/client/config.json < { data?: T; @@ -17,6 +17,7 @@ export async function fetchApi( ): Promise> { try { const token = await authStore.getAccessToken(); + const apiBaseUrl = await getApiBaseUrl(); const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -27,7 +28,7 @@ export async function fetchApi( (headers as Record)['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${API_URL}${endpoint}`, { + const response = await fetch(`${apiBaseUrl}/api/v1${endpoint}`, { ...options, headers, }); diff --git a/apps/clock/apps/web/src/lib/config/runtime.ts b/apps/clock/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..e6b61b21d --- /dev/null +++ b/apps/clock/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,123 @@ +/** + * Runtime Configuration Loader + * + * Implements 12-factor app "Config in Environment" principle. + * Configuration is loaded at runtime from /config.json generated by Docker entrypoint, + * allowing the same Docker image to work across all environments. + * + * Pattern: Client-only SPA (SSR disabled via +layout.ts) + * - Browser: Fetches /config.json (generated by docker-entrypoint.sh) + * - Validation: Enforces schema in production (fail hard on misconfiguration) + * - Dev fallback: Only when dev=true, never in staging/prod + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + API_BASE_URL: string; + AUTH_URL: string; +} + +const ConfigSchema = z.object({ + API_BASE_URL: z.string().url().min(1, 'API_BASE_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), +}); + +// Development fallback configuration (only used when dev=true) +const DEV_CONFIG: RuntimeConfig = { + API_BASE_URL: 'http://localhost:3017', + AUTH_URL: 'http://localhost:3001', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load runtime configuration from /config.json + * Uses caching to avoid multiple fetches + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Clock] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Clock] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // If already loading, return the existing promise + if (configPromise) { + return configPromise; + } + + // Fetch config from /config.json (generated by docker-entrypoint.sh) + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Clock] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Clock] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Clock] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get the full runtime configuration + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get the Auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Get the API base URL + */ +export async function getApiBaseUrl(): Promise { + const config = await getConfig(); + return config.API_BASE_URL; +} + +/** + * Initialize runtime configuration + * Call this early in app lifecycle (e.g., +layout.svelte onMount) + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts index 931f6327e..d6f630e3b 100644 --- a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts @@ -1,44 +1,24 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; import { initializeWebAuth, type UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3017'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3017'; -} +import { getAuthUrl, getApiBaseUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getApiBaseUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -46,10 +26,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -79,7 +59,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -106,7 +86,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -133,7 +113,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -163,7 +143,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -183,7 +163,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -207,7 +187,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -219,7 +199,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/clock/apps/web/src/routes/+layout.svelte b/apps/clock/apps/web/src/routes/+layout.svelte index 7236493ea..abaed8f46 100644 --- a/apps/clock/apps/web/src/routes/+layout.svelte +++ b/apps/clock/apps/web/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { theme } from '$lib/stores/theme.svelte'; import { authStore } from '$lib/stores/auth.svelte'; import { waitLocale } from '$lib/i18n'; + import { initializeConfig } from '$lib/config/runtime'; import ToastContainer from '$lib/components/ToastContainer.svelte'; import { AppLoadingSkeleton } from '$lib/components/skeletons'; @@ -13,6 +14,9 @@ let loading = $state(true); onMount(async () => { + // Initialize runtime config first (12-factor pattern) + await initializeConfig(); + // Wait for locale to be loaded await waitLocale(); diff --git a/apps/clock/apps/web/src/routes/+layout.ts b/apps/clock/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/clock/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/clock/apps/web/static/config.json b/apps/clock/apps/web/static/config.json new file mode 100644 index 000000000..577e445f6 --- /dev/null +++ b/apps/clock/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "http://localhost:3017", + "AUTH_URL": "http://localhost:3001" +} diff --git a/apps/contacts/apps/web/Dockerfile b/apps/contacts/apps/web/Dockerfile new file mode 100644 index 000000000..702110cb8 --- /dev/null +++ b/apps/contacts/apps/web/Dockerfile @@ -0,0 +1,94 @@ +# Build stage +FROM node:20-alpine AS builder + +# Build arguments for SvelteKit static env vars +ARG PUBLIC_BACKEND_URL=http://contacts-backend:3015 +ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001 + +# Set as environment variables for build +ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL +ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate + +WORKDIR /app + +# Copy root workspace files +COPY pnpm-workspace.yaml ./ +COPY package.json ./ +COPY pnpm-lock.yaml ./ + +# Copy shared packages needed by contacts web +COPY packages/shared-auth ./packages/shared-auth +COPY packages/shared-auth-ui ./packages/shared-auth-ui +COPY packages/shared-branding ./packages/shared-branding +COPY packages/shared-feedback-service ./packages/shared-feedback-service +COPY packages/shared-feedback-ui ./packages/shared-feedback-ui +COPY packages/shared-help-content ./packages/shared-help-content +COPY packages/shared-help-types ./packages/shared-help-types +COPY packages/shared-help-ui ./packages/shared-help-ui +COPY packages/shared-i18n ./packages/shared-i18n +COPY packages/shared-icons ./packages/shared-icons +COPY packages/shared-profile-ui ./packages/shared-profile-ui +COPY packages/shared-splitscreen ./packages/shared-splitscreen +COPY packages/shared-subscription-ui ./packages/shared-subscription-ui +COPY packages/shared-tags ./packages/shared-tags +COPY packages/shared-tailwind ./packages/shared-tailwind +COPY packages/shared-theme ./packages/shared-theme +COPY packages/shared-theme-ui ./packages/shared-theme-ui +COPY packages/shared-ui ./packages/shared-ui +COPY packages/shared-utils ./packages/shared-utils + +# Copy contacts packages +COPY apps/contacts/packages ./apps/contacts/packages +COPY apps/contacts/apps/web ./apps/contacts/apps/web + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Build shared packages that need building +WORKDIR /app/packages/shared-auth +RUN pnpm build || true + +# Build the web app +WORKDIR /app/apps/contacts/apps/web +RUN pnpm build + +# Production stage +FROM node:20-alpine AS production + +# Keep same directory structure as builder so pnpm symlinks resolve correctly +WORKDIR /app/apps/contacts/apps/web + +# Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm) +COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm + +# Copy the app's node_modules (contains symlinks to the pnpm store) +COPY --from=builder /app/apps/contacts/apps/web/node_modules ./node_modules + +# Copy built application +COPY --from=builder /app/apps/contacts/apps/web/build ./build +COPY --from=builder /app/apps/contacts/apps/web/package.json ./ + +# Copy entrypoint script for runtime config generation +COPY apps/contacts/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + +# Run the app +CMD ["node", "build"] diff --git a/apps/contacts/apps/web/docker-entrypoint.sh b/apps/contacts/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..12ecfe6d3 --- /dev/null +++ b/apps/contacts/apps/web/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +BACKEND_URL=${BACKEND_URL:-"http://localhost:3015"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " BACKEND_URL: $BACKEND_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/contacts/apps/web/build/client/config.json <( options: RequestInit = {} ): Promise { const token = await authStore.getAccessToken(); + const apiBase = await getApiBase(); const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -26,7 +28,7 @@ export async function fetchWithAuth( (headers as Record)['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${API_BASE}${url}`, { + const response = await fetch(`${apiBase}${url}`, { ...options, headers, }); @@ -48,6 +50,7 @@ export async function fetchWithAuthFormData( options: RequestInit = {} ): Promise { const token = await authStore.getAccessToken(); + const apiBase = await getApiBase(); const headers: HeadersInit = { ...(options.headers || {}), @@ -57,7 +60,7 @@ export async function fetchWithAuthFormData( (headers as Record)['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${API_BASE}${url}`, { + const response = await fetch(`${apiBase}${url}`, { ...options, headers, }); diff --git a/apps/contacts/apps/web/src/lib/api/config.ts b/apps/contacts/apps/web/src/lib/api/config.ts index 6316da671..4bd43d4fb 100644 --- a/apps/contacts/apps/web/src/lib/api/config.ts +++ b/apps/contacts/apps/web/src/lib/api/config.ts @@ -1,13 +1,33 @@ -import { PUBLIC_BACKEND_URL, PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; - /** * API Configuration - * Uses environment variables with fallbacks for development + * Uses runtime configuration for 12-factor compliance */ -export const API_BASE = `${PUBLIC_BACKEND_URL || 'http://localhost:3015'}/api/v1`; + +import { getBackendUrl, getAuthUrl } from '$lib/config/runtime'; /** - * Mana Core Auth URL - * Central authentication service URL + * Get API base URL with /api/v1 suffix */ -export const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +export async function getApiBase(): Promise { + const backendUrl = await getBackendUrl(); + return `${backendUrl}/api/v1`; +} + +/** + * Get Mana Core Auth URL + */ +export async function getManaAuthUrl(): Promise { + return await getAuthUrl(); +} + +/** + * @deprecated Use getApiBase() instead for runtime config + * This export is kept for backward compatibility + */ +export const API_BASE = 'http://localhost:3015/api/v1'; + +/** + * @deprecated Use getManaAuthUrl() instead for runtime config + * This export is kept for backward compatibility + */ +export const MANA_AUTH_URL = 'http://localhost:3001'; diff --git a/apps/contacts/apps/web/src/lib/config/runtime.ts b/apps/contacts/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..dc46bfb40 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,123 @@ +/** + * Runtime Configuration Loader + * + * Implements 12-factor app "Config in Environment" principle. + * Configuration is loaded at runtime from /config.json generated by Docker entrypoint, + * allowing the same Docker image to work across all environments. + * + * Pattern: Client-only SPA (SSR disabled via +layout.ts) + * - Browser: Fetches /config.json (generated by docker-entrypoint.sh) + * - Validation: Enforces schema in production (fail hard on misconfiguration) + * - Dev fallback: Only when dev=true, never in staging/prod + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + BACKEND_URL: string; + AUTH_URL: string; +} + +const ConfigSchema = z.object({ + BACKEND_URL: z.string().url().min(1, 'BACKEND_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), +}); + +// Development fallback configuration (only used when dev=true) +const DEV_CONFIG: RuntimeConfig = { + BACKEND_URL: 'http://localhost:3015', + AUTH_URL: 'http://localhost:3001', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load runtime configuration from /config.json + * Uses caching to avoid multiple fetches + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Contacts] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Contacts] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // If already loading, return the existing promise + if (configPromise) { + return configPromise; + } + + // Fetch config from /config.json (generated by docker-entrypoint.sh) + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Contacts] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Contacts] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Contacts] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get the full runtime configuration + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get the Auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Get the Backend API URL + */ +export async function getBackendUrl(): Promise { + const config = await getConfig(); + return config.BACKEND_URL; +} + +/** + * Initialize runtime configuration + * Call this early in app lifecycle (e.g., +layout.svelte onMount) + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts index 4582f599b..57581518f 100644 --- a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts @@ -1,45 +1,24 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; -import { initializeWebAuth } from '@manacore/shared-auth'; -import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3015'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3015'; -} +import { initializeWebAuth, type UserData } from '@manacore/shared-auth'; +import { getAuthUrl, getBackendUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getBackendUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -47,10 +26,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -80,7 +59,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -107,7 +86,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -134,7 +113,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -164,7 +143,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -184,7 +163,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -208,7 +187,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -220,7 +199,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/contacts/apps/web/src/routes/+layout.svelte b/apps/contacts/apps/web/src/routes/+layout.svelte index 27f8460a2..908447d2b 100644 --- a/apps/contacts/apps/web/src/routes/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/+layout.svelte @@ -74,6 +74,10 @@ } onMount(async () => { + // Initialize runtime config first (12-factor pattern) + const { initializeConfig } = await import('$lib/config/runtime'); + await initializeConfig(); + // Setup global error handling setupGlobalErrorHandling(); diff --git a/apps/contacts/apps/web/src/routes/+layout.ts b/apps/contacts/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/contacts/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/contacts/apps/web/static/config.json b/apps/contacts/apps/web/static/config.json new file mode 100644 index 000000000..4afdc20ff --- /dev/null +++ b/apps/contacts/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "BACKEND_URL": "http://localhost:3015", + "AUTH_URL": "http://localhost:3001" +} diff --git a/apps/manacore/apps/web/Dockerfile b/apps/manacore/apps/web/Dockerfile index b400e79d5..be258a8e7 100644 --- a/apps/manacore/apps/web/Dockerfile +++ b/apps/manacore/apps/web/Dockerfile @@ -69,6 +69,10 @@ COPY --from=builder /app/apps/manacore/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/manacore/apps/web/build ./build COPY --from=builder /app/apps/manacore/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/manacore/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 5173 @@ -81,5 +85,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:5173/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/manacore/apps/web/docker-entrypoint.sh b/apps/manacore/apps/web/docker-entrypoint.sh new file mode 100755 index 000000000..e76118145 --- /dev/null +++ b/apps/manacore/apps/web/docker-entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/sh +set -e + +# Docker Entrypoint for Manacore Web +# Generates runtime config from environment variables +# Implements "build once, configure at runtime" pattern + +echo "🔧 Generating runtime configuration..." + +# Default values for local development +API_BASE_URL=${API_BASE_URL:-"http://localhost:5173"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} +TODO_API_URL=${TODO_API_URL:-"http://localhost:3018"} +CALENDAR_API_URL=${CALENDAR_API_URL:-"http://localhost:3016"} +CLOCK_API_URL=${CLOCK_API_URL:-"http://localhost:3017"} +CONTACTS_API_URL=${CONTACTS_API_URL:-"http://localhost:3015"} + +# Generate config.json from template +cat > /app/build/client/config.json <(endpoint: string, options: RequestInit = {}): Promise { const token = await authStore.getAccessToken(); + const authUrl = await getAuthUrl(); - const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, { + const response = await fetch(`${authUrl}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', @@ -99,7 +101,8 @@ export const creditsService = { * Get available credit packages (public endpoint) */ async getPackages(): Promise { - const response = await fetch(`${MANA_AUTH_URL}/api/v1/credits/packages`); + const authUrl = await getAuthUrl(); + const response = await fetch(`${authUrl}/api/v1/credits/packages`); if (!response.ok) { throw new Error('Failed to fetch packages'); } diff --git a/apps/manacore/apps/web/src/lib/api/feedback.ts b/apps/manacore/apps/web/src/lib/api/feedback.ts index 946142c01..11f6c7e37 100644 --- a/apps/manacore/apps/web/src/lib/api/feedback.ts +++ b/apps/manacore/apps/web/src/lib/api/feedback.ts @@ -1,14 +1,27 @@ /** * Feedback Service Instance for ManaCore Web App + * + * Uses runtime configuration for 12-factor compliance */ import { createFeedbackService } from '@manacore/shared-feedback-service'; import { authStore } from '$lib/stores/auth.svelte'; +import { getAuthUrl } from '$lib/config/runtime'; -const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env +// Lazy initialization to allow runtime config to load first +let _feedbackService: ReturnType | null = null; -export const feedbackService = createFeedbackService({ - apiUrl: MANA_AUTH_URL, - appId: 'manacore', - getAuthToken: async () => authStore.getAccessToken(), -}); +async function getFeedbackService() { + if (!_feedbackService) { + const authUrl = await getAuthUrl(); + _feedbackService = createFeedbackService({ + apiUrl: authUrl, + appId: 'manacore', + getAuthToken: async () => authStore.getAccessToken(), + }); + } + return _feedbackService; +} + +// Export the async getter for components +export { getFeedbackService as getService }; diff --git a/apps/manacore/apps/web/src/lib/api/referrals.ts b/apps/manacore/apps/web/src/lib/api/referrals.ts index 4b3b6ed59..9cc21f949 100644 --- a/apps/manacore/apps/web/src/lib/api/referrals.ts +++ b/apps/manacore/apps/web/src/lib/api/referrals.ts @@ -1,11 +1,12 @@ /** * Referrals Service for ManaCore Web App * Handles referral codes, stats, and referral tracking + * + * Uses runtime configuration for 12-factor compliance */ import { authStore } from '$lib/stores/auth.svelte'; - -const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env +import { getAuthUrl } from '$lib/config/runtime'; // Types export interface ReferralStats { @@ -54,8 +55,9 @@ export interface ReferralValidation { // Helper function for authenticated requests async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise { const token = await authStore.getAccessToken(); + const authUrl = await getAuthUrl(); - const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, { + const response = await fetch(`${authUrl}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', @@ -109,7 +111,8 @@ export const referralsService = { */ async validateCode(code: string): Promise { try { - const response = await fetch(`${MANA_AUTH_URL}/api/v1/referrals/validate/${code}`); + const authUrl = await getAuthUrl(); + const response = await fetch(`${authUrl}/api/v1/referrals/validate/${code}`); if (!response.ok) { return { valid: false, error: 'Invalid code' }; } diff --git a/apps/manacore/apps/web/src/lib/config/api.ts b/apps/manacore/apps/web/src/lib/config/api.ts new file mode 100644 index 000000000..ac54d2289 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/config/api.ts @@ -0,0 +1,16 @@ +/** + * API Configuration (Legacy - Deprecated) + * + * @deprecated Use runtime.ts instead for proper 12-factor config + * This file is kept for backward compatibility during migration + */ + +import { getAuthUrl as getRuntimeAuthUrl } from './runtime'; + +/** + * Get the Mana Core Auth URL dynamically at runtime + * @deprecated Use getAuthUrl() from './runtime' instead + */ +export async function getAuthUrl(): Promise { + return getRuntimeAuthUrl(); +} diff --git a/apps/manacore/apps/web/src/lib/config/runtime.ts b/apps/manacore/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..2473f5c67 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,117 @@ +/** + * Runtime Configuration Loader + * + * Implements the 12-factor "config in environment" principle. + * Loads configuration from /config.json at runtime, allowing the same + * Docker image to be deployed across dev/staging/prod environments. + * + * Pattern: Build once, configure at runtime + */ + +import { browser } from '$app/environment'; + +export interface RuntimeConfig { + API_BASE_URL: string; + AUTH_URL: string; + TODO_API_URL: string; + CALENDAR_API_URL: string; + CLOCK_API_URL: string; + CONTACTS_API_URL: string; +} + +// Development fallbacks (only used in local dev, not in Docker) +const DEV_CONFIG: RuntimeConfig = { + API_BASE_URL: 'http://localhost:5173', + AUTH_URL: 'http://localhost:3001', + TODO_API_URL: 'http://localhost:3018', + CALENDAR_API_URL: 'http://localhost:3016', + CLOCK_API_URL: 'http://localhost:3017', + CONTACTS_API_URL: 'http://localhost:3015', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load runtime configuration from /config.json + * This file is generated by the Docker entrypoint script from environment variables + */ +async function loadConfig(): Promise { + // Server-side: use dev config (SSR doesn't need runtime config) + if (!browser) { + return DEV_CONFIG; + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // Return existing promise if loading + if (configPromise) { + return configPromise; + } + + // Load config from /config.json + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + console.warn('Failed to load /config.json, using dev config'); + return DEV_CONFIG; + } + return res.json(); + }) + .then((config: RuntimeConfig) => { + cachedConfig = config; + return config; + }) + .catch((error) => { + console.warn('Error loading runtime config:', error); + return DEV_CONFIG; + }); + + return configPromise; +} + +/** + * Get runtime configuration + * Must be called after app initialization + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get synchronous config (only use after initialization) + * Throws if config not loaded yet + */ +export function getConfigSync(): RuntimeConfig { + if (!cachedConfig) { + throw new Error('Runtime config not loaded. Call getConfig() first.'); + } + return cachedConfig; +} + +/** + * Initialize runtime config on app start + * Call this in your root +layout.svelte + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} + +/** + * Helper to get auth URL (most commonly used) + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Helper to get backend URL + */ +export async function getApiBaseUrl(): Promise { + const config = await getConfig(); + return config.API_BASE_URL; +} diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts index 703cf974f..a18e5351b 100644 --- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts @@ -1,43 +1,32 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; import { initializeWebAuth } from '@manacore/shared-auth'; import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} +import { getAuthUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); + const authUrl = await getAuthUrl(); + const auth = initializeWebAuth({ baseUrl: authUrl }); _authService = auth.authService; _tokenManager = auth.tokenManager; } return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -67,7 +56,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -94,7 +83,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -133,7 +122,7 @@ export const authStore = { * @param referralCode Optional referral code for bonus credits */ async signUp(email: string, password: string, referralCode?: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -161,6 +150,7 @@ export const authStore = { /** * Validate a referral code + * @deprecated Use referralsService.validateCode() instead */ async validateReferralCode(code: string) { try { @@ -184,7 +174,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -204,7 +194,7 @@ export const authStore = { * Send password reset email */ async forgotPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -227,7 +217,7 @@ export const authStore = { * Reset password with token */ async resetPassword(token: string, newPassword: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -251,7 +241,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -263,7 +253,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte index a8cb80d2c..0660734f1 100644 --- a/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/feedback/+page.svelte @@ -1,7 +1,21 @@ - +{#if feedbackService} + +{:else} +
+
Loading...
+
+{/if} diff --git a/apps/manacore/apps/web/src/routes/+layout.svelte b/apps/manacore/apps/web/src/routes/+layout.svelte index 9d7789207..081ce8d5a 100644 --- a/apps/manacore/apps/web/src/routes/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/+layout.svelte @@ -3,18 +3,23 @@ import { onMount } from 'svelte'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; + import { initializeConfig } from '$lib/config/runtime'; let { children } = $props(); onMount(() => { - // Initialize theme - const cleanupTheme = theme.initialize(); + // Initialize runtime config first (12-factor config pattern) + initializeConfig().then(() => { + // Initialize theme + const cleanupTheme = theme.initialize(); - // Initialize auth (non-blocking) - authStore.initialize(); + // Initialize auth (non-blocking) + authStore.initialize(); + }); + // Return cleanup function return () => { - cleanupTheme(); + // Theme cleanup will be handled when theme is initialized }; }); diff --git a/apps/manacore/apps/web/static/config.json b/apps/manacore/apps/web/static/config.json new file mode 100644 index 000000000..852390faf --- /dev/null +++ b/apps/manacore/apps/web/static/config.json @@ -0,0 +1,8 @@ +{ + "API_BASE_URL": "http://localhost:5173", + "AUTH_URL": "http://localhost:3001", + "TODO_API_URL": "http://localhost:3018", + "CALENDAR_API_URL": "http://localhost:3016", + "CLOCK_API_URL": "http://localhost:3017", + "CONTACTS_API_URL": "http://localhost:3015" +} diff --git a/apps/manacore/apps/web/static/config.json.template b/apps/manacore/apps/web/static/config.json.template new file mode 100644 index 000000000..6f228d09d --- /dev/null +++ b/apps/manacore/apps/web/static/config.json.template @@ -0,0 +1,8 @@ +{ + "API_BASE_URL": "${API_BASE_URL}", + "AUTH_URL": "${AUTH_URL}", + "TODO_API_URL": "${TODO_API_URL}", + "CALENDAR_API_URL": "${CALENDAR_API_URL}", + "CLOCK_API_URL": "${CLOCK_API_URL}", + "CONTACTS_API_URL": "${CONTACTS_API_URL}" +} diff --git a/apps/picture/apps/web/package.json b/apps/picture/apps/web/package.json index 35e3930db..98fc49350 100644 --- a/apps/picture/apps/web/package.json +++ b/apps/picture/apps/web/package.json @@ -34,7 +34,8 @@ "@picture/shared": "workspace:*", "konva": "^10.0.2", "posthog-js": "^1.273.1", - "svelte-i18n": "^4.0.1" + "svelte-i18n": "^4.0.1", + "zod": "^4.2.0" }, "devDependencies": { "@eslint/compat": "^1.4.0", diff --git a/apps/picture/apps/web/src/lib/api/client.ts b/apps/picture/apps/web/src/lib/api/client.ts index 3cac5de48..a18cc23be 100644 --- a/apps/picture/apps/web/src/lib/api/client.ts +++ b/apps/picture/apps/web/src/lib/api/client.ts @@ -6,12 +6,12 @@ * - Uses authStore.getValidToken() which automatically refreshes expired tokens * - The fetch interceptor (setupFetchInterceptor) handles 401 responses by refreshing and retrying * - If refresh fails, the request fails and user should be redirected to login + * + * Uses runtime configuration for 12-factor compliance. */ -import { env } from '$env/dynamic/public'; import { authStore } from '$lib/stores/auth.svelte'; - -const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3006'; +import { getBackendUrl } from '$lib/config/runtime'; type FetchOptions = { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; @@ -29,6 +29,9 @@ export async function fetchApi( // Get a valid token (auto-refreshes if expired) const authToken = token || (await authStore.getValidToken()); + // Get backend URL from runtime config + const backendUrl = await getBackendUrl(); + try { const headers: Record = {}; @@ -41,7 +44,7 @@ export async function fetchApi( headers['Authorization'] = `Bearer ${authToken}`; } - const response = await fetch(`${API_BASE}/api/v1${endpoint}`, { + const response = await fetch(`${backendUrl}/api/v1${endpoint}`, { method, headers, body: isFormData ? (body as FormData) : body ? JSON.stringify(body) : undefined, @@ -81,6 +84,9 @@ export async function uploadFile( // Get a valid token (auto-refreshes if expired) const authToken = token || (await authStore.getValidToken()); + // Get backend URL from runtime config + const backendUrl = await getBackendUrl(); + try { const formData = new FormData(); formData.append('file', file); @@ -90,7 +96,7 @@ export async function uploadFile( headers['Authorization'] = `Bearer ${authToken}`; } - const response = await fetch(`${API_BASE}/api/v1${endpoint}`, { + const response = await fetch(`${backendUrl}/api/v1${endpoint}`, { method: 'POST', headers, body: formData, @@ -125,6 +131,9 @@ export async function uploadFiles( // Get a valid token (auto-refreshes if expired) const authToken = token || (await authStore.getValidToken()); + // Get backend URL from runtime config + const backendUrl = await getBackendUrl(); + try { const formData = new FormData(); files.forEach((file) => { @@ -136,7 +145,7 @@ export async function uploadFiles( headers['Authorization'] = `Bearer ${authToken}`; } - const response = await fetch(`${API_BASE}/api/v1${endpoint}`, { + const response = await fetch(`${backendUrl}/api/v1${endpoint}`, { method: 'POST', headers, body: formData, diff --git a/apps/picture/apps/web/src/lib/config/runtime.ts b/apps/picture/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..7d462a686 --- /dev/null +++ b/apps/picture/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,124 @@ +/** + * Runtime Configuration Loader + * + * Implements 12-factor app "Config in Environment" principle. + * Configuration is loaded at runtime from /config.json generated by Docker entrypoint, + * allowing the same Docker image to work across all environments. + * + * Pattern: Client-only SPA (SSR disabled via +layout.ts) + * - Browser: Fetches /config.json (generated by docker-entrypoint.sh) + * - Validation: Enforces schema in production (fail hard on misconfiguration) + * - Dev fallback: Only when dev=true, never in staging/prod + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + BACKEND_URL: string; + AUTH_URL: string; +} + +const ConfigSchema = z.object({ + BACKEND_URL: z.string().url().min(1, 'BACKEND_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), +}); + +// Development fallback configuration (only used when dev=true) +const DEV_CONFIG: RuntimeConfig = { + BACKEND_URL: 'http://localhost:3006', + AUTH_URL: 'http://localhost:3001', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load runtime configuration from /config.json + * Uses caching to avoid multiple fetches + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Picture] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Picture] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // If already loading, return the existing promise + if (configPromise) { + return configPromise; + } + + // Fetch config from /config.json (generated by docker-entrypoint.sh) + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Picture] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Picture] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + const errors = result.error.errors + .map((e: { path: (string | number)[]; message: string }) => `${e.path.join('.')}: ${e.message}`) + .join(', '); + throw new Error(`[Picture] Invalid config.json schema: ${errors}`); + } + } + + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get the full runtime configuration + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get the Auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Get the Backend API URL + */ +export async function getBackendUrl(): Promise { + const config = await getConfig(); + return config.BACKEND_URL; +} + +/** + * Initialize runtime configuration + * Call this early in app lifecycle (e.g., +layout.svelte onMount) + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts index b63abb804..60aac106e 100644 --- a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts @@ -1,13 +1,10 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Now using Mana Core Auth instead of Supabase Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; -import { env } from '$env/dynamic/public'; - -const MANA_AUTH_URL = env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -const BACKEND_URL = env.PUBLIC_BACKEND_URL || 'http://localhost:3006'; +import { getAuthUrl, getBackendUrl } from '$lib/config/runtime'; export interface UserData { id: string; @@ -28,10 +25,12 @@ async function getAuthService() { if (!browser) return null; if (!_authService) { try { + const authUrl = await getAuthUrl(); + const backendUrl = await getBackendUrl(); const { initializeWebAuth } = await import('@manacore/shared-auth'); const auth = initializeWebAuth({ - baseUrl: MANA_AUTH_URL, - backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; diff --git a/apps/picture/apps/web/src/routes/+layout.svelte b/apps/picture/apps/web/src/routes/+layout.svelte index 6c437df8b..c08fa5207 100644 --- a/apps/picture/apps/web/src/routes/+layout.svelte +++ b/apps/picture/apps/web/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import Toast from '$lib/components/ui/Toast.svelte'; import { onMount } from 'svelte'; import { initPostHog, analytics } from '$lib/analytics/posthog'; + import { initializeConfig } from '$lib/config/runtime'; // Import and initialize theme import { theme } from '$lib/stores/theme'; @@ -15,25 +16,28 @@ let { children, data } = $props(); onMount(() => { - // Initialize theme (applies CSS variables and loads from localStorage) - const cleanupTheme = theme.initialize(); + // Initialize runtime config first (12-factor pattern) + initializeConfig().then(() => { + // Initialize theme (applies CSS variables and loads from localStorage) + const cleanupTheme = theme.initialize(); - // Initialize PostHog - initPostHog(); + // Initialize PostHog + initPostHog(); - // Initialize auth with Mana Core - authStore.initialize().then(() => { - // Identify user in PostHog if logged in - if (authStore.user) { - analytics.identify(authStore.user.id, { - email: authStore.user.email, - }); - } + // Initialize auth with Mana Core + authStore.initialize().then(() => { + // Identify user in PostHog if logged in + if (authStore.user) { + analytics.identify(authStore.user.id, { + email: authStore.user.email, + }); + } + }); + + return () => { + cleanupTheme(); + }; }); - - return () => { - cleanupTheme(); - }; }); diff --git a/apps/picture/apps/web/src/routes/+layout.ts b/apps/picture/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/picture/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/picture/apps/web/static/config.json b/apps/picture/apps/web/static/config.json new file mode 100644 index 000000000..9e4daa477 --- /dev/null +++ b/apps/picture/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "BACKEND_URL": "http://localhost:3006", + "AUTH_URL": "http://localhost:3001" +} diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 0df3cd1fd..f54b340ed 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -137,12 +137,10 @@ services: environment: NODE_ENV: staging PORT: 3000 - # Server-side URLs (Docker internal network) - PUBLIC_BACKEND_URL: http://chat-backend:3002 - PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 - # Client-side URLs (browser access via HTTPS staging domains) - PUBLIC_BACKEND_URL_CLIENT: https://chat-api.staging.manacore.ai - PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai + # Runtime config generation (12-factor pattern) + # These vars are used by docker-entrypoint.sh to generate /config.json + BACKEND_URL: https://chat-api.staging.manacore.ai + AUTH_URL: https://auth.staging.manacore.ai ports: - "3000:3000" healthcheck: @@ -173,16 +171,14 @@ services: environment: NODE_ENV: staging PORT: 5173 - # Auth URLs - PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 - PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai - # Backend URLs for dashboard widgets - PUBLIC_TODO_API_URL: http://todo-backend:3018 - PUBLIC_TODO_API_URL_CLIENT: https://todo-api.staging.manacore.ai - PUBLIC_CALENDAR_API_URL: http://calendar-backend:3016 - PUBLIC_CALENDAR_API_URL_CLIENT: https://calendar-api.staging.manacore.ai - PUBLIC_CLOCK_API_URL: http://clock-backend:3017 - PUBLIC_CLOCK_API_URL_CLIENT: https://clock-api.staging.manacore.ai + # Runtime config generation (12-factor pattern) + # These vars are used by docker-entrypoint.sh to generate /config.json + API_BASE_URL: https://staging.manacore.ai + AUTH_URL: https://auth.staging.manacore.ai + TODO_API_URL: https://todo-api.staging.manacore.ai + CALENDAR_API_URL: https://calendar-api.staging.manacore.ai + CLOCK_API_URL: https://clock-api.staging.manacore.ai + CONTACTS_API_URL: https://contacts-api.staging.manacore.ai ports: - "5173:5173" healthcheck: @@ -315,10 +311,10 @@ services: environment: NODE_ENV: staging PORT: 5186 - PUBLIC_BACKEND_URL: http://calendar-backend:3016 - PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 - PUBLIC_BACKEND_URL_CLIENT: https://calendar-api.staging.manacore.ai - PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai + # Runtime config generation (12-factor pattern) + # These vars are used by docker-entrypoint.sh to generate /config.json + BACKEND_URL: https://calendar-api.staging.manacore.ai + AUTH_URL: https://auth.staging.manacore.ai ports: - "5186:5186" healthcheck: @@ -383,10 +379,10 @@ services: environment: NODE_ENV: staging PORT: 5187 - PUBLIC_BACKEND_URL: http://clock-backend:3017 - PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 - PUBLIC_BACKEND_URL_CLIENT: https://clock-api.staging.manacore.ai - PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.staging.manacore.ai + # Runtime config generation (12-factor pattern) + # These vars are used by docker-entrypoint.sh to generate /config.json + API_BASE_URL: https://clock-api.staging.manacore.ai + AUTH_URL: https://auth.staging.manacore.ai ports: - "5187:5187" healthcheck: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3403484a5..c434d48a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,7 +116,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -149,7 +149,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -173,14 +173,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -189,13 +189,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) + version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) + version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -552,19 +552,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -2426,6 +2426,9 @@ importers: svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@5.44.0) + zod: + specifier: ^4.2.0 + version: 4.2.0 devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -2874,7 +2877,7 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.65.0 - version: 0.65.0(zod@4.1.13) + version: 0.65.0(zod@4.2.0) '@bacons/apple-targets': specifier: ^3.0.2 version: 3.0.5 @@ -3560,7 +3563,7 @@ importers: version: 9.39.1 '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -3596,7 +3599,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6788,7 +6791,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -19700,6 +19703,9 @@ packages: zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zod@4.2.0: + resolution: {integrity: sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -19844,11 +19850,11 @@ snapshots: optionalDependencies: zod: 3.25.76 - '@anthropic-ai/sdk@0.65.0(zod@4.1.13)': + '@anthropic-ai/sdk@0.65.0(zod@4.2.0)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.1.13 + zod: 4.2.0 '@asamuzakjp/css-color@4.1.0': dependencies: @@ -19978,16 +19984,6 @@ snapshots: transitivePeerDependencies: - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': - dependencies: - astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) - autoprefixer: 10.4.22(postcss@8.5.6) - postcss: 8.5.6 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) - tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -22501,7 +22497,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) + expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22655,7 +22651,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e) + expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -23986,43 +23982,6 @@ snapshots: - supports-color - ts-node - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.2.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -27380,17 +27339,17 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 pretty-format: 30.2.0 react: 19.1.0 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': @@ -27924,16 +27883,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -27982,15 +27941,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -28082,14 +28041,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -28121,14 +28080,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -28254,12 +28213,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -28290,12 +28249,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -28477,15 +28436,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -28516,13 +28475,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript @@ -29323,108 +29282,6 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): - dependencies: - '@astrojs/compiler': 2.13.0 - '@astrojs/internal-helpers': 0.7.5 - '@astrojs/markdown-remark': 6.3.9 - '@astrojs/telemetry': 3.3.0 - '@capsizecss/unpack': 3.0.1 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - boxen: 8.0.1 - ci-info: 4.3.1 - clsx: 2.1.1 - common-ancestor-path: 1.0.1 - cookie: 1.1.0 - cssesc: 3.0.0 - debug: 4.4.3 - deterministic-object-hash: 2.0.2 - devalue: 5.5.0 - diff: 5.2.0 - dlv: 1.1.3 - dset: 3.1.4 - es-module-lexer: 1.7.0 - esbuild: 0.25.12 - estree-walker: 3.0.3 - flattie: 1.1.1 - fontace: 0.3.1 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - import-meta-resolve: 4.2.0 - js-yaml: 4.1.1 - magic-string: 0.30.21 - magicast: 0.5.1 - mrmime: 2.0.1 - neotraverse: 0.6.18 - p-limit: 6.2.0 - p-queue: 8.1.1 - package-manager-detector: 1.5.0 - piccolore: 0.1.3 - picomatch: 4.0.3 - prompts: 2.4.2 - rehype: 13.0.2 - semver: 7.7.3 - shiki: 3.15.0 - smol-toml: 1.5.2 - svgo: 4.0.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tsconfck: 3.1.6(typescript@5.9.3) - ultrahtml: 1.6.0 - unifont: 0.6.0 - unist-util-visit: 5.0.0 - unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.8.2) - vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - xxhash-wasm: 1.1.0 - yargs-parser: 21.1.1 - yocto-spinner: 0.2.3 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -31710,11 +31567,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - semver: 7.7.3 - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31725,9 +31577,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -31742,9 +31594,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -31762,14 +31614,14 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31794,17 +31646,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -31842,7 +31694,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -31853,22 +31705,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -31882,12 +31719,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -31902,39 +31739,25 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.48.0 - astro-eslint-parser: 1.2.2 - eslint: 9.39.1(jiti@1.21.7) - eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) - globals: 16.5.0 - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -31958,6 +31781,12 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-utils: 2.1.0 + regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32011,7 +31840,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -32020,9 +31849,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -32034,7 +31863,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -32069,7 +31898,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -32080,7 +31909,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -32098,7 +31927,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -32109,7 +31938,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -32137,6 +31966,16 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) + eslint-utils: 2.1.0 + ignore: 5.3.2 + minimatch: 3.1.2 + resolve: 1.22.11 + semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32167,6 +32006,16 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32191,6 +32040,10 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32221,6 +32074,28 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.39.1(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -33440,21 +33315,21 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): + expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33462,10 +33337,10 @@ snapshots: query-string: 7.1.3 react: 19.1.0 react-fast-compare: 3.2.2 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) semver: 7.6.3 server-only: 0.0.1 sf-symbols-typescript: 2.1.0 @@ -33473,13 +33348,13 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@types/react' @@ -35629,15 +35504,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35819,7 +35694,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35846,9 +35721,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.19.12) - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + '@types/node': 20.19.25 + esbuild-register: 3.6.0(esbuild@0.27.0) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -36509,12 +36383,12 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -40519,16 +40393,6 @@ snapshots: webpack: 5.100.2(esbuild@0.27.0) webpack-sources: 3.3.3 - react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - acorn-loose: 8.5.2 - neo-async: 2.6.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - webpack: 5.97.1(esbuild@0.19.12) - webpack-sources: 3.3.3 - optional: true - react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -41802,6 +41666,17 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.100.2(esbuild@0.19.12) + optionalDependencies: + esbuild: 0.19.12 + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -42067,6 +41942,16 @@ snapshots: babel-jest: 30.2.0(@babel/core@7.28.5) jest-util: 30.2.0 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.100.2(esbuild@0.19.12) + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: chalk: 4.1.2 @@ -42087,16 +41972,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -42703,23 +42578,6 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 - vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.53.3 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.25 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.30.2 - terser: 5.44.1 - tsx: 4.20.6 - yaml: 2.8.1 - vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -42822,10 +42680,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): - optionalDependencies: - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -43114,6 +42968,38 @@ snapshots: - esbuild - uglify-js + webpack@5.100.2(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.100.2(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 @@ -43572,6 +43458,8 @@ snapshots: zod@4.1.13: {} + zod@4.2.0: {} + zustand@4.5.7(@types/react@19.2.7)(react@19.1.0): dependencies: use-sync-external-store: 1.6.0(react@19.1.0) diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index 980d23887..a6841ff79 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -173,7 +173,7 @@ const APP_CONFIGS = [ vars: { PUBLIC_SUPABASE_URL: (env) => env.MANACORE_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY: (env) => env.MANACORE_SUPABASE_ANON_KEY, - MIDDLEWARE_URL: (env) => env.MANA_CORE_AUTH_URL, + PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, }, },