From 8cd5021b50156280d1a7edc81a56ad88a7380d01 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:55:05 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(mana-core-auth):=20use=20BAS?= =?UTF-8?q?E=5FURL=20as=20JWT=20issuer=20for=20OIDC=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OIDC providers like Synapse expect the JWT issuer claim to match the discovery document's issuer URL. Changed JWT plugin config from JWT_ISSUER to BASE_URL to ensure consistency. Also adds: - @manacore/credit-operations package with operation definitions - @manacore/shared-credit-ui package with React Native and Svelte components - CreditInterceptor and @UseCredits decorator in nestjs-integration - Credit system integration in chat backend --- .../mana-core-auth-production-readiness.md | 51 +- .github/workflows/ci.yml | 20 + apps/chat/apps/backend/package.json | 2 + apps/chat/apps/backend/src/app.module.ts | 12 +- .../apps/backend/src/chat/chat.service.ts | 100 ++- packages/credit-operations/package.json | 27 + packages/credit-operations/src/index.ts | 580 ++++++++++++++ packages/credit-operations/tsconfig.json | 28 + .../mana-core-nestjs-integration/package.json | 9 +- .../src/decorators/index.ts | 1 + .../src/decorators/use-credits.decorator.ts | 97 +++ .../mana-core-nestjs-integration/src/index.ts | 23 + .../src/interceptors/credit.interceptor.ts | 195 +++++ .../src/interceptors/index.ts | 1 + packages/shared-credit-ui/package.json | 51 ++ .../src/mobile/CreditBalance.tsx | 217 ++++++ .../src/mobile/CreditToast.tsx | 253 +++++++ packages/shared-credit-ui/src/mobile/index.ts | 19 + .../src/web/CreditBalance.svelte | 339 +++++++++ .../src/web/CreditPricingTable.svelte | 324 ++++++++ .../src/web/CreditToast.svelte | 257 +++++++ packages/shared-credit-ui/src/web/index.ts | 20 + packages/shared-credit-ui/tsconfig.json | 22 + pnpm-lock.yaml | 710 +++++++++++------- services/mana-core-auth/package.json | 3 +- .../src/auth/auth.controller.spec.ts | 4 + .../src/auth/better-auth.config.ts | 4 +- services/mana-core-auth/src/main.ts | 58 ++ .../mana-core-auth/test/__mocks__/jose.ts | 253 ++++++- 29 files changed, 3351 insertions(+), 329 deletions(-) create mode 100644 packages/credit-operations/package.json create mode 100644 packages/credit-operations/src/index.ts create mode 100644 packages/credit-operations/tsconfig.json create mode 100644 packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts create mode 100644 packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts create mode 100644 packages/mana-core-nestjs-integration/src/interceptors/index.ts create mode 100644 packages/shared-credit-ui/package.json create mode 100644 packages/shared-credit-ui/src/mobile/CreditBalance.tsx create mode 100644 packages/shared-credit-ui/src/mobile/CreditToast.tsx create mode 100644 packages/shared-credit-ui/src/mobile/index.ts create mode 100644 packages/shared-credit-ui/src/web/CreditBalance.svelte create mode 100644 packages/shared-credit-ui/src/web/CreditPricingTable.svelte create mode 100644 packages/shared-credit-ui/src/web/CreditToast.svelte create mode 100644 packages/shared-credit-ui/src/web/index.ts create mode 100644 packages/shared-credit-ui/tsconfig.json diff --git a/.claude/plans/mana-core-auth-production-readiness.md b/.claude/plans/mana-core-auth-production-readiness.md index 5501d99ba..f8096fcce 100644 --- a/.claude/plans/mana-core-auth-production-readiness.md +++ b/.claude/plans/mana-core-auth-production-readiness.md @@ -197,35 +197,44 @@ Dieses Dokument beschreibt alle Änderungen, die vor dem Go-Live des `mana-core- - Alle DTOs ### 3.3 Docker Optimierung -- **Status**: [ ] Offen +- **Status**: [x] Erledigt (2026-02-01) - **Priorität**: 🟡 Mittel - **Problem**: Source code kopiert, tsx in prod, kein .dockerignore - **Lösung**: - - `.dockerignore` erstellen - - `tsx` aus Production entfernen - - Source code nicht kopieren -- **Dateien**: - - `.dockerignore` - - `Dockerfile` + - ✅ `.dockerignore` erstellt (node_modules, tests, docs, IDE files) + - ✅ `tsx` aus Production entfernt (Migrations laufen extern) + - ✅ Source code wird nicht mehr kopiert (nur dist/) + - ✅ Multi-stage Build optimiert +- **Geänderte Dateien**: + - `.dockerignore` - NEU + - `Dockerfile` - Optimiert ### 3.4 Dependency Cleanup -- **Status**: [ ] Offen +- **Status**: [x] Erledigt (2026-02-01) - **Priorität**: 🟡 Mittel -- **Problem**: `jsonwebtoken` UND `jose` (nur jose nötig) +- **Problem**: `jsonwebtoken` UND `jose` (nur jose nötig laut CLAUDE.md) - **Lösung**: - - `jsonwebtoken` entfernen - - Alle Imports prüfen -- **Datei**: `package.json` + - ✅ `jsonwebtoken` aus dependencies entfernt + - ✅ `@types/jsonwebtoken` aus devDependencies entfernt + - ✅ RS256 Fallback aus `better-auth.service.ts` entfernt + - ✅ JWT-Tests auf `jose` Mock umgestellt + - ✅ jose Mock implementiert für Jest (ESM-Kompatibilität) +- **Geänderte Dateien**: + - `package.json` - jsonwebtoken entfernt + - `src/auth/services/better-auth.service.ts` - Fallback entfernt + - `src/auth/jwt-validation.spec.ts` - jose statt jsonwebtoken + - `test/__mocks__/jose.ts` - NEU (HS256 Mock für Tests) ### 3.5 Security Scanning in CI/CD -- **Status**: [ ] Offen +- **Status**: [x] Erledigt (2026-02-01) - **Priorität**: 🟡 Mittel - **Problem**: Keine automatische Security-Prüfung - **Lösung**: - - `npm audit` in CI - - Dependabot aktivieren - - SAST Tools (optional) -- **Datei**: `.github/workflows/ci.yml` + - ✅ `pnpm audit --audit-level=high` in CI (validate job) + - ✅ Dependabot bereits konfiguriert (npm, github-actions, docker) + - ✅ Warnung für bekannte vulnerable Pakete (lodash, axios) +- **Geänderte Datei**: `.github/workflows/ci.yml` +- **Existierende Datei**: `.github/dependabot.yml` (war bereits vorhanden) --- @@ -235,10 +244,11 @@ Dieses Dokument beschreibt alle Änderungen, die vor dem Go-Live des `mana-core- |-------|----------|----------|-------------| | Phase 1 | 5 | 5 | 100% | | Phase 2 | 6 | 5 | 83% | -| Phase 3 | 5 | 0 | 0% | -| **Gesamt** | **16** | **10** | **63%** | +| Phase 3 | 5 | 3 | 60% | +| **Gesamt** | **16** | **13** | **81%** | **Hinweis:** Phase 2.3 (Grafana Dashboard) ist als separates Task für später markiert. +**Offen:** 3.1 (E2E Tests), 3.2 (OpenAPI/Swagger), 2.3 (Grafana Dashboard) --- @@ -257,4 +267,7 @@ Dieses Dokument beschreibt alle Änderungen, die vor dem Go-Live des `mana-core- | 2026-02-01 | 2.4 Disaster Recovery Dokumentation erstellt | | 2026-02-01 | 2.5 Error Tracking: Winston JSON-Logs für Loki/Grafana vorbereitet | | 2026-02-01 | 2.6 Stripe Validierung: Warnung in env.validation.ts (Teil von 1.2) | +| 2026-02-01 | 3.3 Docker Optimierung: .dockerignore, tsx entfernt, nur dist/ kopiert | +| 2026-02-01 | 3.4 Dependency Cleanup: jsonwebtoken entfernt, jose Mock für Tests | +| 2026-02-01 | 3.5 Security Scanning: pnpm audit in CI, Dependabot war bereits aktiv | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 657290aaf..7d0186988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -558,6 +558,26 @@ jobs: - name: Lint run: pnpm run lint || echo "Lint warnings found" + - name: Security Audit + run: | + # Run pnpm audit and capture exit code + # Exit 0 if only moderate/low vulnerabilities + pnpm audit --audit-level=high || { + echo "::warning::Security vulnerabilities found. Run 'pnpm audit' locally for details." + exit 0 # Don't fail build on audit issues (just warn) + } + + - name: Check for known vulnerable packages + run: | + # Check for packages with known critical vulnerabilities + # This is a basic check - for production, consider Snyk or similar + if grep -r "lodash@[0-3]\." pnpm-lock.yaml 2>/dev/null; then + echo "::warning::Potentially vulnerable lodash version detected" + fi + if grep -r "axios@0\.[0-9]\." pnpm-lock.yaml 2>/dev/null; then + echo "::warning::Potentially vulnerable axios version detected" + fi + # =========================================== # Build Docker images - only changed services # =========================================== diff --git a/apps/chat/apps/backend/package.json b/apps/chat/apps/backend/package.json index 49f679c65..f9ca6db83 100644 --- a/apps/chat/apps/backend/package.json +++ b/apps/chat/apps/backend/package.json @@ -26,6 +26,8 @@ "docker:clean": "docker compose down -v --rmi local" }, "dependencies": { + "@manacore/credit-operations": "workspace:*", + "@manacore/nestjs-integration": "workspace:*", "@manacore/shared-errors": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", "@manacore/shared-nestjs-health": "workspace:*", diff --git a/apps/chat/apps/backend/src/app.module.ts b/apps/chat/apps/backend/src/app.module.ts index 8a77660fd..d9aa1f777 100644 --- a/apps/chat/apps/backend/src/app.module.ts +++ b/apps/chat/apps/backend/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { MetricsModule } from '@manacore/shared-nestjs-metrics'; +import { ManaCoreModule } from '@manacore/nestjs-integration'; import { DatabaseModule } from './db/database.module'; import { ChatModule } from './chat/chat.module'; import { ConversationModule } from './conversation/conversation.module'; @@ -16,6 +17,15 @@ import { HealthModule } from '@manacore/shared-nestjs-health'; isGlobal: true, envFilePath: '.env', }), + ManaCoreModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + appId: configService.get('APP_ID', 'chat'), + serviceKey: configService.get('MANA_CORE_SERVICE_KEY', ''), + debug: configService.get('NODE_ENV') === 'development', + }), + inject: [ConfigService], + }), MetricsModule.register({ prefix: 'chat_', excludePaths: ['/health'], diff --git a/apps/chat/apps/backend/src/chat/chat.service.ts b/apps/chat/apps/backend/src/chat/chat.service.ts index db5075de4..b5f4be344 100644 --- a/apps/chat/apps/backend/src/chat/chat.service.ts +++ b/apps/chat/apps/backend/src/chat/chat.service.ts @@ -2,6 +2,12 @@ import { Injectable, Inject, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { eq } from 'drizzle-orm'; import { AsyncResult, ok, err, ValidationError, ServiceError } from '@manacore/shared-errors'; +import { + CreditClientService, + InsufficientCreditsException, + CreditOperationType, + CREDIT_COSTS, +} from '@manacore/nestjs-integration'; import OpenAI from 'openai'; import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; @@ -20,7 +26,8 @@ export class ChatService { constructor( private configService: ConfigService, @Inject(DATABASE_CONNECTION) private readonly db: Database, - private readonly ollamaService: OllamaService + private readonly ollamaService: OllamaService, + private readonly creditClient: CreditClientService ) { // OpenRouter setup (cloud provider) const openRouterApiKey = this.configService.get('OPENROUTER_API_KEY'); @@ -69,21 +76,106 @@ export class ChatService { return err(ValidationError.invalidInput('modelId', `Model ${dto.modelId} not found`)); } + // Determine credit operation type and cost based on model + const creditOperation = this.getCreditOperationForModel(model); + const creditCost = CREDIT_COSTS[creditOperation]; + // Log user context for tracking (optional) if (userId) { this.logger.log( - `User ${userId} creating chat completion with model ${dto.modelId} (${model.provider})` + `User ${userId} creating chat completion with model ${dto.modelId} (${model.provider}) - cost: ${creditCost} credits` ); + + // Check if user has enough credits (skip in development if no service key) + const validation = await this.creditClient.validateCredits( + userId, + creditOperation, + creditCost + ); + if (!validation.hasCredits) { + throw new InsufficientCreditsException({ + requiredCredits: creditCost, + availableCredits: validation.availableCredits, + creditType: 'user', + operation: creditOperation, + }); + } } // Route to appropriate provider based on model configuration + let result; switch (model.provider) { case 'ollama': - return this.createOllamaCompletion(model, dto); + result = await this.createOllamaCompletion(model, dto); + break; case 'openrouter': default: - return this.createOpenRouterCompletion(model, dto); + result = await this.createOpenRouterCompletion(model, dto); + break; } + + // Consume credits after successful completion + if (result.ok && userId) { + const modelName = this.getModelDisplayName(model); + const consumed = await this.creditClient.consumeCredits( + userId, + creditOperation, + creditCost, + `Chat with ${modelName}`, + { + modelId: dto.modelId, + provider: model.provider, + tokens: result.value.usage?.total_tokens || 0, + } + ); + + if (!consumed) { + this.logger.warn(`Failed to consume credits for user ${userId}`); + } else { + this.logger.debug(`Consumed ${creditCost} credits for user ${userId}`); + } + } + + return result; + } + + /** + * Determine the credit operation type based on the model. + */ + private getCreditOperationForModel(model: Model): CreditOperationType { + const params = model.parameters as { model?: string } | null; + const modelName = params?.model?.toLowerCase() || ''; + + // Local Ollama models - cheapest + if (model.provider === 'ollama') { + return CreditOperationType.AI_CHAT_OLLAMA; + } + + // Cloud models - price based on model family + if (modelName.includes('gpt-4') || modelName.includes('gpt4')) { + return CreditOperationType.AI_CHAT_GPT4; + } + if (modelName.includes('claude')) { + return CreditOperationType.AI_CHAT_CLAUDE; + } + if (modelName.includes('gemini')) { + return CreditOperationType.AI_CHAT_GEMINI; + } + if (modelName.includes('qwen')) { + return CreditOperationType.AI_CHAT_QWEN; + } + + // Default to Gemini pricing for other cloud models + return CreditOperationType.AI_CHAT_GEMINI; + } + + /** + * Get a display name for the model. + */ + private getModelDisplayName(model: Model): string { + if (model.name) return model.name; + const params = model.parameters as { model?: string } | null; + return params?.model || model.provider; } private async createOllamaCompletion( diff --git a/packages/credit-operations/package.json b/packages/credit-operations/package.json new file mode 100644 index 000000000..724352ac2 --- /dev/null +++ b/packages/credit-operations/package.json @@ -0,0 +1,27 @@ +{ + "name": "@manacore/credit-operations", + "version": "1.0.0", + "private": true, + "description": "Central credit operation definitions and costs for all Mana apps", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/credit-operations/src/index.ts b/packages/credit-operations/src/index.ts new file mode 100644 index 000000000..e603a724f --- /dev/null +++ b/packages/credit-operations/src/index.ts @@ -0,0 +1,580 @@ +/** + * @manacore/credit-operations + * + * Central credit operation definitions for all Mana apps. + * This package defines operation types, costs, and helper functions + * for the unified credit system across the ecosystem. + */ + +// ============================================================================ +// Operation Types +// ============================================================================ + +/** + * All credit operations across the Mana ecosystem. + * Operations are categorized by type: AI, productivity (micro), and premium. + */ +export enum CreditOperationType { + // ------------------------------------------------------------------------- + // AI Operations (Standard Credits: 1-30) + // ------------------------------------------------------------------------- + + // Chat - AI conversations + AI_CHAT_GPT4 = 'ai_chat_gpt4', + AI_CHAT_CLAUDE = 'ai_chat_claude', + AI_CHAT_GEMINI = 'ai_chat_gemini', + AI_CHAT_QWEN = 'ai_chat_qwen', + AI_CHAT_OLLAMA = 'ai_chat_ollama', + + // Picture - Image generation + AI_IMAGE_GENERATION = 'ai_image_generation', + AI_IMAGE_UPSCALE = 'ai_image_upscale', + + // Questions - Research + AI_RESEARCH_QUICK = 'ai_research_quick', + AI_RESEARCH_DEEP = 'ai_research_deep', + + // NutriPhi - Food analysis + AI_FOOD_ANALYSIS = 'ai_food_analysis', + + // ManaDeck - AI deck generation + AI_DECK_GENERATION = 'ai_deck_generation', + AI_CARD_GENERATION = 'ai_card_generation', + + // Zitare - AI explanations + AI_QUOTE_EXPLANATION = 'ai_quote_explanation', + + // General AI features + AI_SMART_SCHEDULING = 'ai_smart_scheduling', + AI_SUGGESTIONS = 'ai_suggestions', + AI_ENRICHMENT = 'ai_enrichment', + + // ------------------------------------------------------------------------- + // Productivity Operations (Micro Credits: 0.01-0.10) + // ------------------------------------------------------------------------- + + // Todo + TASK_CREATE = 'task_create', + PROJECT_CREATE = 'project_create', + + // Calendar + EVENT_CREATE = 'event_create', + CALENDAR_CREATE = 'calendar_create', + + // Contacts + CONTACT_CREATE = 'contact_create', + + // Zitare + COLLECTION_CREATE = 'collection_create', + + // Presi + PRESENTATION_CREATE = 'presentation_create', + SLIDE_CREATE = 'slide_create', + + // ------------------------------------------------------------------------- + // Premium Features (Standard Credits: 0.5-5) + // ------------------------------------------------------------------------- + + // Sync features + CALDAV_SYNC = 'caldav_sync', + GOOGLE_SYNC = 'google_sync', + CLOUD_SYNC = 'cloud_sync', + + // Import/Export + BULK_IMPORT = 'bulk_import', + PDF_EXPORT = 'pdf_export', + + // Premium themes + PREMIUM_THEME = 'premium_theme', +} + +// ============================================================================ +// Credit Costs +// ============================================================================ + +/** + * Credit costs for each operation type. + * Costs are in Credits (decimal values supported for micro-credits). + */ +export const CREDIT_COSTS: Record = { + // AI Operations (Standard Credits) + [CreditOperationType.AI_CHAT_GPT4]: 5, + [CreditOperationType.AI_CHAT_CLAUDE]: 5, + [CreditOperationType.AI_CHAT_GEMINI]: 2, + [CreditOperationType.AI_CHAT_QWEN]: 2, + [CreditOperationType.AI_CHAT_OLLAMA]: 0.1, + + [CreditOperationType.AI_IMAGE_GENERATION]: 10, + [CreditOperationType.AI_IMAGE_UPSCALE]: 5, + + [CreditOperationType.AI_RESEARCH_QUICK]: 5, + [CreditOperationType.AI_RESEARCH_DEEP]: 25, + + [CreditOperationType.AI_FOOD_ANALYSIS]: 3, + + [CreditOperationType.AI_DECK_GENERATION]: 20, + [CreditOperationType.AI_CARD_GENERATION]: 2, + + [CreditOperationType.AI_QUOTE_EXPLANATION]: 2, + + [CreditOperationType.AI_SMART_SCHEDULING]: 2, + [CreditOperationType.AI_SUGGESTIONS]: 2, + [CreditOperationType.AI_ENRICHMENT]: 2, + + // Productivity Operations (Micro Credits) + [CreditOperationType.TASK_CREATE]: 0.02, + [CreditOperationType.PROJECT_CREATE]: 0.1, + + [CreditOperationType.EVENT_CREATE]: 0.02, + [CreditOperationType.CALENDAR_CREATE]: 0.1, + + [CreditOperationType.CONTACT_CREATE]: 0.02, + + [CreditOperationType.COLLECTION_CREATE]: 0.1, + + [CreditOperationType.PRESENTATION_CREATE]: 0.5, + [CreditOperationType.SLIDE_CREATE]: 0.02, + + // Premium Features + [CreditOperationType.CALDAV_SYNC]: 0.5, + [CreditOperationType.GOOGLE_SYNC]: 0.5, + [CreditOperationType.CLOUD_SYNC]: 5, // Monthly + + [CreditOperationType.BULK_IMPORT]: 0.2, // Per 10 items + [CreditOperationType.PDF_EXPORT]: 1, + + [CreditOperationType.PREMIUM_THEME]: 3, +}; + +// ============================================================================ +// Operation Metadata +// ============================================================================ + +/** + * Category of credit operation for grouping and display. + */ +export enum CreditCategory { + AI = 'ai', + PRODUCTIVITY = 'productivity', + PREMIUM = 'premium', +} + +/** + * Metadata about each operation for UI display and documentation. + */ +export interface OperationMetadata { + /** Human-readable name */ + name: string; + /** Description for tooltips/help */ + description: string; + /** Category for grouping */ + category: CreditCategory; + /** Which app this operation belongs to */ + app: string; + /** Is this a per-item cost (e.g., bulk import per 10 items) */ + perItem?: boolean; + /** Item unit name if perItem is true */ + itemUnit?: string; +} + +/** + * Metadata for all operations. + */ +export const OPERATION_METADATA: Record = { + // AI Chat + [CreditOperationType.AI_CHAT_GPT4]: { + name: 'GPT-4 Message', + description: 'Send a message using GPT-4 or GPT-4o', + category: CreditCategory.AI, + app: 'chat', + }, + [CreditOperationType.AI_CHAT_CLAUDE]: { + name: 'Claude Message', + description: 'Send a message using Claude (Anthropic)', + category: CreditCategory.AI, + app: 'chat', + }, + [CreditOperationType.AI_CHAT_GEMINI]: { + name: 'Gemini Message', + description: 'Send a message using Google Gemini', + category: CreditCategory.AI, + app: 'chat', + }, + [CreditOperationType.AI_CHAT_QWEN]: { + name: 'Qwen Message', + description: 'Send a message using Qwen', + category: CreditCategory.AI, + app: 'chat', + }, + [CreditOperationType.AI_CHAT_OLLAMA]: { + name: 'Ollama Message (Local)', + description: 'Send a message using local Ollama models', + category: CreditCategory.AI, + app: 'chat', + }, + + // Image Generation + [CreditOperationType.AI_IMAGE_GENERATION]: { + name: 'Generate Image', + description: 'Generate an AI image', + category: CreditCategory.AI, + app: 'picture', + }, + [CreditOperationType.AI_IMAGE_UPSCALE]: { + name: 'Upscale Image', + description: 'Upscale an image to higher resolution', + category: CreditCategory.AI, + app: 'picture', + }, + + // Research + [CreditOperationType.AI_RESEARCH_QUICK]: { + name: 'Quick Research', + description: 'Quick research with 5 sources', + category: CreditCategory.AI, + app: 'questions', + }, + [CreditOperationType.AI_RESEARCH_DEEP]: { + name: 'Deep Research', + description: 'Comprehensive research with 30+ sources', + category: CreditCategory.AI, + app: 'questions', + }, + + // Food Analysis + [CreditOperationType.AI_FOOD_ANALYSIS]: { + name: 'Analyze Food Photo', + description: 'Analyze nutrition from a food photo', + category: CreditCategory.AI, + app: 'nutriphi', + }, + + // Deck Generation + [CreditOperationType.AI_DECK_GENERATION]: { + name: 'Generate AI Deck', + description: 'Generate a complete deck with AI (10 cards)', + category: CreditCategory.AI, + app: 'manadeck', + }, + [CreditOperationType.AI_CARD_GENERATION]: { + name: 'Generate AI Card', + description: 'Generate a single card with AI', + category: CreditCategory.AI, + app: 'manadeck', + }, + + // Quote Explanation + [CreditOperationType.AI_QUOTE_EXPLANATION]: { + name: 'Explain Quote', + description: 'Get an AI explanation of a quote', + category: CreditCategory.AI, + app: 'zitare', + }, + + // General AI + [CreditOperationType.AI_SMART_SCHEDULING]: { + name: 'Smart Scheduling', + description: 'AI-powered task scheduling suggestions', + category: CreditCategory.AI, + app: 'todo', + }, + [CreditOperationType.AI_SUGGESTIONS]: { + name: 'AI Suggestions', + description: 'Get AI-powered suggestions', + category: CreditCategory.AI, + app: 'general', + }, + [CreditOperationType.AI_ENRICHMENT]: { + name: 'AI Enrichment', + description: 'Enrich data with AI-gathered information', + category: CreditCategory.AI, + app: 'contacts', + }, + + // Productivity - Todo + [CreditOperationType.TASK_CREATE]: { + name: 'Create Task', + description: 'Create a new task', + category: CreditCategory.PRODUCTIVITY, + app: 'todo', + }, + [CreditOperationType.PROJECT_CREATE]: { + name: 'Create Project', + description: 'Create a new project', + category: CreditCategory.PRODUCTIVITY, + app: 'todo', + }, + + // Productivity - Calendar + [CreditOperationType.EVENT_CREATE]: { + name: 'Create Event', + description: 'Create a calendar event', + category: CreditCategory.PRODUCTIVITY, + app: 'calendar', + }, + [CreditOperationType.CALENDAR_CREATE]: { + name: 'Create Calendar', + description: 'Create a new calendar', + category: CreditCategory.PRODUCTIVITY, + app: 'calendar', + }, + + // Productivity - Contacts + [CreditOperationType.CONTACT_CREATE]: { + name: 'Create Contact', + description: 'Create a new contact', + category: CreditCategory.PRODUCTIVITY, + app: 'contacts', + }, + + // Productivity - Zitare + [CreditOperationType.COLLECTION_CREATE]: { + name: 'Create Collection', + description: 'Create a quote collection', + category: CreditCategory.PRODUCTIVITY, + app: 'zitare', + }, + + // Productivity - Presi + [CreditOperationType.PRESENTATION_CREATE]: { + name: 'Create Presentation', + description: 'Create a new presentation', + category: CreditCategory.PRODUCTIVITY, + app: 'presi', + }, + [CreditOperationType.SLIDE_CREATE]: { + name: 'Create Slide', + description: 'Add a slide to a presentation', + category: CreditCategory.PRODUCTIVITY, + app: 'presi', + }, + + // Premium - Sync + [CreditOperationType.CALDAV_SYNC]: { + name: 'CalDAV Sync', + description: 'Sync with CalDAV server', + category: CreditCategory.PREMIUM, + app: 'calendar', + }, + [CreditOperationType.GOOGLE_SYNC]: { + name: 'Google Sync', + description: 'Sync with Google services', + category: CreditCategory.PREMIUM, + app: 'contacts', + }, + [CreditOperationType.CLOUD_SYNC]: { + name: 'Cloud Sync (Monthly)', + description: 'Enable cloud synchronization', + category: CreditCategory.PREMIUM, + app: 'skilltree', + }, + + // Premium - Import/Export + [CreditOperationType.BULK_IMPORT]: { + name: 'Bulk Import', + description: 'Import items in bulk', + category: CreditCategory.PREMIUM, + app: 'general', + perItem: true, + itemUnit: '10 items', + }, + [CreditOperationType.PDF_EXPORT]: { + name: 'PDF Export', + description: 'Export to PDF format', + category: CreditCategory.PREMIUM, + app: 'presi', + }, + + // Premium - Themes + [CreditOperationType.PREMIUM_THEME]: { + name: 'Premium Theme', + description: 'Use a premium theme', + category: CreditCategory.PREMIUM, + app: 'presi', + }, +}; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Get the credit cost for an operation. + * @param operation The operation type + * @returns The cost in credits + */ +export function getCreditCost(operation: CreditOperationType): number { + return CREDIT_COSTS[operation]; +} + +/** + * Get the metadata for an operation. + * @param operation The operation type + * @returns Operation metadata + */ +export function getOperationMetadata(operation: CreditOperationType): OperationMetadata { + return OPERATION_METADATA[operation]; +} + +/** + * Get all operations for a specific app. + * @param app The app name (e.g., 'chat', 'todo', 'calendar') + * @returns Array of operations for that app + */ +export function getOperationsForApp(app: string): CreditOperationType[] { + return Object.entries(OPERATION_METADATA) + .filter(([, meta]) => meta.app === app) + .map(([op]) => op as CreditOperationType); +} + +/** + * Get all operations in a specific category. + * @param category The category + * @returns Array of operations in that category + */ +export function getOperationsByCategory(category: CreditCategory): CreditOperationType[] { + return Object.entries(OPERATION_METADATA) + .filter(([, meta]) => meta.category === category) + .map(([op]) => op as CreditOperationType); +} + +/** + * Calculate total cost for bulk operations. + * @param operation The operation type + * @param count Number of items + * @returns Total cost in credits + */ +export function calculateBulkCost(operation: CreditOperationType, count: number): number { + const cost = CREDIT_COSTS[operation]; + const meta = OPERATION_METADATA[operation]; + + if (meta.perItem) { + // For bulk operations, cost is per batch (e.g., per 10 items) + return Math.ceil(count / 10) * cost; + } + + return cost * count; +} + +/** + * Check if an operation is considered "free" (no credit cost). + * @param operation The operation type + * @returns True if the operation is free + */ +export function isFreeOperation(operation: CreditOperationType): boolean { + return CREDIT_COSTS[operation] === 0; +} + +/** + * Check if an operation is a micro-credit operation (< 0.5 credits). + * @param operation The operation type + * @returns True if micro-credit operation + */ +export function isMicroCreditOperation(operation: CreditOperationType): boolean { + const cost = CREDIT_COSTS[operation]; + return cost > 0 && cost < 0.5; +} + +/** + * Check if an operation is an AI operation. + * @param operation The operation type + * @returns True if AI operation + */ +export function isAiOperation(operation: CreditOperationType): boolean { + return OPERATION_METADATA[operation].category === CreditCategory.AI; +} + +/** + * Format credit cost for display. + * @param cost The credit cost + * @returns Formatted string (e.g., "0.02" or "5") + */ +export function formatCreditCost(cost: number): string { + if (cost === 0) return 'Free'; + if (cost < 1) return cost.toFixed(2); + return cost.toString(); +} + +/** + * Get a pricing table for an app (for display in UI). + * @param app The app name + * @returns Array of pricing entries + */ +export function getPricingTable(app: string): Array<{ + operation: CreditOperationType; + name: string; + description: string; + cost: number; + formattedCost: string; + category: CreditCategory; +}> { + return getOperationsForApp(app).map((op) => { + const meta = OPERATION_METADATA[op]; + const cost = CREDIT_COSTS[op]; + return { + operation: op, + name: meta.name, + description: meta.description, + cost, + formattedCost: formatCreditCost(cost), + category: meta.category, + }; + }); +} + +// ============================================================================ +// Free Operations List +// ============================================================================ + +/** + * Operations that are always free (no credit cost). + * These are read operations, status checks, and engagement actions. + */ +export const FREE_OPERATIONS = [ + // Reading/viewing + 'read', + 'view', + 'list', + 'get', + 'search', + 'browse', + + // Task completion (engagement) + 'complete', + 'check', + 'toggle', + + // Editing (no new resource creation) + 'update', + 'edit', + 'modify', + + // Deletion + 'delete', + 'remove', + 'archive', + + // Organization + 'sort', + 'filter', + 'move', + 'reorder', + + // Metadata + 'tag', + 'label', + 'favorite', + 'unfavorite', +] as const; + +/** + * Check if an action name represents a free operation. + * @param action The action name (e.g., 'update', 'delete') + * @returns True if the action is free + */ +export function isFreeAction(action: string): boolean { + const normalizedAction = action.toLowerCase(); + return FREE_OPERATIONS.some( + (freeOp) => normalizedAction === freeOp || normalizedAction.startsWith(`${freeOp}_`) + ); +} diff --git a/packages/credit-operations/tsconfig.json b/packages/credit-operations/tsconfig.json new file mode 100644 index 000000000..08822bebd --- /dev/null +++ b/packages/credit-operations/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "node", + "baseUrl": ".", + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mana-core-nestjs-integration/package.json b/packages/mana-core-nestjs-integration/package.json index be65f4425..54a163fb5 100644 --- a/packages/mana-core-nestjs-integration/package.json +++ b/packages/mana-core-nestjs-integration/package.json @@ -20,6 +20,11 @@ "types": "./dist/decorators/index.d.ts", "import": "./dist/decorators/index.js", "require": "./dist/decorators/index.js" + }, + "./interceptors": { + "types": "./dist/interceptors/index.d.ts", + "import": "./dist/interceptors/index.js", + "require": "./dist/interceptors/index.js" } }, "scripts": { @@ -30,10 +35,12 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@manacore/credit-operations": "workspace:*", "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/config": "^3.0.0 || ^4.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", - "reflect-metadata": "^0.1.13 || ^0.2.0" + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.0.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/packages/mana-core-nestjs-integration/src/decorators/index.ts b/packages/mana-core-nestjs-integration/src/decorators/index.ts index 83895afa6..b81e276e6 100644 --- a/packages/mana-core-nestjs-integration/src/decorators/index.ts +++ b/packages/mana-core-nestjs-integration/src/decorators/index.ts @@ -1,2 +1,3 @@ export { CurrentUser, JwtPayload } from './current-user.decorator'; export { Public, IS_PUBLIC_KEY } from './public.decorator'; +export { UseCredits, CreditOperationConfig, CREDIT_OPERATION_KEY } from './use-credits.decorator'; diff --git a/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts b/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts new file mode 100644 index 000000000..0f44ccbe3 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts @@ -0,0 +1,97 @@ +import { SetMetadata, applyDecorators, UseInterceptors } from '@nestjs/common'; +import { CreditInterceptor } from '../interceptors/credit.interceptor'; +import { type CreditOperationType } from '@manacore/credit-operations'; + +/** + * Metadata key for credit operation configuration. + */ +export const CREDIT_OPERATION_KEY = 'credit_operation'; + +/** + * Configuration for credit consumption. + */ +export interface CreditOperationConfig { + /** + * The operation type from the credit-operations package. + */ + operation: CreditOperationType; + + /** + * Custom cost override. If not specified, uses the default from CREDIT_COSTS. + */ + customCost?: number; + + /** + * Whether to consume credits before or after the handler execution. + * - 'before': Validate and reserve credits before execution (default) + * - 'after': Consume credits only after successful execution + */ + consumeMode?: 'before' | 'after'; + + /** + * Optional function to calculate cost dynamically based on request. + * Receives the request object and should return the credit cost. + */ + dynamicCost?: (request: any) => number; + + /** + * Optional function to generate description for the transaction. + * Receives the request object and should return a description string. + */ + descriptionFn?: (request: any) => string; + + /** + * Whether to skip the credit check in development mode. + * Default: false + */ + skipInDev?: boolean; +} + +/** + * Decorator to require credits for an endpoint. + * + * @example Simple usage with operation type: + * ```typescript + * @Post('tasks') + * @UseCredits(CreditOperationType.TASK_CREATE) + * async createTask(@Body() dto: CreateTaskDto) { + * return this.taskService.create(dto); + * } + * ``` + * + * @example With configuration object: + * ```typescript + * @Post('generate') + * @UseCredits({ + * operation: CreditOperationType.AI_IMAGE_GENERATION, + * consumeMode: 'after', + * descriptionFn: (req) => `Generated image: ${req.body.prompt}`, + * }) + * async generateImage(@Body() dto: GenerateDto) { + * return this.imageService.generate(dto); + * } + * ``` + * + * @example With dynamic cost: + * ```typescript + * @Post('bulk-import') + * @UseCredits({ + * operation: CreditOperationType.BULK_IMPORT, + * dynamicCost: (req) => Math.ceil(req.body.items.length / 10) * 0.2, + * }) + * async bulkImport(@Body() dto: BulkImportDto) { + * return this.importService.import(dto); + * } + * ``` + */ +export function UseCredits( + operationOrConfig: CreditOperationType | CreditOperationConfig +): MethodDecorator { + const config: CreditOperationConfig = + typeof operationOrConfig === 'string' ? { operation: operationOrConfig } : operationOrConfig; + + return applyDecorators( + SetMetadata(CREDIT_OPERATION_KEY, config), + UseInterceptors(CreditInterceptor) + ); +} diff --git a/packages/mana-core-nestjs-integration/src/index.ts b/packages/mana-core-nestjs-integration/src/index.ts index 9b7b04c84..e32c06adb 100644 --- a/packages/mana-core-nestjs-integration/src/index.ts +++ b/packages/mana-core-nestjs-integration/src/index.ts @@ -15,6 +15,14 @@ export { OptionalAuthGuard } from './guards/optional-auth.guard'; // Decorators export { CurrentUser, JwtPayload } from './decorators/current-user.decorator'; export { Public, IS_PUBLIC_KEY } from './decorators/public.decorator'; +export { + UseCredits, + CreditOperationConfig, + CREDIT_OPERATION_KEY, +} from './decorators/use-credits.decorator'; + +// Interceptors +export { CreditInterceptor } from './interceptors/credit.interceptor'; // Services export { @@ -28,3 +36,18 @@ export { InsufficientCreditsException, InsufficientCreditsDetails, } from './exceptions/insufficient-credits.exception'; + +// Re-export credit operations for convenience +export { + CreditOperationType, + CREDIT_COSTS, + CreditCategory, + getCreditCost, + getOperationMetadata, + getOperationsForApp, + formatCreditCost, + getPricingTable, + isFreeOperation, + isMicroCreditOperation, + isAiOperation, +} from '@manacore/credit-operations'; diff --git a/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts b/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts new file mode 100644 index 000000000..b68b24abe --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts @@ -0,0 +1,195 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, + Inject, + Optional, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable, tap, catchError, throwError } from 'rxjs'; +import { CreditClientService } from '../services/credit-client.service'; +import { + InsufficientCreditsException, + InsufficientCreditsDetails, +} from '../exceptions/insufficient-credits.exception'; +import { CREDIT_OPERATION_KEY, CreditOperationConfig } from '../decorators/use-credits.decorator'; +import { CREDIT_COSTS, getOperationMetadata } from '@manacore/credit-operations'; +import { MANA_CORE_OPTIONS } from '../mana-core.module'; +import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; + +/** + * Interceptor that handles credit validation and consumption for decorated endpoints. + * + * This interceptor: + * 1. Checks if the user has sufficient credits before executing the handler + * 2. Consumes credits after successful execution (or before, depending on config) + * 3. Throws InsufficientCreditsException if the user doesn't have enough credits + */ +@Injectable() +export class CreditInterceptor implements NestInterceptor { + private readonly logger = new Logger(CreditInterceptor.name); + + constructor( + private readonly reflector: Reflector, + private readonly creditClient: CreditClientService, + @Optional() + @Inject(MANA_CORE_OPTIONS) + private readonly options?: ManaCoreModuleOptions + ) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const config = this.reflector.get( + CREDIT_OPERATION_KEY, + context.getHandler() + ); + + // If no config, just proceed (shouldn't happen if decorator is used correctly) + if (!config) { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + // Check if user is authenticated + if (!user?.sub) { + this.logger.warn('No authenticated user found for credit operation'); + return next.handle(); + } + + const userId = user.sub; + const operationName = config.operation; + + // Calculate cost + const cost = this.calculateCost(config, request); + const consumeMode = config.consumeMode || 'after'; + + // Skip in development if configured + if (config.skipInDev && this.isDevelopment()) { + this.logger.debug(`Skipping credit check in development for ${operationName}`); + return next.handle(); + } + + // Validate credits before execution + const validation = await this.creditClient.validateCredits(userId, operationName, cost); + + if (!validation.hasCredits) { + const details: InsufficientCreditsDetails = { + requiredCredits: cost, + availableCredits: validation.availableCredits, + creditType: 'user', + operation: operationName, + }; + throw new InsufficientCreditsException(details); + } + + // If consume mode is 'before', consume now + if (consumeMode === 'before') { + const description = this.generateDescription(config, request); + const consumed = await this.creditClient.consumeCredits( + userId, + operationName, + cost, + description, + this.buildMetadata(config, request) + ); + + if (!consumed) { + this.logger.error(`Failed to consume credits for ${operationName}`); + // Still allow the operation to proceed - fail open + } + + return next.handle(); + } + + // If consume mode is 'after', consume on success + return next.handle().pipe( + tap(async () => { + const description = this.generateDescription(config, request); + const consumed = await this.creditClient.consumeCredits( + userId, + operationName, + cost, + description, + this.buildMetadata(config, request) + ); + + if (!consumed) { + this.logger.error(`Failed to consume credits after success for ${operationName}`); + } else if (this.options?.debug) { + this.logger.log(`Consumed ${cost} credits for ${operationName} (user: ${userId})`); + } + }), + catchError((error) => { + // Don't consume credits if the operation failed + this.logger.debug(`Operation ${operationName} failed, credits not consumed`); + return throwError(() => error); + }) + ); + } + + /** + * Calculate the credit cost for the operation. + */ + private calculateCost(config: CreditOperationConfig, request: any): number { + // Dynamic cost takes priority + if (config.dynamicCost) { + return config.dynamicCost(request); + } + + // Custom cost override + if (config.customCost !== undefined) { + return config.customCost; + } + + // Default cost from CREDIT_COSTS + return CREDIT_COSTS[config.operation] || 0; + } + + /** + * Generate a description for the credit transaction. + */ + private generateDescription(config: CreditOperationConfig, request: any): string { + // Custom description function + if (config.descriptionFn) { + return config.descriptionFn(request); + } + + // Default description from operation metadata + const metadata = getOperationMetadata(config.operation); + return metadata?.name || config.operation; + } + + /** + * Build metadata for the credit transaction. + */ + private buildMetadata(config: CreditOperationConfig, request: any): Record { + const metadata: Record = { + operation: config.operation, + path: request.path, + method: request.method, + }; + + // Add app info from operation metadata + const opMeta = getOperationMetadata(config.operation); + if (opMeta) { + metadata.app = opMeta.app; + metadata.category = opMeta.category; + } + + return metadata; + } + + /** + * Check if running in development mode. + */ + private isDevelopment(): boolean { + return ( + this.options?.debug || + process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'dev' + ); + } +} diff --git a/packages/mana-core-nestjs-integration/src/interceptors/index.ts b/packages/mana-core-nestjs-integration/src/interceptors/index.ts new file mode 100644 index 000000000..4e0a4af26 --- /dev/null +++ b/packages/mana-core-nestjs-integration/src/interceptors/index.ts @@ -0,0 +1 @@ +export { CreditInterceptor } from './credit.interceptor'; diff --git a/packages/shared-credit-ui/package.json b/packages/shared-credit-ui/package.json new file mode 100644 index 000000000..e6aaf7d35 --- /dev/null +++ b/packages/shared-credit-ui/package.json @@ -0,0 +1,51 @@ +{ + "name": "@manacore/shared-credit-ui", + "version": "1.0.0", + "private": true, + "description": "Credit system UI components for web (Svelte) and mobile (React Native)", + "exports": { + ".": { + "types": "./src/web/index.ts", + "svelte": "./src/web/index.ts", + "default": "./src/web/index.ts" + }, + "./web": { + "types": "./src/web/index.ts", + "svelte": "./src/web/index.ts", + "default": "./src/web/index.ts" + }, + "./mobile": { + "types": "./src/mobile/index.ts", + "default": "./src/mobile/index.ts" + } + }, + "scripts": { + "type-check": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@manacore/credit-operations": "workspace:*" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "react": "^18.0.0", + "react-native": "^0.74.0 || ^0.75.0 || ^0.76.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0 || ^0.81.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "react": { + "optional": true + }, + "react-native": { + "optional": true + } + }, + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-native": "^0.73.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/shared-credit-ui/src/mobile/CreditBalance.tsx b/packages/shared-credit-ui/src/mobile/CreditBalance.tsx new file mode 100644 index 000000000..1b6fd218b --- /dev/null +++ b/packages/shared-credit-ui/src/mobile/CreditBalance.tsx @@ -0,0 +1,217 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native'; +import { formatCreditCost } from '@manacore/credit-operations'; + +interface CreditBalanceProps { + /** Current credit balance */ + balance: number; + /** Free credits remaining (optional) */ + freeCredits?: number; + /** Whether the balance is loading */ + loading?: boolean; + /** Callback when "Buy Credits" is pressed */ + onBuyPress?: () => void; + /** Whether to show as compact (header) or expanded */ + variant?: 'compact' | 'expanded'; + /** Low balance threshold for warning */ + lowBalanceThreshold?: number; + /** i18n labels */ + labels?: { + credits?: string; + freeCredits?: string; + buyCredits?: string; + lowBalance?: string; + }; +} + +export function CreditBalance({ + balance, + freeCredits = 0, + loading = false, + onBuyPress, + variant = 'compact', + lowBalanceThreshold = 10, + labels = {}, +}: CreditBalanceProps) { + const { + credits: creditsLabel = 'Credits', + freeCredits: freeCreditsLabel = 'free', + buyCredits: buyCreditsLabel = 'Buy', + lowBalance: lowBalanceLabel = 'Low balance', + } = labels; + + const totalCredits = balance + freeCredits; + const isLowBalance = totalCredits < lowBalanceThreshold; + const formattedBalance = formatCreditCost(totalCredits); + + if (loading) { + return ( + + + + ); + } + + if (variant === 'compact') { + return ( + + + + {formattedBalance} + + {onBuyPress && +} + + ); + } + + return ( + + + + + {creditsLabel} + + {formattedBalance} + + + {freeCredits > 0 && ( + + {formatCreditCost(freeCredits)} {freeCreditsLabel} + + )} + + {isLowBalance && ( + + ⚠️ + {lowBalanceLabel} + + )} + + {onBuyPress && ( + + {buyCreditsLabel} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + }, + compact: { + height: 32, + }, + expanded: { + padding: 16, + borderRadius: 12, + backgroundColor: 'rgba(255, 255, 255, 0.9)', + borderWidth: 1, + borderColor: 'rgba(0, 0, 0, 0.1)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + compactButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 999, + backgroundColor: 'rgba(0, 0, 0, 0.05)', + }, + compactButtonLow: { + backgroundColor: 'rgba(239, 68, 68, 0.1)', + }, + icon: { + fontSize: 14, + color: '#3b82f6', + }, + iconLow: { + color: '#ef4444', + }, + iconLarge: { + fontSize: 18, + }, + compactValue: { + fontSize: 14, + fontWeight: '600', + color: '#1f2937', + }, + valueLow: { + color: '#dc2626', + }, + plusIcon: { + marginLeft: 2, + fontSize: 14, + fontWeight: '700', + color: '#6b7280', + opacity: 0.6, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + titleRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + title: { + fontSize: 14, + fontWeight: '600', + color: '#6b7280', + }, + largeValue: { + fontSize: 24, + fontWeight: '700', + color: '#1f2937', + }, + freeCredits: { + fontSize: 12, + color: '#6b7280', + marginBottom: 12, + }, + warning: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + padding: 8, + marginBottom: 12, + borderRadius: 8, + backgroundColor: 'rgba(239, 68, 68, 0.1)', + }, + warningIcon: { + fontSize: 14, + }, + warningText: { + fontSize: 12, + fontWeight: '500', + color: '#dc2626', + }, + buyButton: { + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 8, + backgroundColor: '#3b82f6', + alignItems: 'center', + }, + buyButtonText: { + fontSize: 14, + fontWeight: '600', + color: '#ffffff', + }, +}); + +export default CreditBalance; diff --git a/packages/shared-credit-ui/src/mobile/CreditToast.tsx b/packages/shared-credit-ui/src/mobile/CreditToast.tsx new file mode 100644 index 000000000..1e507f29a --- /dev/null +++ b/packages/shared-credit-ui/src/mobile/CreditToast.tsx @@ -0,0 +1,253 @@ +import React, { useEffect } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Animated } from 'react-native'; +import { formatCreditCost } from '@manacore/credit-operations'; + +interface CreditToastProps { + /** The operation name or description */ + operation: string; + /** Amount of credits consumed (positive) or refunded (negative) */ + amount: number; + /** Remaining balance after the transaction */ + remainingBalance?: number; + /** Toast type */ + type?: 'success' | 'error' | 'warning'; + /** Whether the toast is visible */ + visible?: boolean; + /** Callback when toast should be dismissed */ + onDismiss?: () => void; + /** Auto-dismiss timeout in ms (0 = no auto-dismiss) */ + autoDismissMs?: number; + /** i18n labels */ + labels?: { + credits?: string; + remaining?: string; + insufficient?: string; + }; +} + +export function CreditToast({ + operation, + amount, + remainingBalance, + type = 'success', + visible = true, + onDismiss, + autoDismissMs = 4000, + labels = {}, +}: CreditToastProps) { + const { + remaining: remainingLabel = 'remaining', + insufficient: insufficientLabel = 'Insufficient credits', + } = labels; + + const fadeAnim = React.useRef(new Animated.Value(0)).current; + const slideAnim = React.useRef(new Animated.Value(-20)).current; + + const isDeduction = amount > 0; + const formattedAmount = formatCreditCost(Math.abs(amount)); + const formattedRemaining = + remainingBalance !== undefined ? formatCreditCost(remainingBalance) : null; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + + if (autoDismissMs > 0 && onDismiss) { + const timer = setTimeout(onDismiss, autoDismissMs); + return () => clearTimeout(timer); + } + } else { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 0, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: -20, + duration: 150, + useNativeDriver: true, + }), + ]).start(); + } + return undefined; + }, [visible, autoDismissMs, onDismiss, fadeAnim, slideAnim]); + + if (!visible) return null; + + const getTypeStyles = () => { + switch (type) { + case 'success': + return { + iconBg: styles.iconBgSuccess, + icon: '✓', + iconColor: '#22c55e', + }; + case 'error': + return { + iconBg: styles.iconBgError, + icon: '✕', + iconColor: '#ef4444', + }; + case 'warning': + return { + iconBg: styles.iconBgWarning, + icon: '⚠', + iconColor: '#f59e0b', + }; + default: + return { + iconBg: styles.iconBgSuccess, + icon: '✓', + iconColor: '#22c55e', + }; + } + }; + + const typeStyles = getTypeStyles(); + + return ( + + + {typeStyles.icon} + + + + + {operation} + + + {type === 'error' ? ( + {insufficientLabel} + ) : ( + <> + + {isDeduction ? '-' : '+'} + {formattedAmount} + + {formattedRemaining !== null && ( + + ({formattedRemaining} {remainingLabel}) + + )} + + )} + + + + {onDismiss && ( + + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 12, + paddingHorizontal: 16, + paddingVertical: 14, + borderRadius: 12, + backgroundColor: 'rgba(255, 255, 255, 0.95)', + borderWidth: 1, + borderColor: 'rgba(0, 0, 0, 0.1)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 5, + minWidth: 280, + maxWidth: 400, + }, + iconWrapper: { + width: 24, + height: 24, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, + iconBgSuccess: { + backgroundColor: 'rgba(34, 197, 94, 0.1)', + }, + iconBgError: { + backgroundColor: 'rgba(239, 68, 68, 0.1)', + }, + iconBgWarning: { + backgroundColor: 'rgba(245, 158, 11, 0.1)', + }, + iconText: { + fontSize: 12, + fontWeight: '700', + }, + content: { + flex: 1, + }, + operation: { + fontSize: 14, + fontWeight: '500', + color: '#1f2937', + marginBottom: 4, + }, + details: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + amount: { + fontSize: 12, + fontWeight: '600', + color: '#ef4444', + }, + amountRefund: { + color: '#22c55e', + }, + amountError: { + fontSize: 12, + fontWeight: '600', + color: '#ef4444', + }, + remaining: { + fontSize: 12, + color: '#6b7280', + }, + dismissButton: { + width: 20, + height: 20, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 4, + }, + dismissIcon: { + fontSize: 12, + color: '#9ca3af', + }, +}); + +export default CreditToast; diff --git a/packages/shared-credit-ui/src/mobile/index.ts b/packages/shared-credit-ui/src/mobile/index.ts new file mode 100644 index 000000000..1866c5abe --- /dev/null +++ b/packages/shared-credit-ui/src/mobile/index.ts @@ -0,0 +1,19 @@ +/** + * Credit UI components for mobile (React Native) + */ + +export { CreditBalance } from './CreditBalance'; +export { CreditToast } from './CreditToast'; + +// Re-export useful functions from credit-operations +export { + formatCreditCost, + getCreditCost, + getOperationMetadata, + getPricingTable, + isFreeOperation, + isMicroCreditOperation, + isAiOperation, + CreditOperationType, + CreditCategory, +} from '@manacore/credit-operations'; diff --git a/packages/shared-credit-ui/src/web/CreditBalance.svelte b/packages/shared-credit-ui/src/web/CreditBalance.svelte new file mode 100644 index 000000000..abc5932e5 --- /dev/null +++ b/packages/shared-credit-ui/src/web/CreditBalance.svelte @@ -0,0 +1,339 @@ + + +{#if variant === 'compact'} +
+ {#if loading} +
+ {:else} + + {/if} +
+{:else} +
+ {#if loading} +
+ {:else} +
+
+ + + +

{creditsLabel}

+
+
+ {formattedBalance} +
+
+ + {#if freeCredits > 0} +

+ {formatCreditCost(freeCredits)} + {freeCreditsLabel} +

+ {/if} + + {#if isLowBalance} +
+ + + + {lowBalanceLabel} +
+ {/if} + + {#if onBuyClick} + + {/if} + {/if} +
+{/if} + + diff --git a/packages/shared-credit-ui/src/web/CreditPricingTable.svelte b/packages/shared-credit-ui/src/web/CreditPricingTable.svelte new file mode 100644 index 000000000..bc7d3f69a --- /dev/null +++ b/packages/shared-credit-ui/src/web/CreditPricingTable.svelte @@ -0,0 +1,324 @@ + + +
+ {#if title} +

{title}

+ {/if} + + {#if filteredOperations.length === 0} +

No pricing information available for this app.

+ {:else if showCategories} + {@const groups = groupedOperations()} + {#each Object.entries(groups) as [category, operations]} +
+
+ + + +

+ {getCategoryLabel(category as CreditCategory)} +

+
+
    + {#each operations as op} +
  • +
    + {op.name} + {op.description} +
    +
    + {#if op.cost === 0} + {freeLabel} + {:else} + {op.formattedCost} + {/if} +
    +
  • + {/each} +
+
+ {/each} + {:else} +
+
+ {operationLabel} + {costLabel} +
+
    + {#each filteredOperations as op} +
  • +
    + {op.name} +
    +
    + {#if op.cost === 0} + {freeLabel} + {:else} + {op.formattedCost} + {/if} +
    +
  • + {/each} +
+
+ {/if} + + +
+ + diff --git a/packages/shared-credit-ui/src/web/CreditToast.svelte b/packages/shared-credit-ui/src/web/CreditToast.svelte new file mode 100644 index 000000000..d5f4062e8 --- /dev/null +++ b/packages/shared-credit-ui/src/web/CreditToast.svelte @@ -0,0 +1,257 @@ + + +{#if visible} + +{/if} + + diff --git a/packages/shared-credit-ui/src/web/index.ts b/packages/shared-credit-ui/src/web/index.ts new file mode 100644 index 000000000..7c69d785b --- /dev/null +++ b/packages/shared-credit-ui/src/web/index.ts @@ -0,0 +1,20 @@ +/** + * Credit UI components for web (Svelte 5) + */ + +export { default as CreditBalance } from './CreditBalance.svelte'; +export { default as CreditToast } from './CreditToast.svelte'; +export { default as CreditPricingTable } from './CreditPricingTable.svelte'; + +// Re-export useful functions from credit-operations +export { + formatCreditCost, + getCreditCost, + getOperationMetadata, + getPricingTable, + isFreeOperation, + isMicroCreditOperation, + isAiOperation, + CreditOperationType, + CreditCategory, +} from '@manacore/credit-operations'; diff --git a/packages/shared-credit-ui/tsconfig.json b/packages/shared-credit-ui/tsconfig.json new file mode 100644 index 000000000..083dd84e3 --- /dev/null +++ b/packages/shared-credit-ui/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-native", + "types": ["svelte", "react", "react-native"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dac62bd3..31c88fa41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,7 +137,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9 + 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) @@ -188,7 +188,7 @@ importers: version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + 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) @@ -212,14 +212,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.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@20.19.25)(ioredis@5.9.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.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -228,13 +228,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -388,6 +388,12 @@ importers: apps/chat/apps/backend: dependencies: + '@manacore/credit-operations': + specifier: workspace:* + version: link:../../../../packages/credit-operations + '@manacore/nestjs-integration': + specifier: workspace:* + version: link:../../../../packages/mana-core-nestjs-integration '@manacore/shared-errors': specifier: workspace:* version: link:../../../../packages/shared-errors @@ -609,19 +615,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -4547,6 +4553,12 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/credit-operations: + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/eslint-config: dependencies: '@eslint/js': @@ -4585,6 +4597,9 @@ importers: packages/mana-core-nestjs-integration: dependencies: + '@manacore/credit-operations': + specifier: workspace:* + version: link:../credit-operations '@nestjs/common': specifier: ^10.0.0 || ^11.0.0 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4597,6 +4612,9 @@ importers: reflect-metadata: specifier: ^0.1.13 || ^0.2.0 version: 0.2.2 + rxjs: + specifier: ^7.0.0 + version: 7.8.2 devDependencies: '@types/node': specifier: ^20.0.0 @@ -4791,6 +4809,34 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/shared-credit-ui: + dependencies: + '@manacore/credit-operations': + specifier: workspace:* + version: link:../credit-operations + react: + specifier: ^18.0.0 + version: 18.3.1 + react-native: + specifier: ^0.74.0 || ^0.75.0 || ^0.76.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0 || ^0.81.0 + version: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 + '@types/react-native': + specifier: ^0.73.0 + version: 0.73.0(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + svelte: + specifier: ^5.0.0 + version: 5.44.0 + svelte-check: + specifier: ^4.0.0 + version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/shared-drizzle-config: dependencies: drizzle-kit: @@ -5471,9 +5517,6 @@ importers: jose: specifier: ^6.1.2 version: 6.1.2 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 nanoid: specifier: ^5.0.9 version: 5.1.6 @@ -5507,7 +5550,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) + version: 11.0.12(@types/node@22.19.1) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -5529,9 +5572,6 @@ importers: '@types/jest': specifier: ^29.5.14 version: 29.5.14 - '@types/jsonwebtoken': - specifier: ^9.0.7 - version: 9.0.10 '@types/node': specifier: ^22.10.2 version: 22.19.1 @@ -5570,7 +5610,7 @@ importers: version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) 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) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -9035,7 +9075,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==} @@ -12514,9 +12554,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} @@ -12590,6 +12627,10 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-native@0.73.0': + resolution: {integrity: sha512-6ZRPQrYM72qYKGWidEttRe6M5DZBEV5F+MHMHqd4TTYx0tfkcdrUFGdef6CCxY0jXU7wldvd/zA/b0A/kTeJmA==} + deprecated: This is a stub types definition. react-native provides its own type definitions, so you do not need this installed. + '@types/react@18.3.27': resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} @@ -18095,10 +18136,6 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} @@ -18107,15 +18144,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} @@ -18391,36 +18422,15 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -23708,6 +23718,16 @@ snapshots: transitivePeerDependencies: - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + dependencies: + astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + autoprefixer: 10.4.22(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.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) @@ -26777,7 +26797,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(5ll7ovd7i5kd7vxhny3dgbs3xy) + expo-router: 6.0.15(7mqaurqidri6vkknnsci36yp4e) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -28679,6 +28699,32 @@ snapshots: - uglify-js - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.19.12)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) + '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) + glob: 10.4.5 + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -28705,7 +28751,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -28716,14 +28762,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2(esbuild@0.19.12) + webpack: 5.100.2 webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -32726,11 +32772,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 22.19.1 - '@types/luxon@3.4.2': {} '@types/mdast@4.0.4': @@ -32810,6 +32851,19 @@ snapshots: dependencies: '@types/react': 19.2.7 + '@types/react-native@0.73.0(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - '@types/react' + - bufferutil + - react + - supports-color + - utf-8-validate + '@types/react@18.3.27': dependencies: '@types/prop-types': 15.7.15 @@ -32918,16 +32972,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -32976,15 +33030,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -33076,14 +33130,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -33115,14 +33169,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -33248,12 +33302,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33284,12 +33338,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33471,15 +33525,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -33510,13 +33564,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -34427,6 +34481,108 @@ snapshots: transitivePeerDependencies: - supports-color + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.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.9.2) + vfile: 6.0.3 + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.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 @@ -36839,6 +36995,11 @@ snapshots: escape-string-regexp@5.0.0: {} + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + semver: 7.7.3 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -36849,9 +37010,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-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 @@ -36866,9 +37027,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-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 @@ -36886,14 +37047,14 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -36918,17 +37079,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -36966,7 +37127,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.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -36977,7 +37138,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -36991,12 +37167,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -37011,25 +37187,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.48.0 + astro-eslint-parser: 1.2.2 + eslint: 9.39.1(jiti@1.21.7) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) + globals: 16.5.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 transitivePeerDependencies: - supports-color @@ -37053,12 +37243,6 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-utils: 2.1.0 - regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37112,7 +37296,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37121,9 +37305,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37135,7 +37319,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -37170,7 +37354,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-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 @@ -37181,7 +37365,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37199,7 +37383,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-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 @@ -37210,7 +37394,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37238,16 +37422,6 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.2 - resolve: 1.22.11 - semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37278,16 +37452,6 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - prettier: 3.6.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37312,10 +37476,6 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37346,28 +37506,6 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.39.1(jiti@1.21.7) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -38512,7 +38650,7 @@ snapshots: - react-native - supports-color - expo-router@6.0.15(5ll7ovd7i5kd7vxhny3dgbs3xy): + expo-router@6.0.15(7mqaurqidri6vkknnsci36yp4e): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -38551,7 +38689,7 @@ snapshots: 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(encoding@0.1.13)(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.19.12)) + 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' @@ -39681,6 +39819,23 @@ snapshots: forever-agent@0.6.1: {} + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 8.3.6(typescript@5.7.2) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -39715,23 +39870,6 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.9.3 - webpack: 5.100.2(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -39749,6 +39887,23 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.100.2 + form-data-encoder@1.7.2: {} form-data@2.3.3: @@ -42177,19 +42332,6 @@ snapshots: jsonpointer@5.0.1: {} - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.3 - jsprim@1.4.2: dependencies: assert-plus: 1.0.0 @@ -42204,23 +42346,12 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - jws@4.0.0: dependencies: jwa: 2.0.1 @@ -42441,26 +42572,12 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.includes@4.3.0: {} - lodash.isarguments@3.1.0: {} - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} lodash.throttle@4.1.1: {} @@ -46306,16 +46423,6 @@ snapshots: webpack-sources: 3.3.3 optional: true - 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.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.100.2(esbuild@0.19.12) - webpack-sources: 3.3.3 - optional: true - 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)): dependencies: acorn-loose: 8.5.2 @@ -46326,6 +46433,16 @@ snapshots: webpack-sources: 3.3.3 optional: true + 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 @@ -47814,14 +47931,14 @@ 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)): + 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 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) + webpack: 5.97.1(esbuild@0.19.12) optionalDependencies: esbuild: 0.19.12 @@ -48092,16 +48209,6 @@ snapshots: esbuild: 0.27.0 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 @@ -48122,6 +48229,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 @@ -48815,6 +48932,23 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 + vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.25 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -48918,6 +49052,10 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -49312,38 +49450,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 @@ -49406,6 +49512,36 @@ snapshots: - esbuild - uglify-js + webpack@5.97.1(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + 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: 3.3.0 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index 7c1cca50c..e7e262370 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -30,6 +30,7 @@ "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", "@nestjs/schedule": "^4.1.2", + "@nestjs/swagger": "^8.1.0", "@nestjs/throttler": "^6.2.1", "bcrypt": "^5.1.1", "better-auth": "^1.4.3", @@ -43,7 +44,6 @@ "duckdb-async": "^1.1.1", "helmet": "^8.0.0", "jose": "^6.1.2", - "jsonwebtoken": "^9.0.2", "nanoid": "^5.0.9", "nodemailer": "^7.0.12", "postgres": "^3.4.5", @@ -64,7 +64,6 @@ "@types/cookie-parser": "^1.4.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", - "@types/jsonwebtoken": "^9.0.7", "@types/node": "^22.10.2", "@types/nodemailer": "^7.0.5", "@types/supertest": "^6.0.2", diff --git a/services/mana-core-auth/src/auth/auth.controller.spec.ts b/services/mana-core-auth/src/auth/auth.controller.spec.ts index 62f24150a..50ee1b505 100644 --- a/services/mana-core-auth/src/auth/auth.controller.spec.ts +++ b/services/mana-core-auth/src/auth/auth.controller.spec.ts @@ -30,6 +30,7 @@ import { ForbiddenException, NotFoundException, } from '@nestjs/common'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { AuthController } from './auth.controller'; import { BetterAuthService } from './services/better-auth.service'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; @@ -63,6 +64,7 @@ describe('AuthController', () => { }; const module: TestingModule = await Test.createTestingModule({ + imports: [ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }])], controllers: [AuthController], providers: [ { @@ -73,6 +75,8 @@ describe('AuthController', () => { }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: jest.fn(() => true) }) + .overrideGuard(ThrottlerGuard) + .useValue({ canActivate: jest.fn(() => true) }) .compile(); controller = module.get(AuthController); diff --git a/services/mana-core-auth/src/auth/better-auth.config.ts b/services/mana-core-auth/src/auth/better-auth.config.ts index cc35c27ce..b028fbafc 100644 --- a/services/mana-core-auth/src/auth/better-auth.config.ts +++ b/services/mana-core-auth/src/auth/better-auth.config.ts @@ -281,7 +281,9 @@ export function createBetterAuth(databaseUrl: string) { */ jwt({ jwt: { - issuer: process.env.JWT_ISSUER || 'manacore', + // For OIDC compatibility, issuer MUST match the discovery document + // Use BASE_URL to match /.well-known/openid-configuration issuer + issuer: process.env.BASE_URL || process.env.JWT_ISSUER || 'http://localhost:3001', audience: process.env.JWT_AUDIENCE || 'manacore', expirationTime: '15m', diff --git a/services/mana-core-auth/src/main.ts b/services/mana-core-auth/src/main.ts index a1681bb9a..8a27731fb 100644 --- a/services/mana-core-auth/src/main.ts +++ b/services/mana-core-auth/src/main.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, RequestMethod } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import type { Request, Response, NextFunction } from 'express'; import helmet from 'helmet'; import cookieParser from 'cookie-parser'; @@ -128,12 +129,69 @@ async function bootstrap() { ], }); + // Swagger/OpenAPI documentation + const swaggerConfig = new DocumentBuilder() + .setTitle('Mana Core Auth API') + .setDescription( + ` +## Authentication & Authorization Service + +Mana Core Auth provides centralized authentication for the Mana ecosystem. + +### Features +- **User Authentication**: Registration, login, password reset +- **JWT Tokens**: EdDSA-signed access tokens via JWKS +- **Organizations (B2B)**: Multi-tenant support with roles +- **Credits**: Usage-based credit system +- **OIDC Provider**: OAuth2/OpenID Connect for SSO + +### Authentication +Most endpoints require a Bearer token in the Authorization header: +\`\`\` +Authorization: Bearer +\`\`\` + +### Rate Limits +- Registration: 5 req/min +- Login: 10 req/min +- Password Reset: 3 req/min +` + ) + .setVersion('1.0') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Enter your JWT access token', + }, + 'JWT-auth' + ) + .addTag('auth', 'User authentication (login, register, logout)') + .addTag('organizations', 'B2B organization management') + .addTag('credits', 'Credit balance and transactions') + .addTag('health', 'Service health checks') + .addServer('http://localhost:3001', 'Local Development') + .addServer('https://auth.mana.how', 'Production') + .build(); + + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api-docs', app, document, { + swaggerOptions: { + persistAuthorization: true, + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + customSiteTitle: 'Mana Core Auth API', + }); + const port = configService.get('port') || 3001; await app.listen(port); logger.info(`Mana Core Auth running on http://localhost:${port}`, { port, environment: configService.get('nodeEnv'), + docs: `http://localhost:${port}/api-docs`, }); } diff --git a/services/mana-core-auth/test/__mocks__/jose.ts b/services/mana-core-auth/test/__mocks__/jose.ts index 1e38f3b7d..1dbca3ecf 100644 --- a/services/mana-core-auth/test/__mocks__/jose.ts +++ b/services/mana-core-auth/test/__mocks__/jose.ts @@ -1,19 +1,244 @@ /** - * Jose Mock - Uses jest.requireActual to get the real module + * Jose Mock - Implements basic JWT functions for testing * - * We use the real jose library for JWT validation tests - * since we're testing actual JWT creation and verification. + * Since jose is an ESM module and jest runs in CommonJS mode, + * we need to provide a compatible mock implementation. + * + * This mock uses Node.js crypto for HS256 signing/verification. */ -// Use jest.requireActual to bypass the mock and get the real module -const actualJose = jest.requireActual('jose'); +import * as crypto from 'crypto'; -export const SignJWT = actualJose.SignJWT; -export const jwtVerify = actualJose.jwtVerify; -export const createRemoteJWKSet = actualJose.createRemoteJWKSet; -export const errors = actualJose.errors; -export const generateKeyPair = actualJose.generateKeyPair; -export const exportJWK = actualJose.exportJWK; -export const importJWK = actualJose.importJWK; -export const decodeJwt = actualJose.decodeJwt; -export const decodeProtectedHeader = actualJose.decodeProtectedHeader; +// Error classes matching jose's error types +export const errors = { + JWTExpired: class JWTExpired extends Error { + constructor(message = 'jwt expired') { + super(message); + this.name = 'JWTExpired'; + } + }, + JWTClaimValidationFailed: class JWTClaimValidationFailed extends Error { + constructor(message = 'jwt claim validation failed') { + super(message); + this.name = 'JWTClaimValidationFailed'; + } + }, + JWSSignatureVerificationFailed: class JWSSignatureVerificationFailed extends Error { + constructor(message = 'signature verification failed') { + super(message); + this.name = 'JWSSignatureVerificationFailed'; + } + }, + JWSInvalid: class JWSInvalid extends Error { + constructor(message = 'Invalid Compact JWS') { + super(message); + this.name = 'JWSInvalid'; + } + }, +}; + +// Base64url encode/decode utilities +function base64urlEncode(data: Buffer | string): string { + const buffer = typeof data === 'string' ? Buffer.from(data) : data; + return buffer.toString('base64url'); +} + +function base64urlDecode(str: string): Buffer { + return Buffer.from(str, 'base64url'); +} + +// SignJWT class for creating JWTs +export class SignJWT { + private payload: Record; + private header: { alg?: string; typ?: string } = { typ: 'JWT' }; + + constructor(payload: Record) { + this.payload = { ...payload }; + } + + setProtectedHeader(header: { alg: string; [key: string]: unknown }): this { + this.header = { ...this.header, ...header }; + return this; + } + + setIssuedAt(iat?: number): this { + this.payload.iat = iat ?? Math.floor(Date.now() / 1000); + return this; + } + + setExpirationTime(exp: string | number): this { + if (typeof exp === 'string') { + const match = exp.match(/^(\d+)(s|m|h|d)$/); + if (match) { + const [, value, unit] = match; + const seconds = { + s: 1, + m: 60, + h: 3600, + d: 86400, + }[unit]!; + const iat = (this.payload.iat as number) || Math.floor(Date.now() / 1000); + this.payload.exp = iat + parseInt(value) * seconds; + } + } else { + this.payload.exp = exp; + } + return this; + } + + setIssuer(issuer: string): this { + this.payload.iss = issuer; + return this; + } + + setAudience(audience: string): this { + this.payload.aud = audience; + return this; + } + + setNotBefore(nbf: number): this { + this.payload.nbf = nbf; + return this; + } + + async sign(secret: Uint8Array): Promise { + if (this.header.alg !== 'HS256') { + throw new Error(`Unsupported algorithm: ${this.header.alg}`); + } + + const headerB64 = base64urlEncode(JSON.stringify(this.header)); + const payloadB64 = base64urlEncode(JSON.stringify(this.payload)); + const signingInput = `${headerB64}.${payloadB64}`; + + const hmac = crypto.createHmac('sha256', Buffer.from(secret)); + hmac.update(signingInput); + const signature = hmac.digest(); + + return `${signingInput}.${base64urlEncode(signature)}`; + } +} + +// jwtVerify function for verifying JWTs +interface VerifyOptions { + algorithms?: string[]; + issuer?: string; + audience?: string; +} + +export async function jwtVerify( + token: string, + secret: Uint8Array, + options: VerifyOptions = {} +): Promise<{ payload: Record; protectedHeader: Record }> { + if (!token || typeof token !== 'string') { + throw new errors.JWSInvalid('jwt must be provided'); + } + + const parts = token.split('.'); + if (parts.length !== 3) { + throw new errors.JWSInvalid('Invalid Compact JWS'); + } + + const [headerB64, payloadB64, signatureB64] = parts; + + // Parse header + let header: Record; + try { + header = JSON.parse(base64urlDecode(headerB64).toString('utf8')); + } catch { + throw new errors.JWSInvalid('Invalid header'); + } + + // Verify algorithm + if (options.algorithms && !options.algorithms.includes(header.alg as string)) { + throw new errors.JWSInvalid('Invalid algorithm'); + } + + // Verify signature + const signingInput = `${headerB64}.${payloadB64}`; + const hmac = crypto.createHmac('sha256', Buffer.from(secret)); + hmac.update(signingInput); + const expectedSignature = hmac.digest(); + const actualSignature = base64urlDecode(signatureB64); + + if (!crypto.timingSafeEqual(expectedSignature, actualSignature)) { + throw new errors.JWSSignatureVerificationFailed('signature verification failed'); + } + + // Parse payload + let payload: Record; + try { + payload = JSON.parse(base64urlDecode(payloadB64).toString('utf8')); + } catch { + throw new errors.JWSInvalid('Invalid payload'); + } + + // Validate claims + const now = Math.floor(Date.now() / 1000); + + // Check expiration + if (payload.exp !== undefined && typeof payload.exp === 'number' && payload.exp <= now) { + throw new errors.JWTExpired('jwt expired'); + } + + // Check not before + if (payload.nbf !== undefined && typeof payload.nbf === 'number' && payload.nbf > now) { + throw new errors.JWTClaimValidationFailed('jwt not active yet'); + } + + // Check issuer + if (options.issuer !== undefined && payload.iss !== options.issuer) { + throw new errors.JWTClaimValidationFailed(`unexpected "iss" claim value`); + } + + // Check audience + if (options.audience !== undefined && payload.aud !== options.audience) { + throw new errors.JWTClaimValidationFailed(`unexpected "aud" claim value`); + } + + return { payload, protectedHeader: header }; +} + +// createRemoteJWKSet mock (returns a verification function) +export function createRemoteJWKSet( + _url: URL +): (header: unknown, token: unknown) => Promise { + return async () => { + throw new Error('Remote JWKS not supported in mock'); + }; +} + +// generateKeyPair mock +export async function generateKeyPair( + _alg: string +): Promise<{ publicKey: unknown; privateKey: unknown }> { + throw new Error('generateKeyPair not supported in mock'); +} + +// exportJWK mock +export async function exportJWK(_key: unknown): Promise { + throw new Error('exportJWK not supported in mock'); +} + +// importJWK mock +export async function importJWK(_jwk: unknown, _alg?: string): Promise { + throw new Error('importJWK not supported in mock'); +} + +// decodeJwt helper +export function decodeJwt(token: string): Record { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new errors.JWSInvalid('Invalid Compact JWS'); + } + return JSON.parse(base64urlDecode(parts[1]).toString('utf8')); +} + +// decodeProtectedHeader helper +export function decodeProtectedHeader(token: string): Record { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new errors.JWSInvalid('Invalid Compact JWS'); + } + return JSON.parse(base64urlDecode(parts[0]).toString('utf8')); +}