From 2cfa09c84d638ad4253dd89a9bedc59d1dd74e4a Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Tue, 2 Dec 2025 01:00:42 +0100 Subject: [PATCH] feat(storage): add MinIO local storage and @manacore/shared-storage package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MinIO service to docker-compose.dev.yml with auto-bucket initialization - Create @manacore/shared-storage package with S3-compatible client - Add factory functions for each project (Picture, Chat, ManaDeck, etc.) - Include file utilities (generateFileKey, getContentType, validators) - Update environment variables for S3/MinIO configuration - Document storage architecture in CLAUDE.md Local dev uses MinIO, production will use Hetzner Object Storage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.development | 31 +- CLAUDE.md | 72 +++++ docker-compose.dev.yml | 45 +++ package.json | 4 +- packages/shared-storage/README.md | 160 ++++++++++ packages/shared-storage/package.json | 23 ++ packages/shared-storage/src/client.ts | 194 ++++++++++++ packages/shared-storage/src/factory.ts | 107 +++++++ packages/shared-storage/src/index.ts | 38 +++ packages/shared-storage/src/types.ts | 86 +++++ packages/shared-storage/src/utils.ts | 135 ++++++++ packages/shared-storage/tsconfig.json | 16 + pnpm-lock.yaml | 414 ++++++++++++++++++------- 13 files changed, 1195 insertions(+), 130 deletions(-) create mode 100644 packages/shared-storage/README.md create mode 100644 packages/shared-storage/package.json create mode 100644 packages/shared-storage/src/client.ts create mode 100644 packages/shared-storage/src/factory.ts create mode 100644 packages/shared-storage/src/index.ts create mode 100644 packages/shared-storage/src/types.ts create mode 100644 packages/shared-storage/src/utils.ts create mode 100644 packages/shared-storage/tsconfig.json diff --git a/.env.development b/.env.development index e31c43f5b..8621b8a7b 100644 --- a/.env.development +++ b/.env.development @@ -28,6 +28,14 @@ REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=devpassword +# MinIO Object Storage (local S3-compatible storage) +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +S3_ENDPOINT=http://localhost:9000 +S3_REGION=us-east-1 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin + # ============================================ # MANA-CORE-AUTH SERVICE # ============================================ @@ -124,17 +132,9 @@ PICTURE_BACKEND_PORT=3006 PICTURE_BACKEND_URL=http://localhost:3006 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 +# Storage Configuration (uses MinIO locally, Hetzner in production) +# Uses shared S3_* variables from above - no project-specific override needed for local dev +PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-images # OAuth (optional - leave empty to disable) PICTURE_GOOGLE_CLIENT_ID= @@ -149,13 +149,8 @@ NUTRIPHI_DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435 NUTRIPHI_APP_ID=nutriphi NUTRIPHI_GEMINI_API_KEY=your-gemini-api-key-here -# S3 Storage (Hetzner Object Storage) -NUTRIPHI_S3_ENDPOINT=https://fsn1.your-objectstorage.com -NUTRIPHI_S3_ACCESS_KEY_ID=your-access-key-id -NUTRIPHI_S3_SECRET_ACCESS_KEY=your-secret-access-key -NUTRIPHI_S3_BUCKET_NAME=nutriphi-meals -NUTRIPHI_S3_REGION=fsn1 -NUTRIPHI_S3_PUBLIC_URL=https://nutriphi-meals.fsn1.your-objectstorage.com +# S3 Storage (uses MinIO locally via shared S3_* variables, Hetzner in production) +NUTRIPHI_S3_PUBLIC_URL=http://localhost:9000/nutriphi-meals # ============================================ # ZITARE PROJECT diff --git a/CLAUDE.md b/CLAUDE.md index 6ad47d2e6..2a2dfa72a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -304,6 +304,7 @@ $: doubled = count * 2; | `@manacore/shared-nestjs-auth` | NestJS JWT validation guards via mana-core-auth | | `@mana-core/nestjs-integration` | NestJS module with auth guards + credit client | | `@manacore/shared-auth` | Client-side auth service for web/mobile apps | +| `@manacore/shared-storage` | S3-compatible storage (MinIO local, Hetzner prod) | | `@manacore/shared-supabase` | Unified Supabase client | | `@manacore/shared-types` | Common TypeScript types | | `@manacore/shared-utils` | Utility functions | @@ -325,6 +326,77 @@ import { formatDate, truncate } from '@manacore/shared-utils'; - Each project has its own Supabase project/schema - Types typically generated via `supabase gen types` +## Object Storage (MinIO / Hetzner) + +S3-compatible object storage for file uploads, generated images, etc. + +### Architecture + +| Environment | Service | Purpose | +|-------------|---------|---------| +| **Local** | MinIO (Docker) | S3-compatible local storage | +| **Production** | Hetzner Object Storage | Cost-effective S3-compatible cloud storage | + +### Local Development + +```bash +# Start infrastructure (includes MinIO) +pnpm docker:up + +# MinIO Web Console: http://localhost:9001 +# Username: minioadmin +# Password: minioadmin + +# S3 API endpoint: http://localhost:9000 +``` + +### Pre-configured Buckets + +| Bucket | Project | Purpose | +|--------|---------|---------| +| `picture-images` | Picture | AI-generated images | +| `chat-files` | Chat | User file uploads | +| `manadeck-assets` | ManaDeck | Card/deck assets | +| `nutriphi-meals` | NutriPhi | Meal photos | +| `presi-slides` | Presi | Presentation slides | + +### Usage in Backend + +```typescript +import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage'; + +const storage = createPictureStorage(); + +// Upload +const key = generateUserFileKey(userId, 'image.png'); +const result = await storage.upload(key, buffer, { + contentType: getContentType('image.png'), + public: true, +}); + +// Download +const data = await storage.download(key); + +// Presigned URLs +const uploadUrl = await storage.getUploadUrl(key, { expiresIn: 3600 }); +``` + +### Environment Variables + +```env +# Local (in .env.development) +S3_ENDPOINT=http://localhost:9000 +S3_REGION=us-east-1 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin + +# Production (Hetzner) +S3_ENDPOINT=https://fsn1.your-objectstorage.com +S3_REGION=fsn1 +S3_ACCESS_KEY=your-access-key +S3_SECRET_KEY=your-secret-key +``` + ## Adding Dependencies ```bash diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 606309043..a0e84c4d5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -44,6 +44,50 @@ services: timeout: 5s retries: 5 + # MinIO Object Storage (S3-compatible) + minio: + image: minio/minio:latest + container_name: manacore-minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + volumes: + - minio-data:/data + ports: + - "9000:9000" # S3 API + - "9001:9001" # Web Console + networks: + - manacore-network + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 30s + timeout: 20s + retries: 3 + + # MinIO bucket initialization (runs once) + minio-init: + image: minio/mc:latest + container_name: manacore-minio-init + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set myminio http://minio:9000 $${MINIO_ROOT_USER:-minioadmin} $${MINIO_ROOT_PASSWORD:-minioadmin}; + mc mb --ignore-existing myminio/picture-images; + mc mb --ignore-existing myminio/chat-files; + mc mb --ignore-existing myminio/manadeck-assets; + mc mb --ignore-existing myminio/nutriphi-meals; + mc mb --ignore-existing myminio/presi-slides; + mc anonymous set download myminio/picture-images; + echo 'Buckets created successfully'; + exit 0; + " + networks: + - manacore-network + # Mana Core Auth Service mana-core-auth: profiles: ["auth", "all"] @@ -115,3 +159,4 @@ networks: volumes: postgres-data: redis-data: + minio-data: diff --git a/package.json b/package.json index 0035870e7..007e0c2eb 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,9 @@ "dev:mana-games:web": "pnpm --filter @mana-games/web dev", "dev:mana-games:backend": "pnpm --filter @mana-games/backend dev", "dev:mana-games:app": "turbo run dev --filter=@mana-games/web --filter=@mana-games/backend", - "docker:up": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis", + "docker:up": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis minio minio-init", + "docker:up:infra": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis minio minio-init", + "docker:up:db": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis", "docker:up:auth": "docker compose -f docker-compose.dev.yml --env-file .env.development --profile auth up -d", "docker:up:chat": "docker compose -f docker-compose.dev.yml --env-file .env.development --profile chat up -d", "docker:up:all": "docker compose -f docker-compose.dev.yml --env-file .env.development --profile all up -d", diff --git a/packages/shared-storage/README.md b/packages/shared-storage/README.md new file mode 100644 index 000000000..d194d1477 --- /dev/null +++ b/packages/shared-storage/README.md @@ -0,0 +1,160 @@ +# @manacore/shared-storage + +S3-compatible object storage client for the Manacore monorepo. Uses MinIO for local development and Hetzner Object Storage in production. + +## Setup + +### Local Development + +1. Start MinIO with Docker: + +```bash +pnpm docker:up +``` + +2. Access MinIO Console at http://localhost:9001 + - Username: `minioadmin` + - Password: `minioadmin` + +### Pre-created Buckets + +The following buckets are automatically created: + +| Bucket | Project | Purpose | +|--------|---------|---------| +| `picture-images` | Picture | Generated AI images | +| `chat-files` | Chat | User file uploads | +| `manadeck-assets` | ManaDeck | Card/deck assets | +| `nutriphi-meals` | NutriPhi | Meal photos | +| `presi-slides` | Presi | Presentation slides | + +## Usage + +### Basic Usage + +```typescript +import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage'; + +// Create client for Picture project +const storage = createPictureStorage(); + +// Upload a file +const key = generateUserFileKey('user-123', 'avatar.png'); +const result = await storage.upload(key, imageBuffer, { + contentType: getContentType('avatar.png'), + public: true, +}); + +console.log(result.url); // http://localhost:9000/picture-images/users/user-123/uuid.png + +// Download a file +const buffer = await storage.download(key); + +// Delete a file +await storage.delete(key); + +// List files +const files = await storage.list('users/user-123/'); + +// Generate presigned URLs +const uploadUrl = await storage.getUploadUrl('temp/upload.png', { expiresIn: 3600 }); +const downloadUrl = await storage.getDownloadUrl(key, { expiresIn: 3600 }); +``` + +### Custom Configuration + +```typescript +import { createStorageClient, BUCKETS } from '@manacore/shared-storage'; + +// Override default config +const storage = createStorageClient(BUCKETS.PICTURE, { + endpoint: 'https://fsn1.your-objectstorage.com', + region: 'fsn1', + accessKeyId: process.env.HETZNER_ACCESS_KEY, + secretAccessKey: process.env.HETZNER_SECRET_KEY, + forcePathStyle: false, +}); +``` + +### Available Factory Functions + +```typescript +import { + createPictureStorage, + createChatStorage, + createManaDeckStorage, + createNutriPhiStorage, + createPresiStorage, +} from '@manacore/shared-storage'; +``` + +## Environment Variables + +### Local Development (MinIO) + +Already configured in `.env.development`: + +```env +S3_ENDPOINT=http://localhost:9000 +S3_REGION=us-east-1 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +``` + +### Production (Hetzner Object Storage) + +```env +S3_ENDPOINT=https://fsn1.your-objectstorage.com +S3_REGION=fsn1 +S3_ACCESS_KEY=your-access-key +S3_SECRET_KEY=your-secret-key + +# Optional: public URLs for CDN access +PICTURE_STORAGE_PUBLIC_URL=https://picture-images.fsn1.your-objectstorage.com +NUTRIPHI_S3_PUBLIC_URL=https://nutriphi-meals.fsn1.your-objectstorage.com +``` + +## Utilities + +```typescript +import { + generateFileKey, + generateUserFileKey, + getContentType, + validateFileSize, + validateFileExtension, + IMAGE_EXTENSIONS, + DOCUMENT_EXTENSIONS, +} from '@manacore/shared-storage'; + +// Generate unique file key +const key = generateFileKey('photo.jpg', 'uploads', '2024'); +// => 'uploads/2024/uuid.jpg' + +// User-scoped key +const userKey = generateUserFileKey('user-123', 'avatar.png', 'avatars'); +// => 'users/user-123/avatars/uuid.png' + +// Get MIME type +const contentType = getContentType('image.png'); // => 'image/png' + +// Validate file +const isValidSize = validateFileSize(fileSize, 10); // max 10MB +const isValidType = validateFileExtension('photo.jpg', IMAGE_EXTENSIONS); +``` + +## Docker Commands + +```bash +# Start all infrastructure (Postgres, Redis, MinIO) +pnpm docker:up + +# Start only database services (no MinIO) +pnpm docker:up:db + +# View MinIO logs +docker logs manacore-minio + +# View bucket init logs +docker logs manacore-minio-init +``` diff --git a/packages/shared-storage/package.json b/packages/shared-storage/package.json new file mode 100644 index 000000000..6becf1021 --- /dev/null +++ b/packages/shared-storage/package.json @@ -0,0 +1,23 @@ +{ + "name": "@manacore/shared-storage", + "version": "0.1.0", + "private": true, + "description": "S3-compatible object storage client for Manacore monorepo (MinIO local, Hetzner production)", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "type-check": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "@aws-sdk/s3-request-presigner": "^3.700.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.9.3" + } +} diff --git a/packages/shared-storage/src/client.ts b/packages/shared-storage/src/client.ts new file mode 100644 index 000000000..57c8f06da --- /dev/null +++ b/packages/shared-storage/src/client.ts @@ -0,0 +1,194 @@ +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectCommand, + ListObjectsV2Command, + HeadObjectCommand, + type PutObjectCommandInput, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import type { + StorageConfig, + BucketConfig, + UploadOptions, + PresignedUrlOptions, + UploadResult, + FileInfo, +} from './types.js'; + +/** + * S3-compatible storage client for MinIO (local) and Hetzner Object Storage (production) + */ +export class StorageClient { + private client: S3Client; + private bucket: BucketConfig; + + constructor(config: StorageConfig, bucket: BucketConfig) { + this.client = new S3Client({ + endpoint: config.endpoint, + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + forcePathStyle: config.forcePathStyle ?? true, + }); + this.bucket = bucket; + } + + /** + * Upload a file to the bucket + */ + async upload( + key: string, + body: Buffer | Uint8Array | string | ReadableStream, + options: UploadOptions = {} + ): Promise { + const input: PutObjectCommandInput = { + Bucket: this.bucket.name, + Key: key, + Body: body, + ContentType: options.contentType, + CacheControl: options.cacheControl, + Metadata: options.metadata, + }; + + if (options.public) { + input.ACL = 'public-read'; + } + + const command = new PutObjectCommand(input); + const result = await this.client.send(command); + + return { + key, + url: this.getPublicUrl(key), + etag: result.ETag, + }; + } + + /** + * Download a file from the bucket + */ + async download(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket.name, + Key: key, + }); + + const response = await this.client.send(command); + + if (!response.Body) { + throw new Error(`File not found: ${key}`); + } + + // Convert stream to buffer + const chunks: Uint8Array[] = []; + const stream = response.Body as AsyncIterable; + for await (const chunk of stream) { + chunks.push(chunk); + } + return Buffer.concat(chunks); + } + + /** + * Delete a file from the bucket + */ + async delete(key: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucket.name, + Key: key, + }); + + await this.client.send(command); + } + + /** + * Check if a file exists + */ + async exists(key: string): Promise { + try { + const command = new HeadObjectCommand({ + Bucket: this.bucket.name, + Key: key, + }); + await this.client.send(command); + return true; + } catch { + return false; + } + } + + /** + * List files in the bucket with optional prefix + */ + async list(prefix?: string, maxKeys = 1000): Promise { + const command = new ListObjectsV2Command({ + Bucket: this.bucket.name, + Prefix: prefix, + MaxKeys: maxKeys, + }); + + const response = await this.client.send(command); + + return (response.Contents ?? []).map((item) => ({ + key: item.Key!, + size: item.Size ?? 0, + lastModified: item.LastModified ?? new Date(), + etag: item.ETag, + })); + } + + /** + * Generate a presigned URL for uploading (PUT) + */ + async getUploadUrl(key: string, options: PresignedUrlOptions = {}): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucket.name, + Key: key, + }); + + return getSignedUrl(this.client, command, { + expiresIn: options.expiresIn ?? 3600, + }); + } + + /** + * Generate a presigned URL for downloading (GET) + */ + async getDownloadUrl(key: string, options: PresignedUrlOptions = {}): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket.name, + Key: key, + }); + + return getSignedUrl(this.client, command, { + expiresIn: options.expiresIn ?? 3600, + }); + } + + /** + * Get the public URL for a file (if bucket is public) + */ + getPublicUrl(key: string): string | undefined { + if (!this.bucket.publicUrl) { + return undefined; + } + return `${this.bucket.publicUrl}/${key}`; + } + + /** + * Get the underlying S3 client for advanced operations + */ + getS3Client(): S3Client { + return this.client; + } + + /** + * Get the bucket name + */ + getBucketName(): string { + return this.bucket.name; + } +} diff --git a/packages/shared-storage/src/factory.ts b/packages/shared-storage/src/factory.ts new file mode 100644 index 000000000..58dc2e8a1 --- /dev/null +++ b/packages/shared-storage/src/factory.ts @@ -0,0 +1,107 @@ +import { StorageClient } from './client.js'; +import { BUCKETS, type StorageConfig, type BucketConfig, type BucketName } from './types.js'; + +/** + * Environment variable names for storage configuration + */ +const ENV_KEYS = { + ENDPOINT: 'S3_ENDPOINT', + REGION: 'S3_REGION', + ACCESS_KEY: 'S3_ACCESS_KEY', + SECRET_KEY: 'S3_SECRET_KEY', +} as const; + +/** + * Default configuration for local MinIO development + */ +const MINIO_DEFAULTS: StorageConfig = { + endpoint: 'http://localhost:9000', + region: 'us-east-1', + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin', + forcePathStyle: true, +}; + +/** + * Get storage configuration from environment variables + * Falls back to MinIO defaults in development + */ +export function getStorageConfig(): StorageConfig { + const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; + + // Use environment variables if available, otherwise use MinIO defaults + return { + endpoint: process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''), + region: process.env[ENV_KEYS.REGION] ?? MINIO_DEFAULTS.region, + accessKeyId: process.env[ENV_KEYS.ACCESS_KEY] ?? (isDev ? MINIO_DEFAULTS.accessKeyId : ''), + secretAccessKey: + process.env[ENV_KEYS.SECRET_KEY] ?? (isDev ? MINIO_DEFAULTS.secretAccessKey : ''), + forcePathStyle: isDev || process.env[ENV_KEYS.ENDPOINT]?.includes('localhost'), + }; +} + +/** + * Create a storage client for a specific bucket + */ +export function createStorageClient( + bucket: BucketName | BucketConfig, + config?: Partial +): StorageClient { + const storageConfig = { + ...getStorageConfig(), + ...config, + }; + + const bucketConfig: BucketConfig = typeof bucket === 'string' ? { name: bucket } : bucket; + + // Validate configuration + if (!storageConfig.endpoint) { + throw new Error('S3_ENDPOINT is required for storage configuration'); + } + if (!storageConfig.accessKeyId || !storageConfig.secretAccessKey) { + throw new Error('S3_ACCESS_KEY and S3_SECRET_KEY are required'); + } + + return new StorageClient(storageConfig, bucketConfig); +} + +/** + * Create a storage client for the Picture project + */ +export function createPictureStorage(publicUrl?: string): StorageClient { + return createStorageClient({ + name: BUCKETS.PICTURE, + publicUrl: publicUrl ?? process.env.PICTURE_STORAGE_PUBLIC_URL, + }); +} + +/** + * Create a storage client for the Chat project + */ +export function createChatStorage(): StorageClient { + return createStorageClient({ name: BUCKETS.CHAT }); +} + +/** + * Create a storage client for the ManaDeck project + */ +export function createManaDeckStorage(): StorageClient { + return createStorageClient({ name: BUCKETS.MANADECK }); +} + +/** + * Create a storage client for the NutriPhi project + */ +export function createNutriPhiStorage(publicUrl?: string): StorageClient { + return createStorageClient({ + name: BUCKETS.NUTRIPHI, + publicUrl: publicUrl ?? process.env.NUTRIPHI_S3_PUBLIC_URL, + }); +} + +/** + * Create a storage client for the Presi project + */ +export function createPresiStorage(): StorageClient { + return createStorageClient({ name: BUCKETS.PRESI }); +} diff --git a/packages/shared-storage/src/index.ts b/packages/shared-storage/src/index.ts new file mode 100644 index 000000000..89c207fc0 --- /dev/null +++ b/packages/shared-storage/src/index.ts @@ -0,0 +1,38 @@ +// Main client +export { StorageClient } from './client.js'; + +// Factory functions +export { + createStorageClient, + getStorageConfig, + createPictureStorage, + createChatStorage, + createManaDeckStorage, + createNutriPhiStorage, + createPresiStorage, +} from './factory.js'; + +// Utilities +export { + generateFileKey, + generateUserFileKey, + getContentType, + validateFileSize, + validateFileExtension, + IMAGE_EXTENSIONS, + DOCUMENT_EXTENSIONS, + AUDIO_EXTENSIONS, + VIDEO_EXTENSIONS, +} from './utils.js'; + +// Types +export { + BUCKETS, + type StorageConfig, + type BucketConfig, + type BucketName, + type UploadOptions, + type PresignedUrlOptions, + type UploadResult, + type FileInfo, +} from './types.js'; diff --git a/packages/shared-storage/src/types.ts b/packages/shared-storage/src/types.ts new file mode 100644 index 000000000..96bae0884 --- /dev/null +++ b/packages/shared-storage/src/types.ts @@ -0,0 +1,86 @@ +/** + * Storage configuration for S3-compatible services + */ +export interface StorageConfig { + /** S3 endpoint URL (e.g., http://localhost:9000 for MinIO) */ + endpoint: string; + /** S3 region (e.g., 'us-east-1' or 'fsn1' for Hetzner) */ + region: string; + /** Access key ID */ + accessKeyId: string; + /** Secret access key */ + secretAccessKey: string; + /** Force path-style URLs (required for MinIO) */ + forcePathStyle?: boolean; +} + +/** + * Bucket configuration for a specific project + */ +export interface BucketConfig { + /** Bucket name */ + name: string; + /** Public URL for accessing files (optional, for CDN/public buckets) */ + publicUrl?: string; +} + +/** + * Options for uploading files + */ +export interface UploadOptions { + /** Content type (MIME type) */ + contentType?: string; + /** Cache control header */ + cacheControl?: string; + /** Custom metadata */ + metadata?: Record; + /** Make the object publicly readable */ + public?: boolean; +} + +/** + * Options for generating presigned URLs + */ +export interface PresignedUrlOptions { + /** URL expiration in seconds (default: 3600 = 1 hour) */ + expiresIn?: number; +} + +/** + * Result of a file upload + */ +export interface UploadResult { + /** The key/path of the uploaded file */ + key: string; + /** Public URL if available */ + url?: string; + /** ETag of the uploaded file */ + etag?: string; +} + +/** + * File info from listing + */ +export interface FileInfo { + /** File key/path */ + key: string; + /** File size in bytes */ + size: number; + /** Last modified date */ + lastModified: Date; + /** ETag */ + etag?: string; +} + +/** + * Predefined bucket names for each project + */ +export const BUCKETS = { + PICTURE: 'picture-images', + CHAT: 'chat-files', + MANADECK: 'manadeck-assets', + NUTRIPHI: 'nutriphi-meals', + PRESI: 'presi-slides', +} as const; + +export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS]; diff --git a/packages/shared-storage/src/utils.ts b/packages/shared-storage/src/utils.ts new file mode 100644 index 000000000..6a8612a4f --- /dev/null +++ b/packages/shared-storage/src/utils.ts @@ -0,0 +1,135 @@ +import { randomUUID } from 'crypto'; +import { extname } from 'path'; + +/** + * Generate a unique file key with optional folder structure + * + * @example + * generateFileKey('image.png', 'user-123') + * // => 'user-123/a1b2c3d4-e5f6-7890-abcd-ef1234567890.png' + * + * generateFileKey('photo.jpg', 'users', 'avatars') + * // => 'users/avatars/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg' + */ +export function generateFileKey(filename: string, ...folders: string[]): string { + const ext = extname(filename); + const uuid = randomUUID(); + const key = `${uuid}${ext}`; + + if (folders.length > 0) { + return [...folders, key].join('/'); + } + + return key; +} + +/** + * Generate a user-scoped file key + * + * @example + * generateUserFileKey('user-123', 'avatar.png') + * // => 'users/user-123/a1b2c3d4-e5f6-7890-abcd-ef1234567890.png' + */ +export function generateUserFileKey(userId: string, filename: string, subfolder?: string): string { + const folders = subfolder ? ['users', userId, subfolder] : ['users', userId]; + return generateFileKey(filename, ...folders); +} + +/** + * Get content type from filename extension + */ +export function getContentType(filename: string): string { + const ext = extname(filename).toLowerCase(); + + const mimeTypes: Record = { + // Images + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.avif': 'image/avif', + + // Documents + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + + // Text + '.txt': 'text/plain', + '.csv': 'text/csv', + '.json': 'application/json', + '.xml': 'application/xml', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + + // Audio + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.ogg': 'audio/ogg', + '.m4a': 'audio/mp4', + + // Video + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.mov': 'video/quicktime', + '.avi': 'video/x-msvideo', + + // Archives + '.zip': 'application/zip', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + '.rar': 'application/vnd.rar', + + // Other + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.otf': 'font/otf', + }; + + return mimeTypes[ext] ?? 'application/octet-stream'; +} + +/** + * Validate file size + */ +export function validateFileSize(sizeInBytes: number, maxSizeMB: number): boolean { + const maxSizeBytes = maxSizeMB * 1024 * 1024; + return sizeInBytes <= maxSizeBytes; +} + +/** + * Validate file extension + */ +export function validateFileExtension(filename: string, allowedExtensions: string[]): boolean { + const ext = extname(filename).toLowerCase(); + return allowedExtensions.includes(ext); +} + +/** + * Common allowed extensions for images + */ +export const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']; + +/** + * Common allowed extensions for documents + */ +export const DOCUMENT_EXTENSIONS = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']; + +/** + * Common allowed extensions for audio + */ +export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a']; + +/** + * Common allowed extensions for video + */ +export const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi']; diff --git a/packages/shared-storage/tsconfig.json b/packages/shared-storage/tsconfig.json new file mode 100644 index 000000000..c0db43203 --- /dev/null +++ b/packages/shared-storage/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 466eb1977..1e98ff08d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@manacore/shared-errors': specifier: workspace:* version: link:../../../../packages/shared-errors + '@manacore/shared-nestjs-auth': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-auth '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -79,7 +82,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -112,7 +115,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -139,14 +142,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.0.0 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.0 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -1587,9 +1590,6 @@ importers: apps/presi: devDependencies: - turbo: - specifier: ^2.3.0 - version: 2.6.1 typescript: specifier: ^5.7.2 version: 5.9.3 @@ -1866,9 +1866,6 @@ importers: apps/zitare: devDependencies: - turbo: - specifier: ^2.3.0 - version: 2.6.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2404,7 +2401,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -2437,7 +2434,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -2816,6 +2813,22 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/shared-storage: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.700.0 + version: 3.940.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.700.0 + version: 3.940.0 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/shared-subscription-types: devDependencies: typescript: @@ -3379,6 +3392,10 @@ packages: resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} engines: {node: '>=18.0.0'} + '@aws-sdk/s3-request-presigner@3.940.0': + resolution: {integrity: sha512-TgTUDM2H7revReDfkVwVtIqxV3K0cJLdyuLDIkefVHRUNKwU1Vd5FB2TaFrs6STO0kx5pTckDCOLh0iy7nW5WQ==} + 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'} @@ -3399,6 +3416,10 @@ packages: resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} engines: {node: '>=18.0.0'} + '@aws-sdk/util-format-url@3.936.0': + resolution: {integrity: sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g==} + engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.893.0': resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} engines: {node: '>=18.0.0'} @@ -5115,7 +5136,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -17774,6 +17795,16 @@ snapshots: transitivePeerDependencies: - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + dependencies: + astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + autoprefixer: 10.4.22(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -18214,6 +18245,17 @@ snapshots: '@smithy/types': 4.9.0 tslib: 2.8.1 + '@aws-sdk/s3-request-presigner@3.940.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.940.0 + '@aws-sdk/types': 3.936.0 + '@aws-sdk/util-format-url': 3.936.0 + '@smithy/middleware-endpoint': 4.3.13 + '@smithy/protocol-http': 5.3.5 + '@smithy/smithy-client': 4.9.9 + '@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 @@ -18252,6 +18294,13 @@ snapshots: '@smithy/util-endpoints': 3.2.5 tslib: 2.8.1 + '@aws-sdk/util-format-url@3.936.0': + dependencies: + '@aws-sdk/types': 3.936.0 + '@smithy/querystring-builder': 4.2.5 + '@smithy/types': 4.9.0 + tslib: 2.8.1 + '@aws-sdk/util-locate-window@3.893.0': dependencies: tslib: 2.8.1 @@ -20156,7 +20205,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq) + expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -20233,7 +20282,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4) + expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -21543,6 +21592,43 @@ snapshots: - supports-color - ts-node + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -24850,17 +24936,17 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 pretty-format: 30.2.0 react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) optional: true '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': @@ -26535,6 +26621,108 @@ snapshots: - uploadthing - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + '@astrojs/compiler': 2.13.0 + '@astrojs/internal-helpers': 0.7.5 + '@astrojs/markdown-remark': 6.3.9 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 3.0.1 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.3.1 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.1.0 + cssesc: 3.0.0 + debug: 4.4.3 + deterministic-object-hash: 2.0.2 + devalue: 5.5.0 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.12 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.3.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.1 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.5.0 + piccolore: 0.1.3 + picomatch: 4.0.3 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.3 + shiki: 3.15.0 + smol-toml: 1.5.2 + svgo: 4.0.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.6.0 + unist-util-visit: 5.0.0 + unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.8.2) + vfile: 6.0.3 + vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -28510,9 +28698,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -28527,9 +28715,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -28599,7 +28787,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -28610,7 +28798,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -28634,25 +28837,25 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -28752,7 +28955,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -28763,7 +28966,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -28781,7 +28984,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -28792,7 +28995,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -29933,21 +30136,21 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq): + expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -29955,10 +30158,10 @@ snapshots: query-string: 7.1.3 react: 19.1.0 react-fast-compare: 3.2.2 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) semver: 7.6.3 server-only: 0.0.1 sf-symbols-typescript: 2.1.0 @@ -29966,13 +30169,13 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@types/react' @@ -32020,15 +32223,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -32191,7 +32394,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -32218,8 +32421,9 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.19.25 - esbuild-register: 3.6.0(esbuild@0.27.0) + '@types/node': 22.19.1 + esbuild-register: 3.6.0(esbuild@0.19.12) + ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -32880,12 +33084,12 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36510,6 +36714,16 @@ snapshots: webpack: 5.100.2(esbuild@0.27.0) webpack-sources: 3.3.3 + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.97.1(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -37734,17 +37948,6 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.100.2(esbuild@0.19.12) - optionalDependencies: - esbuild: 0.19.12 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -37985,16 +38188,6 @@ snapshots: babel-jest: 30.2.0(@babel/core@7.28.5) jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.100.2(esbuild@0.19.12) - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: chalk: 4.1.2 @@ -38015,6 +38208,16 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.97.1(esbuild@0.19.12) + ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -38608,6 +38811,23 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -38685,6 +38905,10 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -38965,38 +39189,6 @@ snapshots: - esbuild - uglify-js - webpack@5.100.2(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.100.2(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7