diff --git a/.env.development b/.env.development index 92535d6e6..484e7d955 100644 --- a/.env.development +++ b/.env.development @@ -130,12 +130,19 @@ MANADECK_SUPABASE_ANON_KEY=your-supabase-anon-key PICTURE_BACKEND_PORT=3006 PICTURE_BACKEND_URL=http://localhost:3006 -PICTURE_DATABASE_URL=postgresql://picture:picturepassword@localhost:5434/picture +PICTURE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/picture + +# Replicate API Token for AI image generation +PICTURE_REPLICATE_API_TOKEN=r8_QlvkstNhIc6NBX1ktpQ6ibvzOE2d2UQ1Emamd # Storage Configuration (uses MinIO locally, Hetzner in production) # Uses shared S3_* variables from above - no project-specific override needed for local dev PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-storage +# Credit System (staging only - freemium: 3 free images, then credits) +PICTURE_APP_ID=picture-app +PICTURE_MANA_CORE_SERVICE_KEY= + # OAuth (optional - leave empty to disable) PICTURE_GOOGLE_CLIENT_ID= PICTURE_APPLE_CLIENT_ID= diff --git a/apps/chat/CLAUDE.md b/apps/chat/CLAUDE.md index 8dc0f8a9b..107216de4 100644 --- a/apps/chat/CLAUDE.md +++ b/apps/chat/CLAUDE.md @@ -66,7 +66,7 @@ pnpm preview # Preview production build - **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router - **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4 - **Landing**: Astro 5.16, Tailwind CSS -- **Backend**: NestJS 10, Google Gemini AI, Supabase +- **Backend**: NestJS 10, OpenRouter/Gemini AI, Supabase - **Types**: TypeScript 5.x ## Architecture @@ -89,11 +89,12 @@ pnpm preview # Preview production build #### Backend (.env) ``` -GOOGLE_GENAI_API_KEY=... +OPENROUTER_API_KEY=... # Get at https://openrouter.ai/keys +GOOGLE_GENAI_API_KEY=... # Optional: For Gemini models SUPABASE_URL=https://... SUPABASE_SERVICE_KEY=... PORT=3002 -DEV_BYPASS_AUTH=true # Optional: Skip auth in development +DEV_BYPASS_AUTH=true # Optional: Skip auth in development ``` #### Mobile (.env) @@ -114,11 +115,33 @@ EXPO_PUBLIC_BACKEND_URL=http://localhost:3001 ## AI Models Available -| Model ID | Name | Description | Default | -| ------------------------------------ | --------------------- | ------------------------- | ------- | -| 550e8400-e29b-41d4-a716-446655440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes | -| 550e8400-e29b-41d4-a716-446655440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No | -| 550e8400-e29b-41d4-a716-446655440103 | Gemini 2.5 Pro | Most capable model | No | +### OpenRouter Models (Recommended) + +| Model ID | Name | Price | Best For | +| -------- | ---- | ----- | -------- | +| ...440201 | Llama 3.1 8B | $0.05/M | Everyday tasks, cheap | +| ...440202 | Llama 3.1 70B | $0.35/M | Complex reasoning | +| ...440203 | DeepSeek V3 | $0.14/M | Reasoning at low cost | +| ...440204 | Mistral Small | $0.10/M | General tasks | +| ...440205 | Claude 3.5 Sonnet | $3/M | Best quality | +| ...440206 | GPT-4o Mini | $0.15/M | Balanced performance | + +### Google Gemini Models + +| Model ID | Name | Description | Default | +| -------- | ---- | ----------- | ------- | +| ...440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes | +| ...440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No | +| ...440103 | Gemini 2.5 Pro | Most capable model | No | + +## OpenRouter Setup + +To enable OpenRouter models: + +- [ ] Get API key at https://openrouter.ai/keys +- [ ] Add `OPENROUTER_API_KEY=sk-or-v1-xxx` to `apps/chat/apps/backend/.env` +- [ ] Re-seed database: `pnpm --filter @chat/backend db:seed` +- [ ] Test: `pnpm dev:chat:backend` ## Important Notes diff --git a/apps/chat/apps/backend/.env.example b/apps/chat/apps/backend/.env.example index 2ba78b004..f011962ae 100644 --- a/apps/chat/apps/backend/.env.example +++ b/apps/chat/apps/backend/.env.example @@ -1,4 +1,11 @@ -# Azure OpenAI Configuration +# OpenRouter Configuration (Recommended - multi-model access) +# Get your API key at https://openrouter.ai/keys +OPENROUTER_API_KEY=your-openrouter-api-key + +# Google Gemini Configuration +GOOGLE_GENAI_API_KEY=your-google-api-key + +# Azure OpenAI Configuration (Optional) AZURE_OPENAI_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com AZURE_OPENAI_API_KEY=your-api-key-here AZURE_OPENAI_API_VERSION=2024-12-01-preview diff --git a/apps/chat/apps/backend/src/chat/chat.service.ts b/apps/chat/apps/backend/src/chat/chat.service.ts index 30b3f572e..edc00e97a 100644 --- a/apps/chat/apps/backend/src/chat/chat.service.ts +++ b/apps/chat/apps/backend/src/chat/chat.service.ts @@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { eq } from 'drizzle-orm'; import { AsyncResult, ok, err, ValidationError, ServiceError } from '@manacore/shared-errors'; import { GoogleGenerativeAI } from '@google/generative-ai'; +import OpenAI from 'openai'; import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; import { models } from '../db/schema/models.schema'; @@ -19,6 +20,8 @@ export class ChatService { private readonly azureApiVersion: string; // Google Gemini config private readonly geminiClient: GoogleGenerativeAI | null = null; + // OpenRouter config + private readonly openRouterClient: OpenAI | null = null; constructor( private configService: ConfigService, @@ -41,6 +44,22 @@ export class ChatService { this.logger.warn('GOOGLE_GENAI_API_KEY is not set - Gemini models unavailable'); } + // OpenRouter setup + const openRouterApiKey = this.configService.get('OPENROUTER_API_KEY'); + if (openRouterApiKey) { + this.openRouterClient = new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: openRouterApiKey, + defaultHeaders: { + 'HTTP-Referer': this.configService.get('APP_URL') || 'http://localhost:3002', + 'X-Title': 'Mana Chat', + }, + }); + this.logger.log('OpenRouter client initialized'); + } else { + this.logger.warn('OPENROUTER_API_KEY is not set - OpenRouter models unavailable'); + } + if (!this.azureApiKey) { this.logger.warn('AZURE_OPENAI_API_KEY is not set - Azure models unavailable'); } @@ -84,6 +103,8 @@ export class ChatService { // Route to appropriate provider if (model.provider === 'gemini') { return this.createGeminiCompletion(model, dto); + } else if (model.provider === 'openrouter') { + return this.createOpenRouterCompletion(model, dto); } else { return this.createAzureCompletion(model, dto); } @@ -250,4 +271,62 @@ export class ChatService { ); } } + + private async createOpenRouterCompletion( + model: Model, + dto: ChatCompletionDto + ): AsyncResult { + if (!this.openRouterClient) { + return err(ServiceError.externalError('OpenRouter', 'OpenRouter client not configured')); + } + + const params = model.parameters as { + model?: string; + temperature?: number; + max_tokens?: number; + } | null; + + const modelName = params?.model || 'meta-llama/llama-3.1-8b-instruct'; + const temperature = dto.temperature ?? params?.temperature ?? 0.7; + const maxTokens = dto.maxTokens ?? params?.max_tokens ?? 4096; + + this.logger.log(`Sending request to OpenRouter model: ${modelName}`); + + try { + const response = await this.openRouterClient.chat.completions.create({ + model: modelName, + messages: dto.messages.map((msg) => ({ + role: msg.role as 'system' | 'user' | 'assistant', + content: msg.content, + })), + temperature, + max_tokens: maxTokens, + }); + + const messageContent = response.choices?.[0]?.message?.content; + + if (!messageContent) { + this.logger.warn('No message content in OpenRouter response'); + return err(ServiceError.generationFailed('OpenRouter', 'No response generated')); + } + + return ok({ + content: messageContent, + usage: { + prompt_tokens: response.usage?.prompt_tokens || 0, + completion_tokens: response.usage?.completion_tokens || 0, + total_tokens: response.usage?.total_tokens || 0, + }, + }); + } catch (error) { + this.logger.error('Error calling OpenRouter API', error); + return err( + ServiceError.generationFailed( + 'OpenRouter', + error instanceof Error ? error.message : 'Unknown error', + error instanceof Error ? error : undefined + ) + ); + } + } } diff --git a/apps/chat/apps/backend/src/db/seed.ts b/apps/chat/apps/backend/src/db/seed.ts index f515c8216..c478cac10 100644 --- a/apps/chat/apps/backend/src/db/seed.ts +++ b/apps/chat/apps/backend/src/db/seed.ts @@ -75,6 +75,87 @@ async function seed() { isDefault: false, }, // ============================================ + // OpenRouter Models (Multi-provider, cost-effective) + // ============================================ + { + id: '550e8400-e29b-41d4-a716-446655440201', + name: 'Llama 3.1 8B', + description: 'Fast & cheap - great for everyday tasks ($0.05/M tokens)', + provider: 'openrouter', + parameters: { + model: 'meta-llama/llama-3.1-8b-instruct', + temperature: 0.7, + max_tokens: 4096, + }, + isActive: true, + isDefault: false, + }, + { + id: '550e8400-e29b-41d4-a716-446655440202', + name: 'Llama 3.1 70B', + description: 'Powerful open model - complex reasoning ($0.35/M tokens)', + provider: 'openrouter', + parameters: { + model: 'meta-llama/llama-3.1-70b-instruct', + temperature: 0.7, + max_tokens: 8192, + }, + isActive: true, + isDefault: false, + }, + { + id: '550e8400-e29b-41d4-a716-446655440203', + name: 'DeepSeek V3', + description: 'Excellent reasoning at low cost ($0.14/M tokens)', + provider: 'openrouter', + parameters: { + model: 'deepseek/deepseek-chat', + temperature: 0.7, + max_tokens: 8192, + }, + isActive: true, + isDefault: false, + }, + { + id: '550e8400-e29b-41d4-a716-446655440204', + name: 'Mistral Small', + description: 'Fast European model - good for general tasks ($0.10/M tokens)', + provider: 'openrouter', + parameters: { + model: 'mistralai/mistral-small-24b-instruct-2501', + temperature: 0.7, + max_tokens: 4096, + }, + isActive: true, + isDefault: false, + }, + { + id: '550e8400-e29b-41d4-a716-446655440205', + name: 'Claude 3.5 Sonnet', + description: 'Best overall quality - coding & analysis ($3/M tokens)', + provider: 'openrouter', + parameters: { + model: 'anthropic/claude-3.5-sonnet', + temperature: 0.7, + max_tokens: 8192, + }, + isActive: true, + isDefault: false, + }, + { + id: '550e8400-e29b-41d4-a716-446655440206', + name: 'GPT-4o Mini', + description: 'OpenAI fast model - balanced performance ($0.15/M tokens)', + provider: 'openrouter', + parameters: { + model: 'openai/gpt-4o-mini', + temperature: 0.7, + max_tokens: 4096, + }, + isActive: true, + isDefault: false, + }, + // ============================================ // Azure OpenAI GPT-5 Family (Inactive - no deployment) // ============================================ { diff --git a/apps/picture/CLAUDE.md b/apps/picture/CLAUDE.md new file mode 100644 index 000000000..918932504 --- /dev/null +++ b/apps/picture/CLAUDE.md @@ -0,0 +1,194 @@ +# Picture App - CLAUDE.md + +AI image generation app using Replicate API with freemium credit system. + +## Project Structure + +``` +apps/picture/ +├── apps/ +│ ├── backend/ # NestJS API (port 3006) +│ ├── mobile/ # Expo React Native app +│ ├── web/ # SvelteKit web app +│ └── landing/ # Astro marketing page +└── packages/ # Shared code +``` + +## Quick Start + +```bash +# From monorepo root +pnpm dev:picture:full # Start backend + web + auto DB setup + +# Individual apps +pnpm --filter @picture/backend dev # Backend only (port 3006) +pnpm --filter @picture/web dev # Web only +pnpm --filter @picture/mobile dev # Mobile only +``` + +## Backend Architecture + +### Key Services + +| Service | Purpose | +|---------|---------| +| `GenerateService` | AI image generation with freemium/credit logic | +| `ReplicateService` | Replicate API integration | +| `StorageService` | MinIO/S3 storage via `@manacore/shared-storage` | +| `CreditClientService` | Credit system via `@mana-core/nestjs-integration` | + +### Freemium Model + +- **Free tier**: 3 free generations per user +- **Paid tier**: 10 credits per generation +- **Enforcement**: Only in staging (`NODE_ENV=staging`) +- **Development**: Fail-open (no credit enforcement) + +### Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `REPLICATE_API_TOKEN` | Replicate API key | Yes | +| `DATABASE_URL` | PostgreSQL connection | Yes | +| `S3_ENDPOINT` | MinIO/S3 endpoint | Yes | +| `MANA_CORE_AUTH_URL` | Auth service URL | Yes | +| `MANA_CORE_SERVICE_KEY` | Service key for credits | Staging only | +| `APP_ID` | App identifier | Yes | + +--- + +## TODO List + +### Testing Required + +- [ ] **Test freemium flow with new user** + - Create new user ID and verify 3 free generations work + - Verify `freeGenerationsRemaining` decrements correctly (3 → 2 → 1 → 0) + - Verify 4th generation still works in development (fail-open) + +- [ ] **Test staging credit enforcement** + - Set `NODE_ENV=staging` and test credit check + - Verify HTTP 402 returned when credits insufficient + - Test with valid `MANA_CORE_SERVICE_KEY` + +- [ ] **Test async generation (webhook mode)** + - Test generation without `waitForResult: true` + - Verify webhook receives completion callback + - Verify credits consumed on webhook success + +- [ ] **Test error handling** + - Test with invalid model ID + - Test with invalid Replicate API token + - Test storage upload failures + +- [ ] **Integration tests** + - Write Jest tests for `GenerateService` + - Mock `CreditClientService` calls + - Test all generation paths (free/paid, sync/async) + +### Features to Implement + +- [ ] **Add credit balance endpoint** + - GET `/api/v1/credits/balance` - Return user's credit balance + - Use `CreditClientService.getBalance()` + +- [ ] **Add generation history endpoint** + - GET `/api/v1/generate/history` - User's generation history + - Include credits used per generation + +- [ ] **Improve error messages** + - Add proper error codes for credit failures + - Return helpful messages for insufficient credits + +- [ ] **Rate limiting** + - Add rate limits for generation endpoints + - Prevent abuse of free tier + +### Web App Tasks + +- [ ] **Show free generations remaining** + - Display counter in UI + - Show warning when approaching limit + +- [ ] **Credit purchase flow** + - Integrate with mana-core credit purchase + - Show credit balance in header + +- [ ] **Generation queue UI** + - Show pending generations + - Poll for status updates + +### Mobile App Tasks + +- [ ] **Implement generation screen** + - Model selection + - Prompt input with suggestions + - Generation progress indicator + +- [ ] **Gallery view** + - Grid view of user's generated images + - Favorites functionality + +### DevOps Tasks + +- [ ] **Staging deployment** + - Deploy backend to staging server + - Configure `MANA_CORE_SERVICE_KEY` in staging + - Test credit system end-to-end + +- [ ] **Monitoring** + - Add logging for credit operations + - Track generation success/failure rates + - Monitor Replicate API usage + +--- + +## API Endpoints + +### Generate + +```bash +# Generate image (sync) +POST /api/v1/generate +{ + "prompt": "A beautiful sunset", + "modelId": "uuid", + "waitForResult": true +} + +# Check status +GET /api/v1/generate/:id/status + +# Cancel generation +DELETE /api/v1/generate/:id + +# Webhook (internal) +POST /api/v1/generate/webhook +``` + +### Models + +```bash +GET /api/v1/models # List all models +GET /api/v1/models/:id # Get model details +``` + +### Images + +```bash +GET /api/v1/images # List user's images +GET /api/v1/images/:id # Get image details +DELETE /api/v1/images/:id # Delete image +``` + +--- + +## Recent Changes + +### 2025-12-10: Credit System Integration + +- Added `@mana-core/nestjs-integration` for credit system +- Implemented freemium model (3 free, then 10 credits) +- Credit enforcement only in staging environment +- Updated `GenerateService` with `checkGenerationAccess()` +- Response includes `freeGenerationsRemaining` count diff --git a/apps/picture/apps/backend/package.json b/apps/picture/apps/backend/package.json index 19ae5fa39..fce788b62 100644 --- a/apps/picture/apps/backend/package.json +++ b/apps/picture/apps/backend/package.json @@ -19,8 +19,10 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.700.0", + "@mana-core/nestjs-integration": "workspace:*", "@manacore/shared-errors": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", + "@manacore/shared-storage": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/apps/picture/apps/backend/src/app.module.ts b/apps/picture/apps/backend/src/app.module.ts index 214475235..b829e6ec0 100644 --- a/apps/picture/apps/backend/src/app.module.ts +++ b/apps/picture/apps/backend/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ManaCoreModule } from '@mana-core/nestjs-integration'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; import { ModelModule } from './model/model.module'; @@ -19,6 +20,16 @@ import { BatchModule } from './batch/batch.module'; isGlobal: true, envFilePath: '.env', }), + ManaCoreModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + appId: configService.get('APP_ID', 'picture-app'), + serviceKey: configService.get('MANA_CORE_SERVICE_KEY', ''), + authUrl: configService.get('MANA_CORE_AUTH_URL', 'http://localhost:3001'), + debug: configService.get('NODE_ENV') === 'development', + }), + inject: [ConfigService], + }), DatabaseModule, HealthModule, ModelModule, diff --git a/apps/picture/apps/backend/src/generate/generate.service.ts b/apps/picture/apps/backend/src/generate/generate.service.ts index 8d95cbd07..1c87dbb6a 100644 --- a/apps/picture/apps/backend/src/generate/generate.service.ts +++ b/apps/picture/apps/backend/src/generate/generate.service.ts @@ -1,6 +1,15 @@ -import { Injectable, Inject, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { + Injectable, + Inject, + NotFoundException, + ForbiddenException, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { eq } from 'drizzle-orm'; +import { eq, and, count } from 'drizzle-orm'; +import { CreditClientService } from '@mana-core/nestjs-integration'; import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; import { imageGenerations, images, models } from '../db/schema'; @@ -9,25 +18,104 @@ import { ReplicateService, GenerationParams } from './replicate.service'; import { StorageService } from '../upload/storage.service'; import { GenerateImageDto } from './dto/generate.dto'; +const FREE_GENERATIONS_LIMIT = 3; +const CREDITS_PER_GENERATION = 10; + export interface GenerateResponse { generationId: string; status: string; image?: Image; + creditsUsed?: number; + freeGenerationsRemaining?: number; } @Injectable() export class GenerateService { private readonly logger = new Logger(GenerateService.name); private readonly webhookBaseUrl: string; + private readonly isStaging: boolean; constructor( @Inject(DATABASE_CONNECTION) private readonly db: Database, private readonly replicateService: ReplicateService, private readonly storageService: StorageService, + private readonly creditClient: CreditClientService, private configService: ConfigService ) { this.webhookBaseUrl = this.configService.get('WEBHOOK_BASE_URL') || 'http://localhost:3003'; + // Freemium/credit system only enforced in staging + this.isStaging = this.configService.get('NODE_ENV') === 'staging'; + } + + /** + * Get count of completed generations for a user + */ + private async getUserGenerationCount(userId: string): Promise { + const result = await this.db + .select({ count: count() }) + .from(imageGenerations) + .where(and(eq(imageGenerations.userId, userId), eq(imageGenerations.status, 'completed'))); + return result[0]?.count ?? 0; + } + + /** + * Check if user can generate (has free generations or credits) + * Returns: { canGenerate, isFree, freeRemaining, creditsRequired } + */ + async checkGenerationAccess(userId: string): Promise<{ + canGenerate: boolean; + isFree: boolean; + freeGenerationsRemaining: number; + creditsRequired: number; + currentBalance?: number; + }> { + const completedCount = await this.getUserGenerationCount(userId); + const freeRemaining = Math.max(0, FREE_GENERATIONS_LIMIT - completedCount); + + // If user has free generations, they can proceed + if (freeRemaining > 0) { + return { + canGenerate: true, + isFree: true, + freeGenerationsRemaining: freeRemaining, + creditsRequired: 0, + }; + } + + // No free generations - check credits (only in staging) + if (!this.isStaging) { + // In development/production without credit enforcement, allow generation + return { + canGenerate: true, + isFree: false, + freeGenerationsRemaining: 0, + creditsRequired: CREDITS_PER_GENERATION, + }; + } + + // In staging, check actual credit balance + try { + const balance = await this.creditClient.getBalance(userId); + const hasEnoughCredits = balance.balance >= CREDITS_PER_GENERATION; + + return { + canGenerate: hasEnoughCredits, + isFree: false, + freeGenerationsRemaining: 0, + creditsRequired: CREDITS_PER_GENERATION, + currentBalance: balance.balance, + }; + } catch (error) { + this.logger.warn(`Failed to check credit balance for user ${userId}`, error); + // On error, allow generation (fail open for better UX) + return { + canGenerate: true, + isFree: false, + freeGenerationsRemaining: 0, + creditsRequired: CREDITS_PER_GENERATION, + }; + } } /** @@ -35,6 +123,16 @@ export class GenerateService { */ async generateImage(userId: string, dto: GenerateImageDto): Promise { try { + // Check if user can generate (freemium/credit check) + const access = await this.checkGenerationAccess(userId); + + if (!access.canGenerate) { + throw new HttpException( + `Insufficient credits. You need ${access.creditsRequired} credits. Current balance: ${access.currentBalance ?? 0}`, + HttpStatus.PAYMENT_REQUIRED + ); + } + // Get model info const modelResult = await this.db .select() @@ -70,6 +168,7 @@ export class GenerateService { .returning(); const generation = generationResult[0]; + const isFreeGeneration = access.isFree; // Build generation params const generationParams: GenerationParams = { @@ -89,13 +188,25 @@ export class GenerateService { // If waitForResult is true, use synchronous generation with polling if (dto.waitForResult) { - return this.generateSync(generation, generationParams); + const result = await this.generateSync(generation, generationParams); + + // Consume credits after successful generation (if not free) + if (result.status === 'completed' && !isFreeGeneration && this.isStaging) { + await this.consumeCreditsForGeneration(userId, generation.id); + result.creditsUsed = CREDITS_PER_GENERATION; + } + + // Add free generations remaining info + const newAccess = await this.checkGenerationAccess(userId); + result.freeGenerationsRemaining = newAccess.freeGenerationsRemaining; + + return result; } - // Otherwise use async generation with webhook - return this.generateAsync(generation, model, generationParams); + // Otherwise use async generation with webhook (credits consumed on webhook completion) + return this.generateAsync(generation, model, generationParams, isFreeGeneration); } catch (error) { - if (error instanceof NotFoundException) { + if (error instanceof NotFoundException || error instanceof HttpException) { throw error; } this.logger.error('Error generating image', error); @@ -103,6 +214,24 @@ export class GenerateService { } } + /** + * Consume credits for a generation + */ + private async consumeCreditsForGeneration(userId: string, generationId: string): Promise { + try { + await this.creditClient.consumeCredits( + userId, + 'image_generation', + CREDITS_PER_GENERATION, + `Image generation: ${generationId}` + ); + this.logger.log(`Consumed ${CREDITS_PER_GENERATION} credits for user ${userId}`); + } catch (error) { + this.logger.error(`Failed to consume credits for generation ${generationId}`, error); + // Don't fail the generation if credit consumption fails + } + } + /** * Synchronous generation - polls until complete */ @@ -200,7 +329,8 @@ export class GenerateService { private async generateAsync( generation: ImageGeneration, model: any, - params: GenerationParams + params: GenerationParams, + isFreeGeneration: boolean ): Promise { try { const webhookUrl = `${this.webhookBaseUrl}/api/generate/webhook`; @@ -212,12 +342,15 @@ export class GenerateService { webhookUrl ); - // Update generation with prediction ID + // Update generation with prediction ID and free generation flag (in metadata) await this.db .update(imageGenerations) .set({ replicatePredictionId: prediction.id, status: 'processing', + // Store isFreeGeneration in a way that can be retrieved in webhook + // We'll use the errorMessage field temporarily for metadata (cleared on success) + errorMessage: isFreeGeneration ? 'FREE_GENERATION' : null, }) .where(eq(imageGenerations.id, generation.id)); @@ -401,8 +534,16 @@ export class GenerateService { const generation = result[0]; + // Check if this was a free generation (stored in errorMessage field temporarily) + const isFreeGeneration = generation.errorMessage === 'FREE_GENERATION'; + if (status === 'succeeded' && output) { await this.processCompletedGeneration(generation, output); + + // Consume credits for paid generations in staging + if (!isFreeGeneration && this.isStaging) { + await this.consumeCreditsForGeneration(generation.userId, generation.id); + } } else if (status === 'failed') { await this.db .update(imageGenerations) diff --git a/apps/picture/apps/backend/src/upload/storage.service.ts b/apps/picture/apps/backend/src/upload/storage.service.ts index f3030e104..72e2eabf3 100644 --- a/apps/picture/apps/backend/src/upload/storage.service.ts +++ b/apps/picture/apps/backend/src/upload/storage.service.ts @@ -1,80 +1,32 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { - S3Client, - PutObjectCommand, - DeleteObjectCommand, - GetObjectCommand, -} from '@aws-sdk/client-s3'; -import * as fs from 'fs/promises'; -import * as path from 'path'; + createPictureStorage, + StorageClient, + generateUserFileKey, + getContentType, +} from '@manacore/shared-storage'; -export type StorageMode = 'local' | 's3'; +export type StorageMode = 's3'; @Injectable() -export class StorageService { +export class StorageService implements OnModuleInit { private readonly logger = new Logger(StorageService.name); - private mode: StorageMode; - private s3Client: S3Client | null = null; - private readonly bucket: string; - private readonly localStoragePath: string; - private readonly publicUrlBase: string; + private storage!: StorageClient; + private publicUrl: string; constructor(private configService: ConfigService) { - // Determine storage mode from environment - const storageMode = this.configService.get('STORAGE_MODE', 'local'); - this.mode = storageMode === 's3' ? 's3' : 'local'; - - // S3 configuration (Hetzner Object Storage is S3-compatible) - const s3Endpoint = this.configService.get('S3_ENDPOINT'); - const s3Region = this.configService.get('S3_REGION', 'eu-central-1'); - const s3AccessKey = this.configService.get('S3_ACCESS_KEY'); - const s3SecretKey = this.configService.get('S3_SECRET_KEY'); - this.bucket = this.configService.get('S3_BUCKET', 'picture-uploads'); - - // Local storage configuration - this.localStoragePath = this.configService.get( - 'LOCAL_STORAGE_PATH', - path.join(process.cwd(), 'uploads') - ); - - // Public URL base for serving files - const backendUrl = this.configService.get('BACKEND_URL', 'http://localhost:3003'); - this.publicUrlBase = this.configService.get( + // Get public URL from config + this.publicUrl = this.configService.get( 'STORAGE_PUBLIC_URL', - this.mode === 'local' ? `${backendUrl}/uploads` : `https://${this.bucket}.${s3Endpoint}` + 'http://localhost:9000/picture-storage' ); - - if (this.mode === 's3') { - if (s3Endpoint && s3AccessKey && s3SecretKey) { - this.s3Client = new S3Client({ - endpoint: s3Endpoint.startsWith('http') ? s3Endpoint : `https://${s3Endpoint}`, - region: s3Region, - credentials: { - accessKeyId: s3AccessKey, - secretAccessKey: s3SecretKey, - }, - forcePathStyle: false, // Hetzner uses virtual-hosted style - }); - this.logger.log(`Storage initialized in S3 mode (endpoint: ${s3Endpoint})`); - } else { - this.logger.warn('S3 credentials not configured, falling back to local storage'); - this.mode = 'local'; - } - } - - if (this.mode === 'local') { - this.logger.log(`Storage initialized in local mode (path: ${this.localStoragePath})`); - this.ensureLocalStorageDirectory(); - } } - private async ensureLocalStorageDirectory(): Promise { - try { - await fs.mkdir(this.localStoragePath, { recursive: true }); - } catch (error) { - this.logger.error('Failed to create local storage directory', error); - } + onModuleInit() { + // Initialize storage client + this.storage = createPictureStorage(this.publicUrl); + this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: picture-storage)`); } async uploadFile( @@ -88,59 +40,16 @@ export class StorageService { const ext = filename.split('.').pop() || 'jpg'; const storagePath = `${userId}/${timestamp}-${randomId}.${ext}`; - if (this.mode === 's3' && this.s3Client) { - return this.uploadToS3(buffer, storagePath, contentType); - } else { - return this.uploadToLocal(buffer, storagePath); - } - } - - private async uploadToS3( - buffer: Buffer, - storagePath: string, - contentType: string - ): Promise<{ storagePath: string; publicUrl: string }> { - if (!this.s3Client) { - throw new Error('S3 client not configured'); - } - - const command = new PutObjectCommand({ - Bucket: this.bucket, - Key: storagePath, - Body: buffer, - ContentType: contentType, - ACL: 'public-read', + const result = await this.storage.upload(storagePath, buffer, { + contentType, + public: true, }); - try { - await this.s3Client.send(command); - const publicUrl = `${this.publicUrlBase}/${storagePath}`; + const publicUrl = result.url || this.getPublicUrl(storagePath); - return { storagePath, publicUrl }; - } catch (error) { - this.logger.error('Error uploading file to S3', error); - throw error; - } - } + this.logger.debug(`Uploaded file to ${storagePath}`); - private async uploadToLocal( - buffer: Buffer, - storagePath: string - ): Promise<{ storagePath: string; publicUrl: string }> { - const fullPath = path.join(this.localStoragePath, storagePath); - const directory = path.dirname(fullPath); - - try { - await fs.mkdir(directory, { recursive: true }); - await fs.writeFile(fullPath, buffer); - - const publicUrl = `${this.publicUrlBase}/${storagePath}`; - - return { storagePath, publicUrl }; - } catch (error) { - this.logger.error('Error uploading file to local storage', error); - throw error; - } + return { storagePath, publicUrl }; } async uploadFromUrl( @@ -161,109 +70,42 @@ export class StorageService { } async deleteFile(storagePath: string): Promise { - if (this.mode === 's3' && this.s3Client) { - return this.deleteFromS3(storagePath); - } else { - return this.deleteFromLocal(storagePath); - } - } - - private async deleteFromS3(storagePath: string): Promise { - if (!this.s3Client) { - throw new Error('S3 client not configured'); - } - - const command = new DeleteObjectCommand({ - Bucket: this.bucket, - Key: storagePath, - }); - try { - await this.s3Client.send(command); + await this.storage.delete(storagePath); + this.logger.debug(`Deleted file: ${storagePath}`); } catch (error) { - this.logger.error(`Error deleting file ${storagePath} from S3`, error); + this.logger.error(`Error deleting file ${storagePath}`, error); throw error; } } - private async deleteFromLocal(storagePath: string): Promise { - const fullPath = path.join(this.localStoragePath, storagePath); - - try { - await fs.unlink(fullPath); - } catch (error) { - // Ignore if file doesn't exist - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - this.logger.error(`Error deleting file ${storagePath} from local storage`, error); - throw error; - } - } - } - async uploadBoardThumbnail(boardId: string, dataUrl: string): Promise { const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); const buffer = Buffer.from(base64Data, 'base64'); const storagePath = `boards/${boardId}/thumbnail-${Date.now()}.png`; - if (this.mode === 's3' && this.s3Client) { - const result = await this.uploadToS3(buffer, storagePath, 'image/png'); - return result.publicUrl; - } else { - const result = await this.uploadToLocal(buffer, storagePath); - return result.publicUrl; - } + const result = await this.storage.upload(storagePath, buffer, { + contentType: 'image/png', + public: true, + }); + + return result.url || this.getPublicUrl(storagePath); } async getFile(storagePath: string): Promise { - if (this.mode === 's3' && this.s3Client) { - return this.getFromS3(storagePath); - } else { - return this.getFromLocal(storagePath); - } - } - - private async getFromS3(storagePath: string): Promise { - if (!this.s3Client) { - throw new Error('S3 client not configured'); - } - - const command = new GetObjectCommand({ - Bucket: this.bucket, - Key: storagePath, - }); - try { - const response = await this.s3Client.send(command); - if (response.Body) { - const byteArray = await response.Body.transformToByteArray(); - return Buffer.from(byteArray); - } - return null; + return await this.storage.download(storagePath); } catch (error) { - this.logger.error(`Error getting file ${storagePath} from S3`, error); - return null; - } - } - - private async getFromLocal(storagePath: string): Promise { - const fullPath = path.join(this.localStoragePath, storagePath); - - try { - return await fs.readFile(fullPath); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return null; - } - this.logger.error(`Error getting file ${storagePath} from local storage`, error); + this.logger.error(`Error getting file ${storagePath}`, error); return null; } } getStorageMode(): StorageMode { - return this.mode; + return 's3'; } getPublicUrl(storagePath: string): string { - return `${this.publicUrlBase}/${storagePath}`; + return this.storage.getPublicUrl(storagePath) || `${this.publicUrl}/${storagePath}`; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85b6369b8..1d7dc9fca 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 @@ -537,19 +537,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 @@ -2012,12 +2012,18 @@ importers: '@aws-sdk/client-s3': specifier: ^3.700.0 version: 3.940.0 + '@mana-core/nestjs-integration': + specifier: workspace:* + version: link:../../../../packages/mana-core-nestjs-integration '@manacore/shared-errors': specifier: workspace:* version: link:../../../../packages/shared-errors '@manacore/shared-nestjs-auth': specifier: workspace:* version: link:../../../../packages/shared-nestjs-auth + '@manacore/shared-storage': + specifier: workspace:* + version: link:../../../../packages/shared-storage '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -2066,7 +2072,7 @@ importers: devDependencies: '@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) @@ -2102,7 +2108,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.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6712,7 +6718,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==} @@ -19888,16 +19894,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) @@ -22411,7 +22407,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' @@ -27278,6 +27274,19 @@ 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@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.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@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@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)': dependencies: jest-matcher-utils: 30.2.0 @@ -27820,16 +27829,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 @@ -27878,15 +27887,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 @@ -27978,14 +27987,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: @@ -28017,14 +28026,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: @@ -28150,12 +28159,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 @@ -28186,12 +28195,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 @@ -28373,15 +28382,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 @@ -28412,13 +28421,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 @@ -29219,108 +29228,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 @@ -31606,11 +31513,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) @@ -31621,9 +31523,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 @@ -31638,9 +31540,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 @@ -31658,14 +31560,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) @@ -31690,17 +31592,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: @@ -31738,7 +31640,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 @@ -31749,22 +31651,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 @@ -31778,12 +31665,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 @@ -31798,39 +31685,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 @@ -31854,6 +31727,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) @@ -31907,7 +31786,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 @@ -31916,9 +31795,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 @@ -31930,7 +31809,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 @@ -31965,7 +31844,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 @@ -31976,7 +31855,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 @@ -31994,7 +31873,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 @@ -32005,7 +31884,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 @@ -32033,6 +31912,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) @@ -32063,6 +31952,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) @@ -32087,6 +31986,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) @@ -32117,6 +32020,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 @@ -33336,6 +33261,53 @@ snapshots: - supports-color optional: true + 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.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.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.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 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + 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 + shallowequal: 1.1.0 + 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.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.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.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): 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) @@ -35525,6 +35497,26 @@ snapshots: - supports-color - ts-node + 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.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@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 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + 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)): 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)) @@ -35696,6 +35688,40 @@ snapshots: - babel-plugin-macros - supports-color + 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 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.25 + esbuild-register: 3.6.0(esbuild@0.27.0) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + 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)): dependencies: '@babel/core': 7.28.5 @@ -36386,6 +36412,20 @@ snapshots: - supports-color - ts-node + 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.27.0)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + 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 + - esbuild-register + - supports-color + - ts-node + optional: true + 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)): 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)) @@ -42540,23 +42580,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 @@ -42659,10 +42682,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) diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index d8be64545..ea7a3fc2e 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -229,24 +229,28 @@ const APP_CONFIGS = [ path: 'apps/picture/apps/backend/.env', vars: { NODE_ENV: () => 'development', - PORT: (env) => env.PICTURE_BACKEND_PORT || '3003', - BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003', + PORT: (env) => env.PICTURE_BACKEND_PORT || '3006', + BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3006', DATABASE_URL: (env) => - env.PICTURE_DATABASE_URL || 'postgresql://picture:picturepassword@localhost:5434/picture', + env.PICTURE_DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/picture', MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, DEV_BYPASS_AUTH: () => 'true', DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000', - REPLICATE_API_TOKEN: (env) => env.MAERCHENZAUBER_REPLICATE_API_KEY, + REPLICATE_API_TOKEN: (env) => env.PICTURE_REPLICATE_API_TOKEN, CORS_ORIGINS: (env) => env.CORS_ORIGINS, - // Storage configuration - STORAGE_MODE: (env) => env.PICTURE_STORAGE_MODE || 'local', + // Storage configuration - use shared MinIO for local dev + STORAGE_MODE: (env) => env.PICTURE_STORAGE_MODE || 's3', LOCAL_STORAGE_PATH: (env) => env.PICTURE_LOCAL_STORAGE_PATH || './uploads', - S3_ENDPOINT: (env) => env.PICTURE_S3_ENDPOINT || '', - S3_REGION: (env) => env.PICTURE_S3_REGION || 'eu-central-1', - S3_ACCESS_KEY: (env) => env.PICTURE_S3_ACCESS_KEY || '', - S3_SECRET_KEY: (env) => env.PICTURE_S3_SECRET_KEY || '', - S3_BUCKET: (env) => env.PICTURE_S3_BUCKET || 'picture-uploads', - STORAGE_PUBLIC_URL: (env) => env.PICTURE_STORAGE_PUBLIC_URL || '', + S3_ENDPOINT: (env) => env.S3_ENDPOINT || 'http://localhost:9000', + S3_REGION: (env) => env.S3_REGION || 'us-east-1', + S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY || 'minioadmin', + S3_SECRET_KEY: (env) => env.S3_SECRET_KEY || 'minioadmin', + S3_BUCKET: (env) => env.PICTURE_S3_BUCKET || 'picture-storage', + STORAGE_PUBLIC_URL: (env) => + env.PICTURE_STORAGE_PUBLIC_URL || 'http://localhost:9000/picture-storage', + // Credit system (for staging) + APP_ID: (env) => env.PICTURE_APP_ID || 'picture-app', + MANA_CORE_SERVICE_KEY: (env) => env.PICTURE_MANA_CORE_SERVICE_KEY || '', }, },