From 51edd522411b872e7d9f5df5f9666763f16e4365 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:22:49 +0100 Subject: [PATCH] refactor(picture): remove Supabase dependency, migrate to NestJS backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: Replace Supabase storage with S3/local hybrid StorageService - Backend: Add rate-limits endpoint to ProfileController - Mobile: Update RateLimitIndicator to use backend API - Mobile: Remove @supabase/supabase-js dependency - Shared: Remove queue.ts and supabase.ts (no longer needed) - Update environment configuration for S3 storage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.development | 16 +- apps/picture/apps/backend/.env.example | 18 +- apps/picture/apps/backend/package.json | 2 +- .../apps/backend/src/board/board.module.ts | 2 + .../apps/backend/src/board/board.service.ts | 41 +- .../backend/src/profile/dto/profile.dto.ts | 12 + .../backend/src/profile/profile.controller.ts | 7 +- .../backend/src/profile/profile.service.ts | 76 +- .../backend/src/upload/storage.service.ts | 252 +++- apps/picture/apps/mobile/CLAUDE.md | 69 +- .../mobile/components/RateLimitIndicator.tsx | 20 +- apps/picture/apps/mobile/package.json | 1 - .../apps/mobile/services/api/profiles.ts | 33 + apps/picture/apps/mobile/utils/supabase.ts | 59 - apps/picture/apps/web/.env.example | 11 +- apps/picture/apps/web/docs/STORAGE_SETUP.md | 136 -- .../apps/web/docs/setup-storage-bucket.sql | 101 -- .../apps/web/src/routes/app/+layout.svelte | 3 +- apps/picture/packages/shared/package.json | 5 +- apps/picture/packages/shared/src/api/index.ts | 2 +- .../packages/shared/src/api/supabase.ts | 8 - apps/picture/packages/shared/src/index.ts | 2 - apps/picture/packages/shared/src/queue.ts | 504 ------- pnpm-lock.yaml | 1223 ++++++++++++++++- scripts/generate-env.mjs | 18 +- 25 files changed, 1631 insertions(+), 990 deletions(-) delete mode 100644 apps/picture/apps/mobile/utils/supabase.ts delete mode 100644 apps/picture/apps/web/docs/STORAGE_SETUP.md delete mode 100644 apps/picture/apps/web/docs/setup-storage-bucket.sql delete mode 100644 apps/picture/packages/shared/src/api/supabase.ts delete mode 100644 apps/picture/packages/shared/src/queue.ts diff --git a/.env.development b/.env.development index 2d04781d2..d6300779c 100644 --- a/.env.development +++ b/.env.development @@ -119,9 +119,21 @@ MANADECK_SUPABASE_ANON_KEY=your-supabase-anon-key # PICTURE PROJECT # ============================================ +PICTURE_BACKEND_PORT=3003 PICTURE_BACKEND_URL=http://localhost:3003 -PICTURE_SUPABASE_URL=https://your-picture-project.supabase.co -PICTURE_SUPABASE_ANON_KEY=your-supabase-anon-key +PICTURE_DATABASE_URL=postgresql://picture:picturepassword@localhost:5434/picture + +# Storage Configuration (local for dev, s3 for production with Hetzner Object Storage) +PICTURE_STORAGE_MODE=local +PICTURE_LOCAL_STORAGE_PATH=./uploads + +# S3/Hetzner Object Storage (for production) +# PICTURE_S3_ENDPOINT=fsn1.your-objectstorage.com +# PICTURE_S3_REGION=eu-central-1 +# PICTURE_S3_ACCESS_KEY=your-access-key +# PICTURE_S3_SECRET_KEY=your-secret-key +# PICTURE_S3_BUCKET=picture-uploads +# PICTURE_STORAGE_PUBLIC_URL=https://picture-uploads.fsn1.your-objectstorage.com # OAuth (optional - leave empty to disable) PICTURE_GOOGLE_CLIENT_ID= diff --git a/apps/picture/apps/backend/.env.example b/apps/picture/apps/backend/.env.example index dc16356f9..107af41f4 100644 --- a/apps/picture/apps/backend/.env.example +++ b/apps/picture/apps/backend/.env.example @@ -1,6 +1,7 @@ # Server PORT=3003 NODE_ENV=development +BACKEND_URL=http://localhost:3003 # Database DATABASE_URL=postgresql://picture:password@localhost:5432/picture @@ -8,9 +9,20 @@ DATABASE_URL=postgresql://picture:password@localhost:5432/picture # Mana Core Auth MANA_CORE_AUTH_URL=http://localhost:3001 -# Supabase Storage (Service Role for Backend) -SUPABASE_URL=https://xxx.supabase.co -SUPABASE_SERVICE_ROLE_KEY=eyJ... +# Storage Configuration +# Options: 'local' (default) or 's3' (for Hetzner Object Storage) +STORAGE_MODE=local + +# Local Storage (for development) +LOCAL_STORAGE_PATH=./uploads + +# S3/Hetzner Object Storage (for production) +# S3_ENDPOINT=fsn1.your-objectstorage.com +# S3_REGION=eu-central-1 +# S3_ACCESS_KEY=your-access-key +# S3_SECRET_KEY=your-secret-key +# S3_BUCKET=picture-uploads +# STORAGE_PUBLIC_URL=https://picture-uploads.fsn1.your-objectstorage.com # Replicate API REPLICATE_API_TOKEN=r8_xxx diff --git a/apps/picture/apps/backend/package.json b/apps/picture/apps/backend/package.json index 4dc87bae2..8360507fb 100644 --- a/apps/picture/apps/backend/package.json +++ b/apps/picture/apps/backend/package.json @@ -18,12 +18,12 @@ "db:seed": "tsx src/db/seed.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", "@manacore/shared-errors": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", - "@supabase/supabase-js": "^2.45.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", diff --git a/apps/picture/apps/backend/src/board/board.module.ts b/apps/picture/apps/backend/src/board/board.module.ts index a11fe11b8..23726f757 100644 --- a/apps/picture/apps/backend/src/board/board.module.ts +++ b/apps/picture/apps/backend/src/board/board.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { BoardController } from './board.controller'; import { BoardService } from './board.service'; +import { UploadModule } from '../upload/upload.module'; @Module({ + imports: [UploadModule], controllers: [BoardController], providers: [BoardService], exports: [BoardService], diff --git a/apps/picture/apps/backend/src/board/board.service.ts b/apps/picture/apps/backend/src/board/board.service.ts index 8b1c127a6..e68e609cb 100644 --- a/apps/picture/apps/backend/src/board/board.service.ts +++ b/apps/picture/apps/backend/src/board/board.service.ts @@ -1,11 +1,10 @@ import { Injectable, Inject, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; import { eq, and, or, desc, sql } from 'drizzle-orm'; -import { ConfigService } from '@nestjs/config'; -import { createClient } from '@supabase/supabase-js'; import { DATABASE_CONNECTION } from '../db/database.module'; import { type Database } from '../db/connection'; import { boards, boardItems, type Board } from '../db/schema'; import { CreateBoardDto, UpdateBoardDto, GetBoardsQueryDto } from './dto/board.dto'; +import { StorageService } from '../upload/storage.service'; export interface BoardWithCount extends Board { itemCount: number; @@ -14,18 +13,11 @@ export interface BoardWithCount extends Board { @Injectable() export class BoardService { private readonly logger = new Logger(BoardService.name); - private supabase: ReturnType; constructor( @Inject(DATABASE_CONNECTION) private readonly db: Database, - private configService: ConfigService - ) { - const supabaseUrl = this.configService.get('SUPABASE_URL'); - const supabaseKey = this.configService.get('SUPABASE_SERVICE_ROLE_KEY'); - if (supabaseUrl && supabaseKey) { - this.supabase = createClient(supabaseUrl, supabaseKey); - } - } + private readonly storageService: StorageService + ) {} async getBoards(userId: string, query: GetBoardsQueryDto): Promise { try { @@ -266,35 +258,14 @@ export class BoardService { try { await this.verifyOwnership(id, userId); - if (!this.supabase) { - throw new Error('Supabase not configured'); - } - - // Convert data URL to buffer - const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); - const buffer = Buffer.from(base64Data, 'base64'); - - // Upload to Supabase Storage - const filename = `boards/${id}/thumbnail-${Date.now()}.png`; - const { error: uploadError } = await this.supabase.storage - .from('user-uploads') - .upload(filename, buffer, { - contentType: 'image/png', - upsert: true, - }); - - if (uploadError) { - throw uploadError; - } - - // Get public URL - const { data: urlData } = this.supabase.storage.from('user-uploads').getPublicUrl(filename); + // Upload thumbnail using StorageService + const thumbnailUrl = await this.storageService.uploadBoardThumbnail(id, dataUrl); // Update board with thumbnail URL const result = await this.db .update(boards) .set({ - thumbnailUrl: urlData.publicUrl, + thumbnailUrl, updatedAt: new Date(), }) .where(eq(boards.id, id)) diff --git a/apps/picture/apps/backend/src/profile/dto/profile.dto.ts b/apps/picture/apps/backend/src/profile/dto/profile.dto.ts index cd5a39157..f7509d028 100644 --- a/apps/picture/apps/backend/src/profile/dto/profile.dto.ts +++ b/apps/picture/apps/backend/src/profile/dto/profile.dto.ts @@ -28,3 +28,15 @@ export interface UserStatsResponse { archivedImages: number; publicImages: number; } + +export interface RateLimitsResponse { + daily_used: number; + daily_limit: number; + daily_reset_at: string; + hourly_used: number; + hourly_limit: number; + hourly_reset_at: string; + active_generations: number; + max_concurrent: number; + total_all_time: number; +} diff --git a/apps/picture/apps/backend/src/profile/profile.controller.ts b/apps/picture/apps/backend/src/profile/profile.controller.ts index fae3065a1..5fa0cce3b 100644 --- a/apps/picture/apps/backend/src/profile/profile.controller.ts +++ b/apps/picture/apps/backend/src/profile/profile.controller.ts @@ -2,7 +2,7 @@ import { Controller, Get, Patch, Body, UseGuards } from '@nestjs/common'; import { ProfileService } from './profile.service'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; import { CurrentUser, CurrentUserData } from '../common/decorators/current-user.decorator'; -import { UpdateProfileDto, ProfileResponse, UserStatsResponse } from './dto/profile.dto'; +import { UpdateProfileDto, ProfileResponse, UserStatsResponse, RateLimitsResponse } from './dto/profile.dto'; @Controller('profiles') @UseGuards(JwtAuthGuard) @@ -27,4 +27,9 @@ export class ProfileController { async getMyStats(@CurrentUser() user: CurrentUserData): Promise { return this.profileService.getUserStats(user.userId); } + + @Get('rate-limits') + async getRateLimits(@CurrentUser() user: CurrentUserData): Promise { + return this.profileService.getRateLimits(user.userId); + } } diff --git a/apps/picture/apps/backend/src/profile/profile.service.ts b/apps/picture/apps/backend/src/profile/profile.service.ts index 7b890f0e0..9f99164aa 100644 --- a/apps/picture/apps/backend/src/profile/profile.service.ts +++ b/apps/picture/apps/backend/src/profile/profile.service.ts @@ -1,9 +1,9 @@ import { Injectable, Inject, NotFoundException, Logger } from '@nestjs/common'; -import { eq, and, isNull, isNotNull, sql } from 'drizzle-orm'; +import { eq, and, isNull, isNotNull, sql, gte, inArray } from 'drizzle-orm'; import { DATABASE_CONNECTION } from '../db/database.module'; import { type Database } from '../db/connection'; -import { profiles, images, type Profile } from '../db/schema'; -import { UpdateProfileDto, ProfileResponse, UserStatsResponse } from './dto/profile.dto'; +import { profiles, images, imageGenerations, type Profile } from '../db/schema'; +import { UpdateProfileDto, ProfileResponse, UserStatsResponse, RateLimitsResponse } from './dto/profile.dto'; @Injectable() export class ProfileService { @@ -132,4 +132,74 @@ export class ProfileService { throw error; } } + + async getRateLimits(userId: string): Promise { + try { + const now = new Date(); + + // Calculate start of current day (UTC) + const startOfDay = new Date(now); + startOfDay.setUTCHours(0, 0, 0, 0); + + // Calculate start of current hour + const startOfHour = new Date(now); + startOfHour.setUTCMinutes(0, 0, 0); + + // Calculate reset times + const dailyReset = new Date(startOfDay); + dailyReset.setUTCDate(dailyReset.getUTCDate() + 1); + + const hourlyReset = new Date(startOfHour); + hourlyReset.setUTCHours(hourlyReset.getUTCHours() + 1); + + // Count daily generations + const dailyResult = await this.db + .select({ count: sql`count(*)` }) + .from(imageGenerations) + .where(and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfDay))); + + // Count hourly generations + const hourlyResult = await this.db + .select({ count: sql`count(*)` }) + .from(imageGenerations) + .where(and(eq(imageGenerations.userId, userId), gte(imageGenerations.createdAt, startOfHour))); + + // Count active generations (pending, queued, processing) + const activeResult = await this.db + .select({ count: sql`count(*)` }) + .from(imageGenerations) + .where( + and( + eq(imageGenerations.userId, userId), + inArray(imageGenerations.status, ['pending', 'queued', 'processing']) + ) + ); + + // Count total all-time generations + const totalResult = await this.db + .select({ count: sql`count(*)` }) + .from(imageGenerations) + .where(eq(imageGenerations.userId, userId)); + + // Default limits (can be made configurable later) + const DAILY_LIMIT = 100; + const HOURLY_LIMIT = 20; + const MAX_CONCURRENT = 5; + + return { + daily_used: Number(dailyResult[0]?.count || 0), + daily_limit: DAILY_LIMIT, + daily_reset_at: dailyReset.toISOString(), + hourly_used: Number(hourlyResult[0]?.count || 0), + hourly_limit: HOURLY_LIMIT, + hourly_reset_at: hourlyReset.toISOString(), + active_generations: Number(activeResult[0]?.count || 0), + max_concurrent: MAX_CONCURRENT, + total_all_time: Number(totalResult[0]?.count || 0), + }; + } catch (error) { + this.logger.error(`Error fetching rate limits for user ${userId}`, error); + throw error; + } + } } diff --git a/apps/picture/apps/backend/src/upload/storage.service.ts b/apps/picture/apps/backend/src/upload/storage.service.ts index 2b8227a60..f3030e104 100644 --- a/apps/picture/apps/backend/src/upload/storage.service.ts +++ b/apps/picture/apps/backend/src/upload/storage.service.ts @@ -1,21 +1,79 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { + S3Client, + PutObjectCommand, + DeleteObjectCommand, + GetObjectCommand, +} from '@aws-sdk/client-s3'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export type StorageMode = 'local' | 's3'; @Injectable() export class StorageService { private readonly logger = new Logger(StorageService.name); - private supabase: SupabaseClient | null = null; - private readonly bucket = 'user-uploads'; + private mode: StorageMode; + private s3Client: S3Client | null = null; + private readonly bucket: string; + private readonly localStoragePath: string; + private readonly publicUrlBase: string; constructor(private configService: ConfigService) { - const supabaseUrl = this.configService.get('SUPABASE_URL'); - const supabaseKey = this.configService.get('SUPABASE_SERVICE_ROLE_KEY'); + // Determine storage mode from environment + const storageMode = this.configService.get('STORAGE_MODE', 'local'); + this.mode = storageMode === 's3' ? 's3' : 'local'; - if (supabaseUrl && supabaseKey) { - this.supabase = createClient(supabaseUrl, supabaseKey); - } else { - this.logger.warn('Supabase credentials not configured'); + // 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( + 'STORAGE_PUBLIC_URL', + this.mode === 'local' ? `${backendUrl}/uploads` : `https://${this.bucket}.${s3Endpoint}` + ); + + 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); } } @@ -25,31 +83,64 @@ export class StorageService { filename: string, contentType: string ): Promise<{ storagePath: string; publicUrl: string }> { - if (!this.supabase) { - throw new Error('Supabase not configured'); - } - const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 10); const ext = filename.split('.').pop() || 'jpg'; const storagePath = `${userId}/${timestamp}-${randomId}.${ext}`; - const { error } = await this.supabase.storage.from(this.bucket).upload(storagePath, buffer, { - contentType, - upsert: false, - }); + if (this.mode === 's3' && this.s3Client) { + return this.uploadToS3(buffer, storagePath, contentType); + } else { + return this.uploadToLocal(buffer, storagePath); + } + } - if (error) { - this.logger.error('Error uploading file to storage', error); - throw error; + 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 { data: urlData } = this.supabase.storage.from(this.bucket).getPublicUrl(storagePath); + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: storagePath, + Body: buffer, + ContentType: contentType, + ACL: 'public-read', + }); - return { - storagePath, - publicUrl: urlData.publicUrl, - }; + try { + await this.s3Client.send(command); + const publicUrl = `${this.publicUrlBase}/${storagePath}`; + + return { storagePath, publicUrl }; + } catch (error) { + this.logger.error('Error uploading file to S3', error); + throw error; + } + } + + 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; + } } async uploadFromUrl( @@ -57,10 +148,6 @@ export class StorageService { userId: string, filename: string ): Promise<{ storagePath: string; publicUrl: string }> { - if (!this.supabase) { - throw new Error('Supabase not configured'); - } - // Download the file const response = await fetch(url); if (!response.ok) { @@ -74,40 +161,109 @@ export class StorageService { } async deleteFile(storagePath: string): Promise { - if (!this.supabase) { - throw new Error('Supabase not configured'); + 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 { error } = await this.supabase.storage.from(this.bucket).remove([storagePath]); + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: storagePath, + }); - if (error) { - this.logger.error(`Error deleting file ${storagePath}`, error); + try { + await this.s3Client.send(command); + } catch (error) { + this.logger.error(`Error deleting file ${storagePath} from S3`, 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 { - if (!this.supabase) { - throw new Error('Supabase not configured'); - } - const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); const buffer = Buffer.from(base64Data, 'base64'); + const storagePath = `boards/${boardId}/thumbnail-${Date.now()}.png`; - const filename = `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 { error } = await this.supabase.storage.from(this.bucket).upload(filename, buffer, { - contentType: 'image/png', - upsert: true, - }); + async getFile(storagePath: string): Promise { + if (this.mode === 's3' && this.s3Client) { + return this.getFromS3(storagePath); + } else { + return this.getFromLocal(storagePath); + } + } - if (error) { - this.logger.error('Error uploading board thumbnail', error); - throw error; + private async getFromS3(storagePath: string): Promise { + if (!this.s3Client) { + throw new Error('S3 client not configured'); } - const { data: urlData } = this.supabase.storage.from(this.bucket).getPublicUrl(filename); + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: storagePath, + }); - return urlData.publicUrl; + try { + const response = await this.s3Client.send(command); + if (response.Body) { + const byteArray = await response.Body.transformToByteArray(); + return Buffer.from(byteArray); + } + return null; + } 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); + return null; + } + } + + getStorageMode(): StorageMode { + return this.mode; + } + + getPublicUrl(storagePath: string): string { + return `${this.publicUrlBase}/${storagePath}`; } } diff --git a/apps/picture/apps/mobile/CLAUDE.md b/apps/picture/apps/mobile/CLAUDE.md index 68cff89a2..faef53974 100644 --- a/apps/picture/apps/mobile/CLAUDE.md +++ b/apps/picture/apps/mobile/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is the **mobile app** within the "picture" monorepo. It's an Expo React Native application built with TypeScript, using Expo Router for navigation and NativeWind (Tailwind CSS) for styling. The app integrates with Supabase for backend services and uses Zustand for state management. +This is the **mobile app** within the "picture" monorepo. It's an Expo React Native application built with TypeScript, using Expo Router for navigation and NativeWind (Tailwind CSS) for styling. The app integrates with a NestJS backend for all API calls and uses Zustand for state management. ## Monorepo Structure @@ -13,11 +13,12 @@ This app is part of a PNPM workspace monorepo: ``` picture/ ├── apps/ +│ ├── backend/ # NestJS API server │ ├── mobile/ # This React Native app (Expo) │ ├── web/ # SvelteKit web app │ └── landing/ # Astro landing page ├── packages/ -│ └── shared/ # Shared code (Supabase types, API client) +│ └── shared/ # Shared code (TypeScript types, utilities) └── pnpm-workspace.yaml ``` @@ -25,14 +26,12 @@ picture/ The shared package provides: -- **Supabase Database Types** - Auto-generated TypeScript types from database schema -- **Supabase Client** - Configured API client for all apps +- **Database Types** - TypeScript types from database schema - **Shared Utilities** - Common helper functions and types Import from shared package: ```tsx -import { supabase } from '@picture/shared'; import type { Database } from '@picture/shared/types'; ``` @@ -82,16 +81,28 @@ Zustand store in `store/store.ts` - Currently contains a sample "bears" store th ### Backend Integration -Supabase client is imported from `@picture/shared`: +All API calls go through the NestJS backend: ```tsx -import { supabase } from '@picture/shared'; +import { fetchApi } from '~/services/api/client'; +import { getRateLimits } from '~/services/api/profiles'; +import { generateImage } from '~/services/api/generate'; ``` -- Shared client configured with AsyncStorage for auth persistence +- API client configured with JWT authentication via `@manacore/shared-auth` - Environment variables managed at root level -- MCP server configured for Supabase integration (see root `.mcp.json`) -- Database types auto-generated in shared package +- Database is PostgreSQL accessed through the backend + +### API Services + +Located in `services/api/`: + +- `client.ts` - Base API client with auth handling +- `images.ts` - Image CRUD operations +- `generate.ts` - Image generation endpoints +- `models.ts` - AI model endpoints +- `profiles.ts` - User profile and rate limits +- `tags.ts` - Image tagging ### Styling @@ -118,7 +129,7 @@ import { supabase } from '@picture/shared'; - **Navigation**: expo-router, react-navigation - **UI**: NativeWind, @expo/vector-icons -- **Backend**: @supabase/supabase-js +- **Auth**: @manacore/shared-auth - **State**: zustand - **Development**: expo-dev-client for custom native builds @@ -126,8 +137,8 @@ import { supabase } from '@picture/shared'; Required environment variables (in `.env` or similar): -- `EXPO_PUBLIC_SUPABASE_URL` - Supabase project URL -- `EXPO_PUBLIC_SUPABASE_ANON_KEY` - Supabase anonymous key +- `EXPO_PUBLIC_API_URL` - Backend API URL +- `EXPO_PUBLIC_MIDDLEWARE_API_URL` - Auth middleware URL ## EAS Build Configuration @@ -137,35 +148,3 @@ The project is configured for EAS Build with: - Preview builds for internal distribution - Production builds with auto-incrementing version numbers - Project ID: `a74891be-7ff7-420c-9ff0-d33c37a59e5a` - -## Supabase Edge Functions - -### WICHTIG: Workflow für Edge Function Änderungen - -**⚠️ KRITISCH: Bevor du eine Edge Function änderst, MUSS folgender Workflow eingehalten werden:** - -1. **ERST Commit erstellen** - - ```bash - git add . - git commit -m "Before Edge Function changes" - ``` - -2. **DANN lokale Änderungen vornehmen** - - Bearbeite die Function in `supabase/functions/[function-name]/` - - Teste lokal mit: `npx supabase functions serve [function-name]` - -3. **ZULETZT auf Supabase deployen** - ```bash - npx supabase functions deploy [function-name] - ``` - -### Edge Functions Struktur - -``` -supabase/ -└── functions/ - └── [function-name]/ - ├── index.ts # Function Code - └── README.md # Dokumentation -``` diff --git a/apps/picture/apps/mobile/components/RateLimitIndicator.tsx b/apps/picture/apps/mobile/components/RateLimitIndicator.tsx index 89333fe45..5b3d7bbd5 100644 --- a/apps/picture/apps/mobile/components/RateLimitIndicator.tsx +++ b/apps/picture/apps/mobile/components/RateLimitIndicator.tsx @@ -2,22 +2,10 @@ import React, { useEffect, useState } from 'react'; import { View, Pressable } from 'react-native'; import { Icon } from './Icon'; import { Text } from './Text'; -import { supabase } from '~/utils/supabase'; +import { getRateLimits, type RateLimits } from '~/services/api/profiles'; import { useAuth } from '~/contexts/AuthContext'; import { useTheme } from '~/contexts/ThemeContext'; -interface RateLimits { - daily_used: number; - daily_limit: number; - daily_reset_at: string; - hourly_used: number; - hourly_limit: number; - hourly_reset_at: string; - active_generations: number; - max_concurrent: number; - total_all_time: number; -} - interface RateLimitIndicatorProps { compact?: boolean; onRefresh?: () => void; @@ -34,11 +22,7 @@ export function RateLimitIndicator({ compact = false, onRefresh }: RateLimitIndi if (!user) return; try { - const { data, error } = await supabase.rpc('get_user_limits', { - p_user_id: user.id, - }); - - if (error) throw error; + const data = await getRateLimits(); setLimits(data); } catch (error) { console.error('Error fetching rate limits:', error); diff --git a/apps/picture/apps/mobile/package.json b/apps/picture/apps/mobile/package.json index 33cc2fdda..74887862c 100644 --- a/apps/picture/apps/mobile/package.json +++ b/apps/picture/apps/mobile/package.json @@ -24,7 +24,6 @@ "@picture/shared": "workspace:*", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.0.3", - "@supabase/supabase-js": "^2.38.4", "blurhash": "^2.0.5", "expo": "54.0.12", "expo-blur": "~15.0.7", diff --git a/apps/picture/apps/mobile/services/api/profiles.ts b/apps/picture/apps/mobile/services/api/profiles.ts index 00cd373a5..30cf6b31d 100644 --- a/apps/picture/apps/mobile/services/api/profiles.ts +++ b/apps/picture/apps/mobile/services/api/profiles.ts @@ -63,3 +63,36 @@ export async function getUserStats(): Promise { } ); } + +export interface RateLimits { + daily_used: number; + daily_limit: number; + daily_reset_at: string; + hourly_used: number; + hourly_limit: number; + hourly_reset_at: string; + active_generations: number; + max_concurrent: number; + total_all_time: number; +} + +/** + * Get user rate limits for image generation + */ +export async function getRateLimits(): Promise { + const { data, error } = await fetchApi('/profiles/rate-limits'); + if (error) throw error; + return ( + data || { + daily_used: 0, + daily_limit: 100, + daily_reset_at: new Date().toISOString(), + hourly_used: 0, + hourly_limit: 20, + hourly_reset_at: new Date().toISOString(), + active_generations: 0, + max_concurrent: 5, + total_all_time: 0, + } + ); +} diff --git a/apps/picture/apps/mobile/utils/supabase.ts b/apps/picture/apps/mobile/utils/supabase.ts deleted file mode 100644 index 5b6b01608..000000000 --- a/apps/picture/apps/mobile/utils/supabase.ts +++ /dev/null @@ -1,59 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { createClient } from '@supabase/supabase-js'; -import { Platform } from 'react-native'; -import type { Database } from '@picture/shared/types'; - -const supabaseUrl = - process.env.EXPO_PUBLIC_SUPABASE_URL || 'https://mjuvnnjxwfwlmxjsgkqu.supabase.co'; -const supabaseAnonKey = - process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1qdXZubmp4d2Z3bG14anNna3F1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTYyNTg5NTUsImV4cCI6MjA3MTgzNDk1NX0.EeOKzyPnZ42zpFl7oi54qDcAZSW-XGoB0tSNwUiX9GU'; - -// Create a storage adapter that works for both web and mobile -const createStorage = () => { - if (Platform.OS === 'web') { - // For web, use a simple localStorage wrapper - return { - getItem: async (key: string) => { - try { - if (typeof window !== 'undefined' && window.localStorage) { - const item = window.localStorage.getItem(key); - return item; - } - } catch (error) { - console.error('Error getting item from localStorage:', error); - } - return null; - }, - setItem: async (key: string, value: string) => { - try { - if (typeof window !== 'undefined' && window.localStorage) { - window.localStorage.setItem(key, value); - } - } catch (error) { - console.error('Error setting item in localStorage:', error); - } - }, - removeItem: async (key: string) => { - try { - if (typeof window !== 'undefined' && window.localStorage) { - window.localStorage.removeItem(key); - } - } catch (error) { - console.error('Error removing item from localStorage:', error); - } - }, - }; - } - // For mobile, use AsyncStorage - return AsyncStorage; -}; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey, { - auth: { - storage: createStorage(), - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: Platform.OS === 'web', - }, -}); diff --git a/apps/picture/apps/web/.env.example b/apps/picture/apps/web/.env.example index 0814c5521..342aed853 100644 --- a/apps/picture/apps/web/.env.example +++ b/apps/picture/apps/web/.env.example @@ -1,5 +1,12 @@ -PUBLIC_SUPABASE_URL=https://mjuvnnjxwfwlmxjsgkqu.supabase.co -PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here +# Backend API +PUBLIC_BACKEND_URL=http://localhost:3003 + +# Mana Core Auth +PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 + +# OAuth (optional - leave empty to disable) +PUBLIC_GOOGLE_CLIENT_ID= +PUBLIC_APPLE_CLIENT_ID= # Umami Analytics PUBLIC_UMAMI_URL=https://your-umami-instance.com diff --git a/apps/picture/apps/web/docs/STORAGE_SETUP.md b/apps/picture/apps/web/docs/STORAGE_SETUP.md deleted file mode 100644 index e8051373a..000000000 --- a/apps/picture/apps/web/docs/STORAGE_SETUP.md +++ /dev/null @@ -1,136 +0,0 @@ -# Storage Bucket Setup für User Uploads - -## Schritt 1: SQL Statement ausführen - -### Option A: Erste Installation (Policies existieren noch nicht) - -1. Öffne die **Supabase Dashboard**: https://supabase.com/dashboard -2. Wähle dein Projekt aus -3. Navigiere zu **SQL Editor** -4. Kopiere den Inhalt von `setup-storage-bucket.sql` -5. Führe das SQL-Script aus - -### Option B: Update (Policies existieren bereits) - -1. Öffne die **Supabase Dashboard**: https://supabase.com/dashboard -2. Wähle dein Projekt aus -3. Navigiere zu **SQL Editor** -4. Kopiere den Inhalt von `update-storage-policies.sql` -5. Führe das SQL-Script aus - -**Falls Fehler "policy already exists"**: Verwende `update-storage-policies.sql` statt `setup-storage-bucket.sql` - -## Schritt 2: Überprüfung - -### Bucket überprüfen - -Navigiere zu **Storage** im Supabase Dashboard: - -- Du solltest einen Bucket namens `user-uploads` sehen -- Public: ✓ Enabled -- File Size Limit: 10 MB -- Allowed MIME types: image/jpeg, image/jpg, image/png, image/webp - -### Policies überprüfen - -Navigiere zu **Storage > Policies**: - -- Du solltest 4 Policies für `user-uploads` sehen: - - ✓ Users can upload their own images (INSERT) - - ✓ Public images are publicly accessible (SELECT) - - ✓ Users can update their own images (UPDATE) - - ✓ Users can delete their own images (DELETE) - -## Schritt 3: Testen - -### Test 1: Upload über die Web-App - -1. Öffne die Web-App: http://localhost:5173/app/upload -2. Wähle ein Bild aus oder nutze Drag & Drop -3. Klicke auf "Upload" -4. Das Bild sollte erfolgreich hochgeladen werden -5. Überprüfe in **Storage > user-uploads** im Supabase Dashboard - -### Test 2: Zugriff auf öffentliche URL - -1. Nachdem Upload erfolgreich war, kopiere die `public_url` aus der Konsole -2. Öffne die URL in einem neuen Browser-Tab -3. Das Bild sollte sichtbar sein (ohne Authentifizierung) - -### Test 3: Galerie-Integration - -1. Navigiere zur Galerie: http://localhost:5173/app/gallery -2. Die hochgeladenen Bilder sollten in der Galerie erscheinen -3. Klicke auf ein Bild, um die Detail-Ansicht zu öffnen - -## Datei-Struktur im Bucket - -``` -user-uploads/ - ├── {user_id_1}/ - │ ├── 1234567890-abc123.jpg - │ ├── 1234567891-def456.png - │ └── 1234567892-ghi789.webp - └── {user_id_2}/ - ├── 1234567893-jkl012.jpg - └── 1234567894-mno345.png -``` - -## Sicherheit - -### ✅ Was ist geschützt: - -- User können nur in ihren eigenen Ordner hochladen -- User können nur ihre eigenen Dateien bearbeiten/löschen -- Upload nur für authentifizierte User -- Datei-Größe ist auf 10MB begrenzt -- Nur erlaubte Bild-Formate (JPG, PNG, WebP) - -### ⚠️ Was ist öffentlich: - -- Alle hochgeladenen Bilder sind über ihre public_url zugänglich -- Jeder mit der URL kann das Bild sehen (auch ohne Account) -- Dies ist gewollt für die Galerie-Anzeige - -### 🔒 Optionale Verbesserungen für später: - -- Private Bilder: Separate Bucket für private Uploads -- Signed URLs: Temporäre URLs für sensible Inhalte -- CDN: CloudFlare oder AWS CloudFront vor Supabase Storage - -## Troubleshooting - -### Fehler: "Bucket bereits vorhanden" - -- Kein Problem! Das Script verwendet `ON CONFLICT DO NOTHING` -- Die Policies werden trotzdem erstellt - -### Fehler: "Permission denied" - -1. Überprüfe ob du als authentifizierter User eingeloggt bist -2. Überprüfe die Policies im Supabase Dashboard -3. Führe das SQL-Script erneut aus - -### Fehler: "File too large" - -- Stelle sicher, dass die Datei kleiner als 10MB ist -- Die Validierung erfolgt sowohl im Frontend als auch im Backend - -### Bilder werden nicht in der Galerie angezeigt - -1. Überprüfe ob der Bucket `public` ist -2. Überprüfe ob die `public_url` korrekt generiert wird -3. Öffne die Browser-Konsole für Fehler-Logs - -## Alternative: UI-basiertes Setup - -Falls du das SQL-Script nicht ausführen möchtest, kannst du den Bucket auch manuell im UI erstellen: - -1. **Storage > Create Bucket** - - Name: `user-uploads` - - Public: ✓ Enable - - File Size Limit: 10485760 (10MB) - - Allowed MIME types: image/jpeg, image/jpg, image/png, image/webp - -2. **Storage > Policies > New Policy** - - Erstelle die 4 Policies manuell mit den gleichen Bedingungen wie im SQL-Script diff --git a/apps/picture/apps/web/docs/setup-storage-bucket.sql b/apps/picture/apps/web/docs/setup-storage-bucket.sql deleted file mode 100644 index 3633d2251..000000000 --- a/apps/picture/apps/web/docs/setup-storage-bucket.sql +++ /dev/null @@ -1,101 +0,0 @@ --- ============================================ --- Storage Bucket Setup für User Uploads --- ============================================ --- Dieses Script muss in der Supabase SQL-Konsole ausgeführt werden - --- 1. Erstelle Storage Bucket für User Uploads -INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) -VALUES ( - 'user-uploads', - 'user-uploads', - true, -- Public bucket, damit Bilder über public_url zugänglich sind - 10485760, -- 10MB in Bytes (10 * 1024 * 1024) - ARRAY['image/jpeg', 'image/jpg', 'image/png', 'image/webp']::text[] -) -ON CONFLICT (id) DO NOTHING; - --- 2. Storage Policy: Benutzer können nur ihre eigenen Dateien hochladen -CREATE POLICY "Users can upload their own images" -ON storage.objects -FOR INSERT -TO authenticated -WITH CHECK ( - bucket_id = 'user-uploads' AND - (storage.foldername(name))[1] = auth.uid()::text -); - --- 3. Storage Policy: Jeder kann Bilder lesen (public bucket) -CREATE POLICY "Public images are publicly accessible" -ON storage.objects -FOR SELECT -TO public -USING (bucket_id = 'user-uploads'); - --- 4. Storage Policy: Benutzer können nur ihre eigenen Dateien aktualisieren -CREATE POLICY "Users can update their own images" -ON storage.objects -FOR UPDATE -TO authenticated -USING ( - bucket_id = 'user-uploads' AND - (storage.foldername(name))[1] = auth.uid()::text -) -WITH CHECK ( - bucket_id = 'user-uploads' AND - (storage.foldername(name))[1] = auth.uid()::text -); - --- 5. Storage Policy: Benutzer können nur ihre eigenen Dateien löschen -CREATE POLICY "Users can delete their own images" -ON storage.objects -FOR DELETE -TO authenticated -USING ( - bucket_id = 'user-uploads' AND - (storage.foldername(name))[1] = auth.uid()::text -); - --- ============================================ --- Überprüfung der Bucket-Konfiguration --- ============================================ --- Führe diese Queries aus, um die Konfiguration zu überprüfen: - --- Bucket-Details anzeigen -SELECT * FROM storage.buckets WHERE id = 'user-uploads'; - --- Alle Policies für den Bucket anzeigen -SELECT - schemaname, - tablename, - policyname, - permissive, - roles, - cmd, - qual, - with_check -FROM pg_policies -WHERE tablename = 'objects' - AND policyname ILIKE '%user%' -ORDER BY policyname; - --- ============================================ --- Hinweise --- ============================================ --- --- 1. Die Datei-Struktur im Bucket ist: user-uploads/{user_id}/{timestamp}-{random}.{ext} --- Dies stellt sicher, dass jeder User nur auf seine eigenen Dateien zugreifen kann. --- --- 2. Der Bucket ist PUBLIC, d.h. Bilder sind über die public_url ohne Auth zugänglich. --- Dies ist notwendig, damit Bilder in der Galerie angezeigt werden können. --- --- 3. Die Policies stellen sicher, dass: --- - Nur authentifizierte User hochladen können --- - User nur in ihren eigenen Ordner ({user_id}/) hochladen können --- - Jeder User nur seine eigenen Dateien bearbeiten/löschen kann --- - Alle Bilder öffentlich lesbar sind --- --- 4. File Size Limit: 10MB pro Datei --- Allowed Types: JPG, JPEG, PNG, WebP --- --- 5. Falls der Bucket bereits existiert, wird er nicht neu erstellt (ON CONFLICT DO NOTHING) --- diff --git a/apps/picture/apps/web/src/routes/app/+layout.svelte b/apps/picture/apps/web/src/routes/app/+layout.svelte index 5480a991e..aa70f044f 100644 --- a/apps/picture/apps/web/src/routes/app/+layout.svelte +++ b/apps/picture/apps/web/src/routes/app/+layout.svelte @@ -64,7 +64,7 @@ } }); - // Navigation items + // Navigation items (Mana is in user dropdown via manaHref) const navItems: PillNavItem[] = [ { href: '/app/gallery', label: 'Galerie', icon: 'home' }, { href: '/app/board', label: 'Moodboards', icon: 'grid' }, @@ -73,7 +73,6 @@ { href: '/app/upload', label: 'Upload', icon: 'upload' }, { href: '/app/tags', label: 'Tags', icon: 'tag' }, { href: '/app/archive', label: 'Archiv', icon: 'archive' }, - { href: '/app/mana', label: 'Mana', icon: 'mana' }, ]; // View mode options for tab group diff --git a/apps/picture/packages/shared/package.json b/apps/picture/packages/shared/package.json index 1efa7c433..626817ef1 100644 --- a/apps/picture/packages/shared/package.json +++ b/apps/picture/packages/shared/package.json @@ -6,16 +6,13 @@ "exports": { ".": "./src/index.ts", "./types": "./src/types/index.ts", - "./api": "./src/api/index.ts", "./utils": "./src/utils/index.ts" }, "scripts": { "type-check": "tsc --noEmit", "clean": "rm -rf dist .turbo node_modules" }, - "dependencies": { - "@supabase/supabase-js": "^2.38.4" - }, + "dependencies": {}, "devDependencies": { "@babel/core": "^7.28.4", "@babel/preset-typescript": "^7.27.1", diff --git a/apps/picture/packages/shared/src/api/index.ts b/apps/picture/packages/shared/src/api/index.ts index ae0621fe5..d91a925ba 100644 --- a/apps/picture/packages/shared/src/api/index.ts +++ b/apps/picture/packages/shared/src/api/index.ts @@ -1 +1 @@ -export * from './supabase'; +// API exports - now handled via backend endpoints diff --git a/apps/picture/packages/shared/src/api/supabase.ts b/apps/picture/packages/shared/src/api/supabase.ts deleted file mode 100644 index fe3d680a1..000000000 --- a/apps/picture/packages/shared/src/api/supabase.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; -import type { Database } from '../types/database.types'; - -export function createSupabaseClient(supabaseUrl: string, supabaseAnonKey: string) { - return createClient(supabaseUrl, supabaseAnonKey); -} - -export type SupabaseClient = ReturnType; diff --git a/apps/picture/packages/shared/src/index.ts b/apps/picture/packages/shared/src/index.ts index 0053ba729..6d5a6ef49 100644 --- a/apps/picture/packages/shared/src/index.ts +++ b/apps/picture/packages/shared/src/index.ts @@ -1,4 +1,2 @@ export * from './types'; -export * from './api'; export * from './utils'; -export * from './queue'; diff --git a/apps/picture/packages/shared/src/queue.ts b/apps/picture/packages/shared/src/queue.ts deleted file mode 100644 index 5031ef932..000000000 --- a/apps/picture/packages/shared/src/queue.ts +++ /dev/null @@ -1,504 +0,0 @@ -/** - * Job Queue Helper Functions - * - * Provides client-side utilities for interacting with the async job queue system. - * Uses Supabase database functions to enqueue jobs and monitor status. - */ - -import type { SupabaseClient } from '@supabase/supabase-js'; -import type { Database } from './types'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export type JobType = 'generate-image' | 'download-image' | 'process-webhook' | 'cleanup-storage'; - -export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'; - -export interface JobQueueRow { - id: string; - job_type: JobType; - payload: Record; - status: JobStatus; - attempts: number; - max_attempts: number; - scheduled_at: string; - started_at: string | null; - completed_at: string | null; - error_message: string | null; - error_details: Record | null; - created_by: string | null; - priority: number; - created_at: string; - updated_at: string; -} - -export interface EnqueueJobParams { - jobType: JobType; - payload: Record; - priority?: number; - scheduledAt?: Date; - maxAttempts?: number; -} - -export interface JobStats { - total: number; - pending: number; - processing: number; - completed: number; - failed: number; - avgDurationSeconds: number; -} - -// ============================================================================ -// QUEUE FUNCTIONS -// ============================================================================ - -/** - * Enqueue a new job for background processing - * - * @example - * ```typescript - * const jobId = await enqueueJob(supabase, { - * jobType: 'generate-image', - * payload: { prompt: 'A beautiful sunset', model_id: 'flux-dev' }, - * priority: 10 - * }); - * ``` - */ -export async function enqueueJob( - supabase: SupabaseClient, - params: EnqueueJobParams -): Promise { - const { jobType, payload, priority = 0, scheduledAt = new Date(), maxAttempts = 3 } = params; - - const { data, error } = await supabase.rpc('enqueue_job', { - p_job_type: jobType, - p_payload: payload as any, - p_priority: priority, - p_scheduled_at: scheduledAt.toISOString(), - p_max_attempts: maxAttempts, - }); - - if (error) { - console.error('Failed to enqueue job:', error); - throw new Error(`Failed to enqueue job: ${error.message}`); - } - - return data as string; -} - -/** - * Get job by ID - */ -export async function getJob( - supabase: SupabaseClient, - jobId: string -): Promise { - const { data, error } = await supabase.from('job_queue').select('*').eq('id', jobId).single(); - - if (error) { - if (error.code === 'PGRST116') { - return null; // Not found - } - throw error; - } - - return data as JobQueueRow; -} - -/** - * Get all jobs for current user - */ -export async function getUserJobs( - supabase: SupabaseClient, - options?: { - status?: JobStatus; - limit?: number; - offset?: number; - } -): Promise { - let query = supabase.from('job_queue').select('*').order('created_at', { ascending: false }); - - if (options?.status) { - query = query.eq('status', options.status); - } - - if (options?.limit) { - query = query.limit(options.limit); - } - - if (options?.offset) { - query = query.range(options.offset, options.offset + (options.limit || 10) - 1); - } - - const { data, error } = await query; - - if (error) { - throw error; - } - - return (data || []) as JobQueueRow[]; -} - -/** - * Cancel a pending job - */ -export async function cancelJob(supabase: SupabaseClient, jobId: string): Promise { - const { error } = await supabase - .from('job_queue') - .update({ status: 'cancelled', updated_at: new Date().toISOString() }) - .eq('id', jobId) - .eq('status', 'pending'); // Only cancel pending jobs - - if (error) { - throw new Error(`Failed to cancel job: ${error.message}`); - } -} - -/** - * Get queue health statistics - */ -export async function getQueueStats( - supabase: SupabaseClient, - jobType?: JobType -): Promise { - const { data, error } = await supabase.from('queue_health').select('*'); - - if (error) { - throw error; - } - - // Aggregate stats - let stats: JobStats = { - total: 0, - pending: 0, - processing: 0, - completed: 0, - failed: 0, - avgDurationSeconds: 0, - }; - - const filtered = jobType ? data?.filter((row) => row.job_type === jobType) : data; - - filtered?.forEach((row) => { - const count = row.count || 0; - stats.total += count; - - switch (row.status) { - case 'pending': - stats.pending += count; - break; - case 'processing': - stats.processing += count; - break; - case 'completed': - stats.completed += count; - break; - case 'failed': - stats.failed += count; - break; - } - - if (row.avg_duration_seconds) { - stats.avgDurationSeconds = row.avg_duration_seconds; - } - }); - - return stats; -} - -/** - * Get failed jobs (last 24 hours) - */ -export async function getRecentFailedJobs( - supabase: SupabaseClient -): Promise { - const { data, error } = await supabase.from('failed_jobs_recent').select('*'); - - if (error) { - throw error; - } - - return (data || []) as JobQueueRow[]; -} - -// ============================================================================ -// REALTIME SUBSCRIPTION HELPERS -// ============================================================================ - -export type JobUpdateCallback = (job: JobQueueRow) => void; - -/** - * Subscribe to job updates via Realtime - * - * @example - * ```typescript - * const unsubscribe = subscribeToJob(supabase, jobId, (job) => { - * console.log('Job updated:', job.status); - * if (job.status === 'completed') { - * unsubscribe(); - * } - * }); - * ``` - */ -export function subscribeToJob( - supabase: SupabaseClient, - jobId: string, - callback: JobUpdateCallback -): () => void { - const channel = supabase - .channel(`job:${jobId}`) - .on( - 'postgres_changes', - { - event: 'UPDATE', - schema: 'public', - table: 'job_queue', - filter: `id=eq.${jobId}`, - }, - (payload) => { - callback(payload.new as JobQueueRow); - } - ) - .subscribe(); - - return () => { - channel.unsubscribe(); - }; -} - -/** - * Subscribe to all job updates for current user - */ -export function subscribeToUserJobs( - supabase: SupabaseClient, - userId: string, - callback: JobUpdateCallback -): () => void { - const channel = supabase - .channel(`user-jobs:${userId}`) - .on( - 'postgres_changes', - { - event: '*', - schema: 'public', - table: 'job_queue', - filter: `created_by=eq.${userId}`, - }, - (payload) => { - if (payload.new) { - callback(payload.new as JobQueueRow); - } - } - ) - .subscribe(); - - return () => { - channel.unsubscribe(); - }; -} - -// ============================================================================ -// IMAGE GENERATION HELPERS (Convenience wrappers) -// ============================================================================ - -export interface GenerateImageJobParams { - prompt: string; - model_id: string; - model_version?: string; - width?: number; - height?: number; - num_inference_steps?: number; - guidance_scale?: number; - seed?: number; - negative_prompt?: string; - source_image_url?: string; - strength?: number; - style?: string; -} - -/** - * Start an image generation job (high-level wrapper) - * - * @example - * ```typescript - * const { generationId, jobId } = await startImageGeneration(supabase, { - * prompt: 'A beautiful sunset over mountains', - * model_id: 'black-forest-labs/flux-dev' - * }); - * - * // Subscribe to updates - * subscribeToGeneration(supabase, generationId, (generation) => { - * console.log('Status:', generation.status); - * }); - * ``` - */ -export async function startImageGeneration( - supabase: SupabaseClient, - params: GenerateImageJobParams -): Promise<{ generationId: string; jobId: string }> { - // Call start-generation Edge Function - const { data, error } = await supabase.functions.invoke('start-generation', { - body: params, - }); - - if (error) { - throw new Error(`Failed to start generation: ${error.message}`); - } - - if (!data.success) { - throw new Error(data.error || 'Failed to start generation'); - } - - return { - generationId: data.generation_id, - jobId: data.job_id, - }; -} - -/** - * Subscribe to generation updates via Realtime - */ -export function subscribeToGeneration( - supabase: SupabaseClient, - generationId: string, - callback: (generation: any) => void -): () => void { - const channel = supabase - .channel(`generation:${generationId}`) - .on( - 'postgres_changes', - { - event: 'UPDATE', - schema: 'public', - table: 'image_generations', - filter: `id=eq.${generationId}`, - }, - (payload) => { - callback(payload.new); - } - ) - .subscribe(); - - return () => { - channel.unsubscribe(); - }; -} - -/** - * Combined helper: Start generation and subscribe to updates - * - * @example - * ```typescript - * const { generationId, unsubscribe } = await generateImageWithUpdates( - * supabase, - * { prompt: 'A sunset', model_id: 'flux-dev' }, - * (generation) => { - * console.log('Status:', generation.status); - * if (generation.status === 'completed') { - * console.log('Image ready!', generation.id); - * unsubscribe(); - * } - * } - * ); - * ``` - */ -export async function generateImageWithUpdates( - supabase: SupabaseClient, - params: GenerateImageJobParams, - onUpdate: (generation: any) => void -): Promise<{ generationId: string; jobId: string; unsubscribe: () => void }> { - // Start generation - const { generationId, jobId } = await startImageGeneration(supabase, params); - - // Subscribe to updates - const unsubscribe = subscribeToGeneration(supabase, generationId, onUpdate); - - return { generationId, jobId, unsubscribe }; -} - -// ============================================================================ -// POLLING HELPERS (fallback if Realtime not available) -// ============================================================================ - -/** - * Poll for job status (fallback for environments without Realtime) - */ -export async function pollJobUntilComplete( - supabase: SupabaseClient, - jobId: string, - options: { - maxAttempts?: number; - intervalMs?: number; - onUpdate?: JobUpdateCallback; - } = {} -): Promise { - const { maxAttempts = 60, intervalMs = 2000, onUpdate } = options; - - let attempts = 0; - - while (attempts < maxAttempts) { - const job = await getJob(supabase, jobId); - - if (!job) { - throw new Error('Job not found'); - } - - if (onUpdate) { - onUpdate(job); - } - - if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') { - return job; - } - - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - attempts++; - } - - throw new Error('Job polling timeout'); -} - -/** - * Poll for generation completion - */ -export async function pollGenerationUntilComplete( - supabase: SupabaseClient, - generationId: string, - options: { - maxAttempts?: number; - intervalMs?: number; - onUpdate?: (generation: any) => void; - } = {} -): Promise { - const { maxAttempts = 120, intervalMs = 2000, onUpdate } = options; - - let attempts = 0; - - while (attempts < maxAttempts) { - const { data: generation, error } = await supabase - .from('image_generations') - .select('*') - .eq('id', generationId) - .single(); - - if (error) { - throw error; - } - - if (onUpdate) { - onUpdate(generation); - } - - if (generation.status === 'completed' || generation.status === 'failed') { - return generation; - } - - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - attempts++; - } - - throw new Error('Generation polling timeout'); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14c1c1a4f..b4cfe945f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1077,6 +1077,9 @@ importers: apps/picture/apps/backend: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.700.0 + version: 3.940.0 '@manacore/shared-errors': specifier: workspace:* version: link:../../../../packages/shared-errors @@ -1092,9 +1095,6 @@ importers: '@nestjs/platform-express': specifier: ^10.4.15 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@supabase/supabase-js': - specifier: ^2.45.0 - version: 2.84.0 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -1259,9 +1259,6 @@ importers: '@react-navigation/native': specifier: ^7.0.3 version: 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) - '@supabase/supabase-js': - specifier: ^2.38.4 - version: 2.84.0 blurhash: specifier: ^2.0.5 version: 2.0.5 @@ -1571,10 +1568,6 @@ importers: version: 5.9.3 apps/picture/packages/shared: - dependencies: - '@supabase/supabase-js': - specifier: ^2.38.4 - version: 2.84.0 devDependencies: '@babel/core': specifier: ^7.28.4 @@ -3253,6 +3246,165 @@ packages: '@astrojs/yaml2ts@0.2.2': resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.940.0': + resolution: {integrity: sha512-Wi4qnBT6shRRMXuuTgjMFTU5mu2KFWisgcigEMPptjPGUtJvBVi4PTGgS64qsLoUk/obqDAyOBOfEtRZ2ddC2w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.940.0': + resolution: {integrity: sha512-SdqJGWVhmIURvCSgkDditHRO+ozubwZk9aCX9MK8qxyOndhobCndW1ozl3hX9psvMAo9Q4bppjuqy/GHWpjB+A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.940.0': + resolution: {integrity: sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.940.0': + resolution: {integrity: sha512-/G3l5/wbZYP2XEQiOoIkRJmlv15f1P3MSd1a0gz27lHEMrOJOGq66rF1Ca4OJLzapWt3Fy9BPrZAepoAX11kMw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.940.0': + resolution: {integrity: sha512-dOrc03DHElNBD6N9Okt4U0zhrG4Wix5QUBSZPr5VN8SvmjD9dkrrxOkkJaMCl/bzrW7kbQEp7LuBdbxArMmOZQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.940.0': + resolution: {integrity: sha512-gn7PJQEzb/cnInNFTOaDoCN/hOKqMejNmLof1W5VW95Qk0TPO52lH8R4RmJPnRrwFMswOWswTOpR1roKNLIrcw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.940.0': + resolution: {integrity: sha512-fOKC3VZkwa9T2l2VFKWRtfHQPQuISqqNl35ZhcXjWKVwRwl/o7THPMkqI4XwgT2noGa7LLYVbWMwnsgSsBqglg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.940.0': + resolution: {integrity: sha512-M8NFAvgvO6xZjiti5kztFiAYmSmSlG3eUfr4ZHSfXYZUA/KUdZU/D6xJyaLnU8cYRWBludb6K9XPKKVwKfqm4g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.940.0': + resolution: {integrity: sha512-pILBzt5/TYCqRsJb7vZlxmRIe0/T+FZPeml417EK75060ajDGnVJjHcuVdLVIeKoTKm9gmJc9l45gon6PbHyUQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.940.0': + resolution: {integrity: sha512-q6JMHIkBlDCOMnA3RAzf8cGfup+8ukhhb50fNpghMs1SNBGhanmaMbZSgLigBRsPQW7fOk2l8jnzdVLS+BB9Uw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.940.0': + resolution: {integrity: sha512-9QLTIkDJHHaYL0nyymO41H8g3ui1yz6Y3GmAN1gYQa6plXisuFBnGAbmKVj7zNvjWaOKdF0dV3dd3AFKEDoJ/w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.936.0': + resolution: {integrity: sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-expect-continue@3.936.0': + resolution: {integrity: sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.940.0': + resolution: {integrity: sha512-WdsxDAVj5qaa5ApAP+JbpCOMHFGSmzjs2Y2OBSbWPeR9Ew7t/Okj+kUub94QJPsgzhvU1/cqNejhsw5VxeFKSQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.936.0': + resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-location-constraint@3.936.0': + resolution: {integrity: sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.936.0': + resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.936.0': + resolution: {integrity: sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.940.0': + resolution: {integrity: sha512-JYkLjgS1wLoKHJ40G63+afM1ehmsPsjcmrHirKh8+kSCx4ip7+nL1e/twV4Zicxr8RJi9Y0Ahq5mDvneilDDKQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-ssec@3.936.0': + resolution: {integrity: sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.940.0': + resolution: {integrity: sha512-nJbLrUj6fY+l2W2rIB9P4Qvpiy0tnTdg/dmixRxrU1z3e8wBdspJlyE+AZN4fuVbeL6rrRrO/zxQC1bB3cw5IA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.940.0': + resolution: {integrity: sha512-x0mdv6DkjXqXEcQj3URbCltEzW6hoy/1uIL+i8gExP6YKrnhiZ7SzuB4gPls2UOpK5UqLiqXjhRLfBb1C9i4Dw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.936.0': + resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.940.0': + resolution: {integrity: sha512-ugHZEoktD/bG6mdgmhzLDjMP2VrYRAUPRPF1DpCyiZexkH7DCU7XrSJyXMvkcf0DHV+URk0q2sLf/oqn1D2uYw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.940.0': + resolution: {integrity: sha512-k5qbRe/ZFjW9oWEdzLIa2twRVIEx7p/9rutofyrRysrtEnYh3HAWCngAnwbgKMoiwa806UzcTRx0TjyEpnKcCg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.936.0': + resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.893.0': + resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.936.0': + resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.893.0': + resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.936.0': + resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} + + '@aws-sdk/util-user-agent-node@3.940.0': + resolution: {integrity: sha512-dlD/F+L/jN26I8Zg5x0oDGJiA+/WEQmnSE27fi5ydvYnpfQLwThtQo9SsNS47XSR/SOULaaoC9qx929rZuo74A==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.930.0': + resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.1': + resolution: {integrity: sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==} + engines: {node: '>=18.0.0'} + '@azure-rest/core-client@2.5.1': resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} engines: {node: '>=20.0.0'} @@ -7261,6 +7413,222 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@smithy/abort-controller@4.2.5': + resolution: {integrity: sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.3': + resolution: {integrity: sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.18.6': + resolution: {integrity: sha512-8Q/ugWqfDUEU1Exw71+DoOzlONJ2Cn9QA8VeeDzLLjzO/qruh9UKFzbszy4jXcIYgGofxYiT0t1TT6+CT/GupQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.5': + resolution: {integrity: sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.5': + resolution: {integrity: sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.5': + resolution: {integrity: sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.5': + resolution: {integrity: sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.5': + resolution: {integrity: sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.5': + resolution: {integrity: sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.6': + resolution: {integrity: sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.6': + resolution: {integrity: sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.5': + resolution: {integrity: sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.5': + resolution: {integrity: sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.5': + resolution: {integrity: sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.5': + resolution: {integrity: sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.5': + resolution: {integrity: sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.3.13': + resolution: {integrity: sha512-X4za1qCdyx1hEVVXuAWlZuK6wzLDv1uw1OY9VtaYy1lULl661+frY7FeuHdYdl7qAARUxH2yvNExU2/SmRFfcg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.13': + resolution: {integrity: sha512-RzIDF9OrSviXX7MQeKOm8r/372KTyY8Jmp6HNKOOYlrguHADuM3ED/f4aCyNhZZFLG55lv5beBin7nL0Nzy1Dw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.6': + resolution: {integrity: sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.5': + resolution: {integrity: sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.5': + resolution: {integrity: sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.5': + resolution: {integrity: sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.5': + resolution: {integrity: sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.5': + resolution: {integrity: sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.5': + resolution: {integrity: sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.5': + resolution: {integrity: sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.5': + resolution: {integrity: sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.0': + resolution: {integrity: sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.5': + resolution: {integrity: sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.9.9': + resolution: {integrity: sha512-SUnZJMMo5yCmgjopJbiNeo1vlr8KvdnEfIHV9rlD77QuOGdRotIVBcOrBuMr+sI9zrnhtDtLP054bZVbpZpiQA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.9.0': + resolution: {integrity: sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.5': + resolution: {integrity: sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.12': + resolution: {integrity: sha512-TKc6FnOxFULKxLgTNHYjcFqdOYzXVPFFVm5JhI30F3RdhT7nYOtOsjgaOwfDRmA/3U66O9KaBQ3UHoXwayRhAg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.15': + resolution: {integrity: sha512-94NqfQVo+vGc5gsQ9SROZqOvBkGNMQu6pjXbnn8aQvBUhc31kx49gxlkBEqgmaZQHUUfdRUin5gK/HlHKmbAwg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.5': + resolution: {integrity: sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.5': + resolution: {integrity: sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.5': + resolution: {integrity: sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.6': + resolution: {integrity: sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.5': + resolution: {integrity: sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} @@ -8758,6 +9126,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + boxen@5.1.2: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} @@ -11069,6 +11440,10 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -15507,6 +15882,9 @@ packages: resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} engines: {node: '>=12.*'} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -17306,6 +17684,485 @@ snapshots: dependencies: yaml: 2.8.1 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-locate-window': 3.893.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.936.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.940.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/credential-provider-node': 3.940.0 + '@aws-sdk/middleware-bucket-endpoint': 3.936.0 + '@aws-sdk/middleware-expect-continue': 3.936.0 + '@aws-sdk/middleware-flexible-checksums': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-location-constraint': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-sdk-s3': 3.940.0 + '@aws-sdk/middleware-ssec': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/signature-v4-multi-region': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.6 + '@smithy/eventstream-serde-browser': 4.2.5 + '@smithy/eventstream-serde-config-resolver': 4.3.5 + '@smithy/eventstream-serde-node': 4.2.5 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-blob-browser': 4.2.6 + '@smithy/hash-node': 4.2.5 + '@smithy/hash-stream-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/md5-js': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.13 + '@smithy/middleware-retry': 4.4.13 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.12 + '@smithy/util-defaults-mode-node': 4.2.15 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.5 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.940.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.6 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.13 + '@smithy/middleware-retry': 4.4.13 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.12 + '@smithy/util-defaults-mode-node': 4.2.15 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.940.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws-sdk/xml-builder': 3.930.0 + '@smithy/core': 3.18.6 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/credential-provider-env': 3.940.0 + '@aws-sdk/credential-provider-http': 3.940.0 + '@aws-sdk/credential-provider-login': 3.940.0 + '@aws-sdk/credential-provider-process': 3.940.0 + '@aws-sdk/credential-provider-sso': 3.940.0 + '@aws-sdk/credential-provider-web-identity': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.940.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.940.0 + '@aws-sdk/credential-provider-http': 3.940.0 + '@aws-sdk/credential-provider-ini': 3.940.0 + '@aws-sdk/credential-provider-process': 3.940.0 + '@aws-sdk/credential-provider-sso': 3.940.0 + '@aws-sdk/credential-provider-web-identity': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.940.0': + dependencies: + '@aws-sdk/client-sso': 3.940.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/token-providers': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.940.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@aws/lambda-invoke-store': 0.2.1 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/core': 3.18.6 + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@smithy/core': 3.18.6 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.940.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.940.0 + '@aws-sdk/middleware-host-header': 3.936.0 + '@aws-sdk/middleware-logger': 3.936.0 + '@aws-sdk/middleware-recursion-detection': 3.936.0 + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/region-config-resolver': 3.936.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-endpoints': 3.936.0 + '@aws-sdk/util-user-agent-browser': 3.936.0 + '@aws-sdk/util-user-agent-node': 3.940.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/core': 3.18.6 + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/hash-node': 4.2.5 + '@smithy/invalid-dependency': 4.2.5 + '@smithy/middleware-content-length': 4.2.5 + '@smithy/middleware-endpoint': 4.3.13 + '@smithy/middleware-retry': 4.4.13 + '@smithy/middleware-serde': 4.2.6 + '@smithy/middleware-stack': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/node-http-handler': 4.4.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.12 + '@smithy/util-defaults-mode-node': 4.2.15 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/config-resolver': 4.4.3 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.940.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/signature-v4': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.940.0': + dependencies: + '@aws-sdk/core': 3.940.0 + '@aws-sdk/nested-clients': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.936.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.893.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-endpoints': 3.2.5 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.893.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/types': 4.9.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.940.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.930.0': + dependencies: + '@smithy/types': 4.9.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.1': {} + '@azure-rest/core-client@2.5.1': dependencies: '@azure/abort-controller': 2.1.2 @@ -23105,6 +23962,344 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.1': + dependencies: + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.3': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/core@3.18.6': + dependencies: + '@smithy/middleware-serde': 4.2.6 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-stream': 4.5.6 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.5': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.9.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.5': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.5': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.5': + dependencies: + '@smithy/eventstream-codec': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.6': + dependencies: + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.5': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.3.13': + dependencies: + '@smithy/core': 3.18.6 + '@smithy/middleware-serde': 4.2.6 + '@smithy/node-config-provider': 4.3.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + '@smithy/url-parser': 4.2.5 + '@smithy/util-middleware': 4.2.5 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.13': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/service-error-classification': 4.2.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-retry': 4.2.5 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.6': + dependencies: + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.5': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/shared-ini-file-loader': 4.4.0 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.5': + dependencies: + '@smithy/abort-controller': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + + '@smithy/shared-ini-file-loader@4.4.0': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.5': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.5 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.9.9': + dependencies: + '@smithy/core': 3.18.6 + '@smithy/middleware-endpoint': 4.3.13 + '@smithy/middleware-stack': 4.2.5 + '@smithy/protocol-http': 5.3.5 + '@smithy/types': 4.9.0 + '@smithy/util-stream': 4.5.6 + tslib: 2.8.1 + + '@smithy/types@4.9.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.5': + dependencies: + '@smithy/querystring-parser': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.12': + dependencies: + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.15': + dependencies: + '@smithy/config-resolver': 4.4.3 + '@smithy/credential-provider-imds': 4.2.5 + '@smithy/node-config-provider': 4.3.5 + '@smithy/property-provider': 4.2.5 + '@smithy/smithy-client': 4.9.9 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.5': + dependencies: + '@smithy/node-config-provider': 4.3.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.5': + dependencies: + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.5': + dependencies: + '@smithy/service-error-classification': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.6': + dependencies: + '@smithy/fetch-http-handler': 5.3.6 + '@smithy/node-http-handler': 4.4.5 + '@smithy/types': 4.9.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.5': + dependencies: + '@smithy/abort-controller': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + '@so-ric/colorspace@1.1.6': dependencies: color: 5.0.3 @@ -25797,6 +26992,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.13.1: {} + boxen@5.1.2: dependencies: ansi-align: 3.0.1 @@ -29374,6 +30571,10 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -36266,6 +37467,8 @@ snapshots: '@types/node': 22.19.1 qs: 6.14.0 + strnum@2.1.1: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index aac5543bf..84b6348f2 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -224,11 +224,21 @@ const APP_CONFIGS = [ path: 'apps/picture/apps/backend/.env', vars: { NODE_ENV: () => 'development', - PORT: () => '3003', - DATABASE_URL: () => 'postgresql://picture:picturepassword@localhost:5434/picture', + PORT: (env) => env.PICTURE_BACKEND_PORT || '3003', + BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003', + DATABASE_URL: (env) => env.PICTURE_DATABASE_URL || 'postgresql://picture:picturepassword@localhost:5434/picture', MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, - REPLICATE_API_TOKEN: (env) => env.MAERCHENZAUBER_REPLICATE_API_KEY, // Reuse existing Replicate key + REPLICATE_API_TOKEN: (env) => env.MAERCHENZAUBER_REPLICATE_API_KEY, CORS_ORIGINS: (env) => env.CORS_ORIGINS, + // Storage configuration + STORAGE_MODE: (env) => env.PICTURE_STORAGE_MODE || 'local', + 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 || '', }, }, @@ -241,7 +251,7 @@ const APP_CONFIGS = [ }, }, - // Picture Web (SvelteKit) + // Picture Web (SvelteKit) - No Supabase, uses Backend API { path: 'apps/picture/apps/web/.env', vars: {