From 942c588e15f1bd35d8a5c85f4a26f98159ffaf4a Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Mon, 1 Dec 2025 17:16:21 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20feat(auth):=20centraliz?= =?UTF-8?q?e=20JWT=20validation=20via=20mana-core-auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create @manacore/shared-nestjs-auth package with JwtAuthGuard - Update @mana-core/nestjs-integration to validate tokens via auth service - Replace insecure local JWT decode with server-side validation - Integrate Zitare, Presi, ManaDeck backends with centralized auth - Add DEV_BYPASS_AUTH support for development mode - Document auth architecture in CLAUDE.md --- CLAUDE.md | 164 +++++++++++++-- apps/manadeck/apps/backend/src/app.module.ts | 12 +- .../presi/apps/backend/src/auth/auth.guard.ts | 109 +++++----- apps/zitare/apps/backend/package.json | 1 + .../src/favorite/favorite.controller.ts | 41 +--- .../apps/backend/src/list/list.controller.ts | 58 ++---- .../mana-core-nestjs-integration/package.json | 7 +- .../src/guards/auth.guard.ts | 147 +++++++++++-- .../src/guards/optional-auth.guard.ts | 85 ++++++-- .../interfaces/mana-core-options.interface.ts | 5 +- .../src/mana-core.module.ts | 4 +- .../src/services/credit-client.service.ts | 191 ++++++++++------- packages/shared-nestjs-auth/README.md | 142 +++++++++++++ packages/shared-nestjs-auth/package.json | 33 +++ .../src/decorators/current-user.decorator.ts | 21 ++ .../src/guards/jwt-auth.guard.ts | 133 ++++++++++++ packages/shared-nestjs-auth/src/index.ts | 29 +++ .../shared-nestjs-auth/src/types/index.ts | 40 ++++ packages/shared-nestjs-auth/tsconfig.json | 24 +++ pnpm-lock.yaml | 194 +++++++++++++----- 20 files changed, 1126 insertions(+), 314 deletions(-) create mode 100644 packages/shared-nestjs-auth/README.md create mode 100644 packages/shared-nestjs-auth/package.json create mode 100644 packages/shared-nestjs-auth/src/decorators/current-user.decorator.ts create mode 100644 packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts create mode 100644 packages/shared-nestjs-auth/src/index.ts create mode 100644 packages/shared-nestjs-auth/src/types/index.ts create mode 100644 packages/shared-nestjs-auth/tsconfig.json diff --git a/CLAUDE.md b/CLAUDE.md index 21a414fb5..10946440c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,12 +139,146 @@ apps/{project}/ ### Authentication Architecture -All projects use a **middleware-based authentication** pattern via Mana Core: +All projects use **mana-core-auth** as the central authentication service: -- Middleware issues: `manaToken`, `appToken` (Supabase-compatible JWT), `refreshToken` -- Mobile apps use `@manacore/shared-auth` package for auth services -- Tokens stored via platform-specific storage (SecureStore on mobile, localStorage on web) -- Supabase RLS policies use JWT claims (`sub`, `role`, `app_id`) +``` +┌─────────────┐ ┌─────────────┐ ┌────────────────┐ +│ Client │────>│ Backend │────>│ mana-core-auth │ +│ (Web/Mobile)│ │ (NestJS) │ │ (port 3001) │ +└─────────────┘ └─────────────┘ └────────────────┘ + │ │ │ + │ Bearer token │ POST /validate │ + │ │ {token} │ + │ │<────────────────────│ + │ │ {valid, payload} │ + │<──────────────────│ │ + │ Response │ │ +``` + +#### Key Components + +| Component | Purpose | +|-----------|---------| +| `services/mana-core-auth` | Central auth service (Better Auth + EdDSA JWT) | +| `@manacore/shared-nestjs-auth` | Shared NestJS guards/decorators for JWT validation | +| `@mana-core/nestjs-integration` | Extended NestJS module with auth + credits | +| `@manacore/shared-auth` | Client-side auth for web/mobile apps | + +#### NestJS Backend Integration + +**Option 1: Simple auth only** - Use `@manacore/shared-nestjs-auth`: + +```typescript +// In your controller +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller('api') +@UseGuards(JwtAuthGuard) +export class MyController { + @Get('profile') + getProfile(@CurrentUser() user: CurrentUserData) { + return { userId: user.userId, email: user.email }; + } +} +``` + +**Option 2: Auth + Credits** - Use `@mana-core/nestjs-integration`: + +```typescript +// app.module.ts +import { ManaCoreModule } from '@mana-core/nestjs-integration'; + +@Module({ + imports: [ + ManaCoreModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + appId: config.get('APP_ID'), + serviceKey: config.get('MANA_CORE_SERVICE_KEY'), + debug: config.get('NODE_ENV') === 'development', + }), + inject: [ConfigService], + }), + ], +}) +export class AppModule {} + +// In controller +import { AuthGuard } from '@mana-core/nestjs-integration/guards'; +import { CurrentUser } from '@mana-core/nestjs-integration/decorators'; +import { CreditClientService } from '@mana-core/nestjs-integration'; + +@Controller('api') +@UseGuards(AuthGuard) +export class ApiController { + constructor(private creditClient: CreditClientService) {} + + @Post('generate') + async generate(@CurrentUser() user: any) { + await this.creditClient.consumeCredits(user.sub, 'generation', 10, 'AI generation'); + // ... do work + } +} +``` + +#### Required Environment Variables + +```env +# All backends need this +MANA_CORE_AUTH_URL=http://localhost:3001 + +# For development bypass (optional) +NODE_ENV=development +DEV_BYPASS_AUTH=true +DEV_USER_ID=your-test-user-id + +# For credit operations (optional) +MANA_CORE_SERVICE_KEY=your-service-key +APP_ID=your-app-id +``` + +#### JWT Token Structure (EdDSA) + +```json +{ + "sub": "user-id", + "email": "user@example.com", + "role": "user", + "sid": "session-id", + "exp": 1764606251, + "iss": "manacore", + "aud": "manacore" +} +``` + +#### Testing Auth Integration + +```bash +# 1. Start mana-core-auth +pnpm dev:auth + +# 2. Start a backend (e.g., Zitare) +pnpm dev:zitare:backend + +# 3. Get a token +TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken') + +# 4. Call protected endpoint +curl http://localhost:3007/api/favorites \ + -H "Authorization: Bearer $TOKEN" +``` + +#### Integrated Backends + +| Backend | Package | Port | +|---------|---------|------| +| Chat | `@mana-core/nestjs-integration` | 3002 | +| Picture | `@manacore/shared-nestjs-auth` | 3006 | +| Zitare | `@manacore/shared-nestjs-auth` | 3007 | +| Presi | Custom (same pattern) | 3008 | +| ManaDeck | `@mana-core/nestjs-integration` | 3009 | ### Svelte 5 Runes Mode (Web Apps) @@ -165,15 +299,17 @@ $: doubled = count * 2; ## Shared Packages (`packages/`) -| Package | Purpose | -| --------------------------- | ------------------------------------------------------- | -| `@manacore/shared-auth` | Configurable auth service, token manager, JWT utilities | -| `@manacore/shared-supabase` | Unified Supabase client | -| `@manacore/shared-types` | Common TypeScript types | -| `@manacore/shared-utils` | Utility functions | -| `@manacore/shared-ui` | React Native UI components | -| `@manacore/shared-theme` | Theme configuration | -| `@manacore/shared-i18n` | Internationalization | +| Package | Purpose | +| -------------------------------- | ------------------------------------------------------- | +| `@manacore/shared-nestjs-auth` | NestJS JWT validation guards via mana-core-auth | +| `@mana-core/nestjs-integration` | NestJS module with auth guards + credit client | +| `@manacore/shared-auth` | Client-side auth service for web/mobile apps | +| `@manacore/shared-supabase` | Unified Supabase client | +| `@manacore/shared-types` | Common TypeScript types | +| `@manacore/shared-utils` | Utility functions | +| `@manacore/shared-ui` | React Native UI components | +| `@manacore/shared-theme` | Theme configuration | +| `@manacore/shared-i18n` | Internationalization | Import shared packages: diff --git a/apps/manadeck/apps/backend/src/app.module.ts b/apps/manadeck/apps/backend/src/app.module.ts index 497b206d7..00ccf64f3 100644 --- a/apps/manadeck/apps/backend/src/app.module.ts +++ b/apps/manadeck/apps/backend/src/app.module.ts @@ -39,20 +39,12 @@ import { ignoreEnvFile: process.env.NODE_ENV === 'production', }), - // Mana Core authentication + // Mana Core authentication (validates JWT via mana-core-auth service) ManaCoreModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ - manaServiceUrl: configService.get( - 'MANA_SERVICE_URL', - 'https://mana-core-middleware-111768794939.europe-west3.run.app' - ), appId: configService.get('APP_ID', 'cea4bfc6-a4de-4e17-91e2-54275940156e'), - serviceKey: configService.get('MANA_SUPABASE_SECRET_KEY', ''), // Required for service-to-service communication - signupRedirectUrl: configService.get( - 'SIGNUP_REDIRECT_URL', - 'https://manadeck.com/welcome' - ), + serviceKey: configService.get('MANA_CORE_SERVICE_KEY', ''), debug: configService.get('NODE_ENV') === 'development', }), inject: [ConfigService], diff --git a/apps/presi/apps/backend/src/auth/auth.guard.ts b/apps/presi/apps/backend/src/auth/auth.guard.ts index fc14f4152..426445614 100644 --- a/apps/presi/apps/backend/src/auth/auth.guard.ts +++ b/apps/presi/apps/backend/src/auth/auth.guard.ts @@ -1,67 +1,84 @@ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import * as crypto from 'crypto'; +/** + * JWT Auth Guard - Validates tokens via Mana Core Auth service + * + * Uses Better Auth with EdDSA algorithm (not RS256). + * Validates tokens by calling the central auth service's /validate endpoint. + */ @Injectable() export class AuthGuard implements CanActivate { constructor(private configService: ConfigService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const authHeader = request.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('Missing or invalid authorization header'); + // Development mode: bypass auth if DEV_BYPASS_AUTH is set + const isDev = this.configService.get('NODE_ENV') === 'development'; + const bypassAuth = this.configService.get('DEV_BYPASS_AUTH') === 'true'; + + if (isDev && bypassAuth) { + request.user = { + sub: '00000000-0000-0000-0000-000000000000', + email: 'dev@example.com', + role: 'user', + }; + return true; } - const token = authHeader.substring(7); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('No token provided'); + } try { - const payload = this.verifyToken(token); - request.user = payload; + // Get Mana Core Auth URL from config + const authUrl = + this.configService.get('MANA_CORE_AUTH_URL') || 'http://localhost:3001'; + + // Validate token with Mana Core Auth + const response = await fetch(`${authUrl}/api/v1/auth/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + throw new UnauthorizedException('Invalid token'); + } + + const { valid, payload } = await response.json(); + + if (!valid || !payload) { + throw new UnauthorizedException('Invalid token'); + } + + // Attach user to request + request.user = { + sub: payload.sub, + email: payload.email, + role: payload.role, + sessionId: payload.sessionId || payload.sid, + }; + return true; - } catch { - throw new UnauthorizedException('Invalid token'); + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + console.error('[AuthGuard] Error validating token:', error); + throw new UnauthorizedException('Token validation failed'); } } - private verifyToken(token: string): { sub: string; email: string; role: string } { - const publicKeyPem = this.configService.get('JWT_PUBLIC_KEY'); - if (!publicKeyPem) { - throw new Error('JWT_PUBLIC_KEY not configured'); + private extractTokenFromHeader(request: any): string | undefined { + const authHeader = request.headers.authorization; + if (!authHeader) { + return undefined; } - - // Decode token parts - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('Invalid token format'); - } - - const [headerB64, payloadB64, signatureB64] = parts; - - // Verify signature using RS256 - const verifier = crypto.createVerify('RSA-SHA256'); - verifier.update(`${headerB64}.${payloadB64}`); - - const publicKey = publicKeyPem.replace(/\\n/g, '\n'); - const signature = Buffer.from(signatureB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); - - if (!verifier.verify(publicKey, signature)) { - throw new Error('Invalid signature'); - } - - // Decode and parse payload - const payloadJson = Buffer.from( - payloadB64.replace(/-/g, '+').replace(/_/g, '/'), - 'base64' - ).toString('utf-8'); - const payload = JSON.parse(payloadJson); - - // Check expiration - if (payload.exp && payload.exp < Date.now() / 1000) { - throw new Error('Token expired'); - } - - return payload; + const [type, token] = authHeader.split(' '); + return type === 'Bearer' ? token : undefined; } } diff --git a/apps/zitare/apps/backend/package.json b/apps/zitare/apps/backend/package.json index d30b55c72..9d74e7770 100644 --- a/apps/zitare/apps/backend/package.json +++ b/apps/zitare/apps/backend/package.json @@ -18,6 +18,7 @@ "db:seed": "tsx src/db/seed.ts" }, "dependencies": { + "@manacore/shared-nestjs-auth": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/apps/zitare/apps/backend/src/favorite/favorite.controller.ts b/apps/zitare/apps/backend/src/favorite/favorite.controller.ts index 355e04f38..3b82751b9 100644 --- a/apps/zitare/apps/backend/src/favorite/favorite.controller.ts +++ b/apps/zitare/apps/backend/src/favorite/favorite.controller.ts @@ -5,10 +5,10 @@ import { Delete, Body, Param, - Headers, - UnauthorizedException, + UseGuards, ConflictException, } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; import { FavoriteService } from './favorite.service'; import { IsString, IsNotEmpty } from 'class-validator'; @@ -18,56 +18,35 @@ class CreateFavoriteDto { quoteId!: string; } -// Simple JWT extraction - in production, use proper auth middleware -function extractUserId(authHeader?: string): string { - if (!authHeader?.startsWith('Bearer ')) { - throw new UnauthorizedException('Missing or invalid authorization header'); - } - - try { - const token = authHeader.substring(7); - const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); - if (!payload.sub) { - throw new UnauthorizedException('Invalid token payload'); - } - return payload.sub; - } catch { - throw new UnauthorizedException('Invalid token'); - } -} - @Controller('favorites') +@UseGuards(JwtAuthGuard) export class FavoriteController { constructor(private readonly favoriteService: FavoriteService) {} @Get() - async findAll(@Headers('authorization') authHeader: string) { - const userId = extractUserId(authHeader); - const favorites = await this.favoriteService.findByUserId(userId); + async findAll(@CurrentUser() user: CurrentUserData) { + const favorites = await this.favoriteService.findByUserId(user.userId); return { favorites }; } @Post() - async create(@Headers('authorization') authHeader: string, @Body() dto: CreateFavoriteDto) { - const userId = extractUserId(authHeader); - + async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateFavoriteDto) { // Check if already favorited - const exists = await this.favoriteService.exists(userId, dto.quoteId); + const exists = await this.favoriteService.exists(user.userId, dto.quoteId); if (exists) { throw new ConflictException('Quote already in favorites'); } const favorite = await this.favoriteService.create({ - userId, + userId: user.userId, quoteId: dto.quoteId, }); return { favorite }; } @Delete(':quoteId') - async delete(@Headers('authorization') authHeader: string, @Param('quoteId') quoteId: string) { - const userId = extractUserId(authHeader); - await this.favoriteService.delete(userId, quoteId); + async delete(@CurrentUser() user: CurrentUserData, @Param('quoteId') quoteId: string) { + await this.favoriteService.delete(user.userId, quoteId); return { success: true }; } } diff --git a/apps/zitare/apps/backend/src/list/list.controller.ts b/apps/zitare/apps/backend/src/list/list.controller.ts index 3f441a35c..2810a0edd 100644 --- a/apps/zitare/apps/backend/src/list/list.controller.ts +++ b/apps/zitare/apps/backend/src/list/list.controller.ts @@ -6,9 +6,9 @@ import { Delete, Body, Param, - Headers, - UnauthorizedException, + UseGuards, } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; import { ListService } from './list.service'; import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator'; @@ -43,47 +43,27 @@ class AddQuoteDto { quoteId!: string; } -// Simple JWT extraction - in production, use proper auth middleware -function extractUserId(authHeader?: string): string { - if (!authHeader?.startsWith('Bearer ')) { - throw new UnauthorizedException('Missing or invalid authorization header'); - } - - try { - const token = authHeader.substring(7); - const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); - if (!payload.sub) { - throw new UnauthorizedException('Invalid token payload'); - } - return payload.sub; - } catch { - throw new UnauthorizedException('Invalid token'); - } -} - @Controller('lists') +@UseGuards(JwtAuthGuard) export class ListController { constructor(private readonly listService: ListService) {} @Get() - async findAll(@Headers('authorization') authHeader: string) { - const userId = extractUserId(authHeader); - const lists = await this.listService.findByUserId(userId); + async findAll(@CurrentUser() user: CurrentUserData) { + const lists = await this.listService.findByUserId(user.userId); return { lists }; } @Get(':id') - async findOne(@Headers('authorization') authHeader: string, @Param('id') id: string) { - const userId = extractUserId(authHeader); - const list = await this.listService.findById(userId, id); + async findOne(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { + const list = await this.listService.findById(user.userId, id); return { list }; } @Post() - async create(@Headers('authorization') authHeader: string, @Body() dto: CreateListDto) { - const userId = extractUserId(authHeader); + async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateListDto) { const list = await this.listService.create({ - userId, + userId: user.userId, name: dto.name, description: dto.description, }); @@ -92,41 +72,37 @@ export class ListController { @Put(':id') async update( - @Headers('authorization') authHeader: string, + @CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: UpdateListDto ) { - const userId = extractUserId(authHeader); - const list = await this.listService.update(userId, id, dto); + const list = await this.listService.update(user.userId, id, dto); return { list }; } @Delete(':id') - async delete(@Headers('authorization') authHeader: string, @Param('id') id: string) { - const userId = extractUserId(authHeader); - await this.listService.delete(userId, id); + async delete(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { + await this.listService.delete(user.userId, id); return { success: true }; } @Post(':id/quotes') async addQuote( - @Headers('authorization') authHeader: string, + @CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: AddQuoteDto ) { - const userId = extractUserId(authHeader); - const list = await this.listService.addQuoteToList(userId, id, dto.quoteId); + const list = await this.listService.addQuoteToList(user.userId, id, dto.quoteId); return { list }; } @Delete(':id/quotes/:quoteId') async removeQuote( - @Headers('authorization') authHeader: string, + @CurrentUser() user: CurrentUserData, @Param('id') id: string, @Param('quoteId') quoteId: string ) { - const userId = extractUserId(authHeader); - const list = await this.listService.removeQuoteFromList(userId, id, quoteId); + const list = await this.listService.removeQuoteFromList(user.userId, id, quoteId); return { list }; } } diff --git a/packages/mana-core-nestjs-integration/package.json b/packages/mana-core-nestjs-integration/package.json index 97f119a7d..674a85943 100644 --- a/packages/mana-core-nestjs-integration/package.json +++ b/packages/mana-core-nestjs-integration/package.json @@ -30,14 +30,9 @@ "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/config": "^3.0.0 || ^4.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", - "@nestjs/axios": "^3.0.0 || ^4.0.0", - "axios": "^1.6.0", - "jsonwebtoken": "^9.0.0", - "reflect-metadata": "^0.1.13 || ^0.2.0", - "rxjs": "^7.8.0" + "reflect-metadata": "^0.1.13 || ^0.2.0" }, "devDependencies": { - "@types/jsonwebtoken": "^9.0.0", "@types/node": "^20.0.0", "typescript": "^5.0.0" }, diff --git a/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts b/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts index 05caee14e..f70beccdb 100644 --- a/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts +++ b/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts @@ -6,20 +6,68 @@ import { Inject, Optional, } from '@nestjs/common'; -import * as jwt from 'jsonwebtoken'; +import { Reflector } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; import { MANA_CORE_OPTIONS } from '../mana-core.module'; import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; +import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; +interface TokenValidationResponse { + valid: boolean; + payload?: { + sub: string; + email: string; + role: string; + sessionId?: string; + sid?: string; + app_id?: string; + iat?: number; + exp?: number; + }; + error?: string; +} + +// Default development test user ID +const DEFAULT_DEV_USER_ID = '00000000-0000-0000-0000-000000000000'; + +/** + * JWT Authentication Guard for NestJS backends. + * + * Validates JWT tokens by calling the Mana Core Auth service. + * Supports development mode bypass via DEV_BYPASS_AUTH=true. + */ @Injectable() export class AuthGuard implements CanActivate { constructor( @Optional() @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions + private readonly options?: ManaCoreModuleOptions, + @Optional() + private readonly reflector?: Reflector, + @Optional() + private readonly configService?: ConfigService ) {} async canActivate(context: ExecutionContext): Promise { + // Check if route is marked as public + if (this.reflector) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + } + const request = context.switchToHttp().getRequest(); + + // Development mode: bypass auth if DEV_BYPASS_AUTH is set + if (this.shouldBypassAuth()) { + request.user = this.getDevUser(); + return true; + } + const token = this.extractTokenFromHeader(request); if (!token) { @@ -27,33 +75,19 @@ export class AuthGuard implements CanActivate { } try { - // Decode the token to extract user information - // The actual verification happens at the Mana Core middleware level - const decoded = jwt.decode(token) as jwt.JwtPayload | null; - - if (!decoded || !decoded.sub) { - throw new UnauthorizedException('Invalid token structure'); - } - - // Attach user info to request - request.user = { - sub: decoded.sub, - email: decoded.email || '', - role: decoded.role || 'user', - app_id: decoded.app_id, - iat: decoded.iat, - exp: decoded.exp, - }; - - // Store raw token for downstream services + const userData = await this.validateToken(token); + request.user = userData; request.accessToken = token; if (this.options?.debug) { - console.log('[AuthGuard] User authenticated:', decoded.sub); + console.log('[AuthGuard] User authenticated:', userData.sub); } return true; } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } if (this.options?.debug) { console.error('[AuthGuard] Token validation failed:', error); } @@ -61,6 +95,75 @@ export class AuthGuard implements CanActivate { } } + /** + * Check if auth should be bypassed (development mode) + */ + private shouldBypassAuth(): boolean { + const isDev = + this.configService?.get('NODE_ENV') === 'development' || + process.env.NODE_ENV === 'development'; + const bypassAuth = + this.configService?.get('DEV_BYPASS_AUTH') === 'true' || + process.env.DEV_BYPASS_AUTH === 'true'; + return isDev && bypassAuth; + } + + /** + * Get development user data + */ + private getDevUser() { + const devUserId = + this.configService?.get('DEV_USER_ID') || + process.env.DEV_USER_ID || + DEFAULT_DEV_USER_ID; + return { + sub: devUserId, + email: 'dev@example.com', + role: 'user', + app_id: this.options?.appId, + }; + } + + /** + * Validate token with Mana Core Auth service + */ + private async validateToken(token: string): Promise { + const authUrl = + this.configService?.get('MANA_CORE_AUTH_URL') || + process.env.MANA_CORE_AUTH_URL || + 'http://localhost:3001'; + + const response = await fetch(`${authUrl}/api/v1/auth/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + if (this.options?.debug) { + console.error('[AuthGuard] Token validation failed:', response.status, errorText); + } + throw new UnauthorizedException('Invalid token'); + } + + const result = (await response.json()) as TokenValidationResponse; + + if (!result.valid || !result.payload) { + throw new UnauthorizedException(result.error || 'Invalid token'); + } + + return { + sub: result.payload.sub, + email: result.payload.email, + role: result.payload.role, + app_id: result.payload.app_id || this.options?.appId, + sessionId: result.payload.sessionId || result.payload.sid, + iat: result.payload.iat, + exp: result.payload.exp, + }; + } + private extractTokenFromHeader(request: any): string | undefined { const authHeader = request.headers.authorization; if (!authHeader) { diff --git a/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts b/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts index 339df2212..11506a522 100644 --- a/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts +++ b/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts @@ -5,19 +5,36 @@ import { Inject, Optional, } from '@nestjs/common'; -import * as jwt from 'jsonwebtoken'; +import { ConfigService } from '@nestjs/config'; import { MANA_CORE_OPTIONS } from '../mana-core.module'; import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; +interface TokenValidationResponse { + valid: boolean; + payload?: { + sub: string; + email: string; + role: string; + sessionId?: string; + sid?: string; + app_id?: string; + iat?: number; + exp?: number; + }; + error?: string; +} + /** - * Optional auth guard - allows unauthenticated requests but still extracts user info if token is present + * Optional auth guard - allows unauthenticated requests but still validates and extracts user info if token is present */ @Injectable() export class OptionalAuthGuard implements CanActivate { constructor( @Optional() @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions + private readonly options?: ManaCoreModuleOptions, + @Optional() + private readonly configService?: ConfigService ) {} async canActivate(context: ExecutionContext): Promise { @@ -31,39 +48,69 @@ export class OptionalAuthGuard implements CanActivate { } try { - // Decode the token to extract user information - const decoded = jwt.decode(token) as jwt.JwtPayload | null; + const userData = await this.validateToken(token); - if (decoded && decoded.sub) { - // Attach user info to request - request.user = { - sub: decoded.sub, - email: decoded.email || '', - role: decoded.role || 'user', - app_id: decoded.app_id, - iat: decoded.iat, - exp: decoded.exp, - }; - - // Store raw token for downstream services + if (userData) { + request.user = userData; request.accessToken = token; if (this.options?.debug) { - console.log('[OptionalAuthGuard] User authenticated:', decoded.sub); + console.log('[OptionalAuthGuard] User authenticated:', userData.sub); } } else { request.user = null; } } catch (error) { if (this.options?.debug) { - console.error('[OptionalAuthGuard] Token decode failed:', error); + console.error('[OptionalAuthGuard] Token validation failed:', error); } + // For optional auth, we allow the request to proceed even if token validation fails request.user = null; } return true; } + /** + * Validate token with Mana Core Auth service + */ + private async validateToken(token: string): Promise { + const authUrl = + this.configService?.get('MANA_CORE_AUTH_URL') || + process.env.MANA_CORE_AUTH_URL || + 'http://localhost:3001'; + + try { + const response = await fetch(`${authUrl}/api/v1/auth/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + return null; + } + + const result = (await response.json()) as TokenValidationResponse; + + if (!result.valid || !result.payload) { + return null; + } + + return { + sub: result.payload.sub, + email: result.payload.email, + role: result.payload.role, + app_id: result.payload.app_id || this.options?.appId, + sessionId: result.payload.sessionId || result.payload.sid, + iat: result.payload.iat, + exp: result.payload.exp, + }; + } catch { + return null; + } + } + private extractTokenFromHeader(request: any): string | undefined { const authHeader = request.headers.authorization; if (!authHeader) { diff --git a/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts b/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts index d0b3b9f45..893202126 100644 --- a/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts +++ b/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts @@ -1,7 +1,10 @@ import { ModuleMetadata, Type } from '@nestjs/common'; export interface ManaCoreModuleOptions { - manaServiceUrl: string; + /** + * @deprecated No longer used - auth URL is read from MANA_CORE_AUTH_URL env variable + */ + manaServiceUrl?: string; appId: string; serviceKey?: string; signupRedirectUrl?: string; diff --git a/packages/mana-core-nestjs-integration/src/mana-core.module.ts b/packages/mana-core-nestjs-integration/src/mana-core.module.ts index 74b0dc5a7..28bca0a19 100644 --- a/packages/mana-core-nestjs-integration/src/mana-core.module.ts +++ b/packages/mana-core-nestjs-integration/src/mana-core.module.ts @@ -1,5 +1,4 @@ import { DynamicModule, Module, Global, Provider } from '@nestjs/common'; -import { HttpModule, HttpService } from '@nestjs/axios'; import { ManaCoreModuleOptions, ManaCoreModuleAsyncOptions, @@ -16,7 +15,6 @@ export class ManaCoreModule { static forRoot(options: ManaCoreModuleOptions): DynamicModule { return { module: ManaCoreModule, - imports: [HttpModule], providers: [ { provide: MANA_CORE_OPTIONS, @@ -34,7 +32,7 @@ export class ManaCoreModule { return { module: ManaCoreModule, - imports: [...(options.imports || []), HttpModule], + imports: options.imports || [], providers: [...asyncProviders, AuthGuard, CreditClientService], exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService], }; diff --git a/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts b/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts index 47195b1be..1e62022aa 100644 --- a/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts +++ b/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts @@ -1,6 +1,5 @@ import { Injectable, Inject, Optional, Logger } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { firstValueFrom } from 'rxjs'; +import { ConfigService } from '@nestjs/config'; import { MANA_CORE_OPTIONS } from '../mana-core.module'; import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; @@ -26,9 +25,35 @@ export class CreditClientService { @Inject(MANA_CORE_OPTIONS) private readonly options?: ManaCoreModuleOptions, @Optional() - private readonly httpService?: HttpService + private readonly configService?: ConfigService ) {} + private getAuthUrl(): string { + return ( + this.configService?.get('MANA_CORE_AUTH_URL') || + process.env.MANA_CORE_AUTH_URL || + 'http://localhost:3001' + ); + } + + private getServiceKey(): string { + return ( + this.options?.serviceKey || + this.configService?.get('MANA_CORE_SERVICE_KEY') || + process.env.MANA_CORE_SERVICE_KEY || + '' + ); + } + + private getAppId(): string { + return ( + this.options?.appId || + this.configService?.get('APP_ID') || + process.env.APP_ID || + '' + ); + } + async validateCredits( userId: string, operation: string, @@ -56,10 +81,11 @@ export class CreditClientService { } async getBalance(userId: string): Promise { - if (!this.httpService || !this.options?.manaServiceUrl) { - this.logger.warn( - 'HTTP service or Mana service URL not configured, returning default balance' - ); + const authUrl = this.getAuthUrl(); + const serviceKey = this.getServiceKey(); + + if (!serviceKey) { + this.logger.warn('Service key not configured, returning default balance'); return { balance: 1000, freeCreditsRemaining: 100, @@ -69,28 +95,30 @@ export class CreditClientService { } try { - const response = await firstValueFrom( - this.httpService.get( - `${this.options.manaServiceUrl}/credits/balance/${userId}`, - { - headers: { - 'X-Service-Key': this.options.serviceKey || '', - 'X-App-Id': this.options.appId || '', - }, - } - ) - ); + const response = await fetch(`${authUrl}/api/v1/credits/balance/${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Key': serviceKey, + 'X-App-Id': this.getAppId(), + }, + }); - return response.data; + if (!response.ok) { + this.logger.warn(`Credit balance request failed: ${response.status}`); + return this.getDefaultBalance(); + } + + const data = (await response.json()) as CreditBalance; + return { + balance: data.balance || 0, + freeCreditsRemaining: data.freeCreditsRemaining || 0, + totalEarned: data.totalEarned || 0, + totalSpent: data.totalSpent || 0, + }; } catch (error) { this.logger.error(`Failed to get balance for user ${userId}:`, error); - // Return default balance on error - return { - balance: 1000, - freeCreditsRemaining: 100, - totalEarned: 0, - totalSpent: 0, - }; + return this.getDefaultBalance(); } } @@ -101,37 +129,41 @@ export class CreditClientService { description: string, metadata?: Record ): Promise { - if (!this.httpService || !this.options?.manaServiceUrl) { - this.logger.warn( - 'HTTP service or Mana service URL not configured, skipping credit consumption' - ); + const authUrl = this.getAuthUrl(); + const serviceKey = this.getServiceKey(); + + if (!serviceKey) { + this.logger.warn('Service key not configured, skipping credit consumption'); return true; } try { - await firstValueFrom( - this.httpService.post( - `${this.options.manaServiceUrl}/credits/use`, - { - userId, - amount, - appId: this.options.appId, - description, - metadata: { - operation, - ...metadata, - }, + const response = await fetch(`${authUrl}/api/v1/credits/use`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Key': serviceKey, + 'X-App-Id': this.getAppId(), + }, + body: JSON.stringify({ + userId, + amount, + appId: this.getAppId(), + description, + metadata: { + operation, + ...metadata, }, - { - headers: { - 'X-Service-Key': this.options.serviceKey || '', - 'X-App-Id': this.options.appId || '', - }, - } - ) - ); + }), + }); - if (this.options.debug) { + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + this.logger.error(`Failed to consume credits: ${response.status} ${errorText}`); + return false; + } + + if (this.options?.debug) { this.logger.log(`Consumed ${amount} credits for user ${userId}: ${description}`); } @@ -148,32 +180,38 @@ export class CreditClientService { description: string, metadata?: Record ): Promise { - if (!this.httpService || !this.options?.manaServiceUrl) { - this.logger.warn('HTTP service or Mana service URL not configured, skipping credit refund'); + const authUrl = this.getAuthUrl(); + const serviceKey = this.getServiceKey(); + + if (!serviceKey) { + this.logger.warn('Service key not configured, skipping credit refund'); return true; } try { - await firstValueFrom( - this.httpService.post( - `${this.options.manaServiceUrl}/credits/refund`, - { - userId, - amount, - appId: this.options.appId, - description, - metadata, - }, - { - headers: { - 'X-Service-Key': this.options.serviceKey || '', - 'X-App-Id': this.options.appId || '', - }, - } - ) - ); + const response = await fetch(`${authUrl}/api/v1/credits/refund`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Key': serviceKey, + 'X-App-Id': this.getAppId(), + }, + body: JSON.stringify({ + userId, + amount, + appId: this.getAppId(), + description, + metadata, + }), + }); - if (this.options.debug) { + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + this.logger.error(`Failed to refund credits: ${response.status} ${errorText}`); + return false; + } + + if (this.options?.debug) { this.logger.log(`Refunded ${amount} credits for user ${userId}: ${description}`); } @@ -183,4 +221,13 @@ export class CreditClientService { return false; } } + + private getDefaultBalance(): CreditBalance { + return { + balance: 1000, + freeCreditsRemaining: 100, + totalEarned: 0, + totalSpent: 0, + }; + } } diff --git a/packages/shared-nestjs-auth/README.md b/packages/shared-nestjs-auth/README.md new file mode 100644 index 000000000..4b6801a61 --- /dev/null +++ b/packages/shared-nestjs-auth/README.md @@ -0,0 +1,142 @@ +# @manacore/shared-nestjs-auth + +Shared authentication utilities for NestJS backends in the Mana Core ecosystem. + +## Installation + +```bash +pnpm add @manacore/shared-nestjs-auth +``` + +## Usage + +### 1. Configure Environment Variables + +```env +# Required: Mana Core Auth service URL +MANA_CORE_AUTH_URL=http://localhost:3001 + +# Optional: Development mode auth bypass +NODE_ENV=development +DEV_BYPASS_AUTH=true +DEV_USER_ID=your-test-user-id +``` + +### 2. Use in Controllers + +```typescript +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller('api') +export class MyController { + // Public endpoint + @Get('health') + health() { + return { status: 'ok' }; + } + + // Protected endpoint + @Get('profile') + @UseGuards(JwtAuthGuard) + getProfile(@CurrentUser() user: CurrentUserData) { + return { + userId: user.userId, + email: user.email, + role: user.role, + }; + } +} +``` + +### 3. Apply Guard Globally (Optional) + +```typescript +// app.module.ts +import { Module } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtAuthGuard } from '@manacore/shared-nestjs-auth'; + +@Module({ + providers: [ + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], +}) +export class AppModule {} +``` + +## API + +### JwtAuthGuard + +A NestJS guard that validates JWT tokens via the Mana Core Auth service. + +- Extracts Bearer token from `Authorization` header +- Calls `POST /api/v1/auth/validate` on auth service +- Attaches user data to request object +- Supports `DEV_BYPASS_AUTH=true` for development + +### CurrentUser Decorator + +Parameter decorator to extract the authenticated user from the request. + +```typescript +@Get('me') +@UseGuards(JwtAuthGuard) +getMe(@CurrentUser() user: CurrentUserData) { + return user; +} +``` + +### CurrentUserData Interface + +```typescript +interface CurrentUserData { + userId: string; // User ID (from JWT sub claim) + email: string; // User email + role: string; // User role (user, admin, service) + sessionId?: string; // Session ID (optional) +} +``` + +## How It Works + +1. Client sends request with `Authorization: Bearer ` header +2. JwtAuthGuard extracts the token +3. Guard calls Mana Core Auth service to validate token +4. On success, user data is attached to `request.user` +5. Controller receives user via `@CurrentUser()` decorator + +``` +┌─────────────┐ ┌─────────────┐ ┌────────────────┐ +│ Client │────>│ Your API │────>│ mana-core-auth │ +│ │ │ (NestJS) │ │ (port 3001) │ +└─────────────┘ └─────────────┘ └────────────────┘ + │ │ │ + │ Bearer token │ POST /validate │ + │ │ {token} │ + │ │<────────────────────│ + │ │ {valid, payload} │ + │<──────────────────│ │ + │ Response │ │ +``` + +## Development Mode + +Set `DEV_BYPASS_AUTH=true` in development to skip token validation: + +```env +NODE_ENV=development +DEV_BYPASS_AUTH=true +DEV_USER_ID=17cb0be7-058a-4964-9e18-1fe7055fd014 +``` + +This will use a mock user for all authenticated requests. + +## Related Packages + +- `@manacore/shared-auth` - Client-side auth for web/mobile apps +- `@manacore/shared-types` - Shared TypeScript types diff --git a/packages/shared-nestjs-auth/package.json b/packages/shared-nestjs-auth/package.json new file mode 100644 index 000000000..10ea942d5 --- /dev/null +++ b/packages/shared-nestjs-auth/package.json @@ -0,0 +1,33 @@ +{ + "name": "@manacore/shared-nestjs-auth", + "version": "1.0.0", + "description": "Shared authentication utilities for NestJS backends - JWT validation via Mana Core Auth", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "pnpm build" + }, + "files": [ + "dist" + ], + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/config": "^3.0.0 || ^4.0.0" + }, + "devDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "keywords": [ + "nestjs", + "auth", + "jwt", + "manacore" + ], + "author": "Mana Core Team", + "license": "MIT" +} diff --git a/packages/shared-nestjs-auth/src/decorators/current-user.decorator.ts b/packages/shared-nestjs-auth/src/decorators/current-user.decorator.ts new file mode 100644 index 000000000..0929abf87 --- /dev/null +++ b/packages/shared-nestjs-auth/src/decorators/current-user.decorator.ts @@ -0,0 +1,21 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { CurrentUserData } from '../types'; + +/** + * Parameter decorator to extract the current user from the request. + * + * @example + * ```typescript + * @Get('profile') + * @UseGuards(JwtAuthGuard) + * getProfile(@CurrentUser() user: CurrentUserData) { + * return { userId: user.userId }; + * } + * ``` + */ +export const CurrentUser = createParamDecorator( + (data: unknown, ctx: ExecutionContext): CurrentUserData => { + const request = ctx.switchToHttp().getRequest(); + return request.user; + } +); diff --git a/packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts b/packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts new file mode 100644 index 000000000..7f38c5ae8 --- /dev/null +++ b/packages/shared-nestjs-auth/src/guards/jwt-auth.guard.ts @@ -0,0 +1,133 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TokenValidationResponse, CurrentUserData } from '../types'; + +// Default development test user ID +const DEFAULT_DEV_USER_ID = '00000000-0000-0000-0000-000000000000'; + +/** + * JWT Authentication Guard for NestJS backends. + * + * Validates JWT tokens by calling the Mana Core Auth service. + * Supports development mode bypass via DEV_BYPASS_AUTH=true. + * + * @example + * ```typescript + * // In your controller + * @Controller('api') + * @UseGuards(JwtAuthGuard) + * export class MyController { + * @Get('protected') + * getProtected(@CurrentUser() user: CurrentUserData) { + * return { userId: user.userId }; + * } + * } + * ``` + * + * @example + * ```typescript + * // Environment variables + * MANA_CORE_AUTH_URL=http://localhost:3001 + * DEV_BYPASS_AUTH=true // Optional: for development + * DEV_USER_ID=your-test-user-id // Optional: custom dev user + * ``` + */ +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private configService: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + // Development mode: bypass auth if DEV_BYPASS_AUTH is set + if (this.shouldBypassAuth()) { + request.user = this.getDevUser(); + return true; + } + + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('No token provided'); + } + + try { + const userData = await this.validateToken(token); + request.user = userData; + return true; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + console.error('[JwtAuthGuard] Error validating token:', error); + throw new UnauthorizedException('Token validation failed'); + } + } + + /** + * Check if auth should be bypassed (development mode) + */ + private shouldBypassAuth(): boolean { + const isDev = this.configService.get('NODE_ENV') === 'development'; + const bypassAuth = this.configService.get('DEV_BYPASS_AUTH') === 'true'; + return isDev && bypassAuth; + } + + /** + * Get development user data + */ + private getDevUser(): CurrentUserData { + return { + userId: this.configService.get('DEV_USER_ID') || DEFAULT_DEV_USER_ID, + email: 'dev@example.com', + role: 'user', + sessionId: 'dev-session', + }; + } + + /** + * Validate token with Mana Core Auth service + */ + private async validateToken(token: string): Promise { + const authUrl = + this.configService.get('MANA_CORE_AUTH_URL') || 'http://localhost:3001'; + + const response = await fetch(`${authUrl}/api/v1/auth/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + console.error('[JwtAuthGuard] Token validation failed:', response.status, errorText); + throw new UnauthorizedException('Invalid token'); + } + + const result: TokenValidationResponse = await response.json(); + + if (!result.valid || !result.payload) { + throw new UnauthorizedException(result.error || 'Invalid token'); + } + + return { + userId: result.payload.sub, + email: result.payload.email, + role: result.payload.role, + sessionId: result.payload.sessionId || result.payload.sid, + }; + } + + /** + * Extract Bearer token from Authorization header + */ + private extractTokenFromHeader(request: any): string | undefined { + const authHeader = request.headers.authorization; + if (!authHeader) { + return undefined; + } + + const [type, token] = authHeader.split(' '); + return type === 'Bearer' ? token : undefined; + } +} diff --git a/packages/shared-nestjs-auth/src/index.ts b/packages/shared-nestjs-auth/src/index.ts new file mode 100644 index 000000000..ac1d3f772 --- /dev/null +++ b/packages/shared-nestjs-auth/src/index.ts @@ -0,0 +1,29 @@ +/** + * @manacore/shared-nestjs-auth + * + * Shared authentication utilities for NestJS backends. + * Validates JWT tokens via the central Mana Core Auth service. + * + * @example + * ```typescript + * import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; + * + * @Controller('api') + * @UseGuards(JwtAuthGuard) + * export class MyController { + * @Get('profile') + * getProfile(@CurrentUser() user: CurrentUserData) { + * return { userId: user.userId, email: user.email }; + * } + * } + * ``` + */ + +// Guards +export { JwtAuthGuard } from './guards/jwt-auth.guard'; + +// Decorators +export { CurrentUser } from './decorators/current-user.decorator'; + +// Types +export type { CurrentUserData, AuthModuleConfig, TokenValidationResponse } from './types'; diff --git a/packages/shared-nestjs-auth/src/types/index.ts b/packages/shared-nestjs-auth/src/types/index.ts new file mode 100644 index 000000000..62e39dc5a --- /dev/null +++ b/packages/shared-nestjs-auth/src/types/index.ts @@ -0,0 +1,40 @@ +/** + * User data extracted from JWT token + */ +export interface CurrentUserData { + userId: string; + email: string; + role: string; + sessionId?: string; +} + +/** + * Configuration for the auth module + */ +export interface AuthModuleConfig { + /** URL of the Mana Core Auth service (default: http://localhost:3001) */ + authServiceUrl?: string; + /** Whether to bypass auth in development mode (default: false) */ + devBypassAuth?: boolean; + /** Test user ID for development mode */ + devUserId?: string; +} + +/** + * Response from token validation endpoint + */ +export interface TokenValidationResponse { + valid: boolean; + payload?: { + sub: string; + email: string; + role: string; + sessionId?: string; + sid?: string; + iat?: number; + exp?: number; + iss?: string; + aud?: string; + }; + error?: string; +} diff --git a/packages/shared-nestjs-auth/tsconfig.json b/packages/shared-nestjs-auth/tsconfig.json new file mode 100644 index 000000000..71e4820d5 --- /dev/null +++ b/packages/shared-nestjs-auth/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29762af48..228fbdbd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + concurrently: + specifier: ^9.2.0 + version: 9.2.1 prettier: specifier: ^3.3.3 version: 3.6.2 @@ -1866,6 +1869,9 @@ importers: apps/zitare/apps/backend: dependencies: + '@manacore/shared-nestjs-auth': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-auth '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -2508,9 +2514,6 @@ importers: packages/mana-core-nestjs-integration: dependencies: - '@nestjs/axios': - specifier: ^3.0.0 || ^4.0.0 - version: 4.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) '@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) @@ -2520,22 +2523,10 @@ importers: '@nestjs/core': specifier: ^10.0.0 || ^11.0.0 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2) - axios: - specifier: ^1.6.0 - version: 1.13.2 - jsonwebtoken: - specifier: ^9.0.0 - version: 9.0.2 reflect-metadata: specifier: ^0.1.13 || ^0.2.0 version: 0.2.2 - rxjs: - specifier: ^7.8.0 - version: 7.8.2 devDependencies: - '@types/jsonwebtoken': - specifier: ^9.0.0 - version: 9.0.10 '@types/node': specifier: ^20.0.0 version: 20.19.25 @@ -2792,6 +2783,21 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/shared-nestjs-auth: + devDependencies: + '@nestjs/common': + specifier: ^10.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) + '@nestjs/config': + specifier: ^3.0.0 + version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@types/node': + specifier: ^20.0.0 + version: 20.19.25 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/shared-profile-ui: devDependencies: svelte: @@ -2973,8 +2979,8 @@ importers: specifier: ^5.1.1 version: 5.1.1 better-auth: - specifier: ^1.1.1 - version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0) + specifier: ^1.4.3 + version: 1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -2996,6 +3002,9 @@ importers: helmet: specifier: ^8.0.0 version: 8.1.0 + jose: + specifier: ^6.1.2 + version: 6.1.2 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -4170,20 +4179,20 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@better-auth/core@1.4.2': - resolution: {integrity: sha512-bVXGpbWD8osNXYXVRMkWzv9BxfmOwqhKZp7QEHhyG1TZPTFpLLXBO7jPBplI2ve5rbmpl+0q5lDaYxG5msZtLg==} + '@better-auth/core@1.4.4': + resolution: {integrity: sha512-mlhoHPhgIPZNX73kJp1YzID4uaFXH6xgcjcxm2bYwTClvpgsY/n8MtI65Uy/K4vVd6fUvgfQETAni7nUkGWF3w==} peerDependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 - better-call: 1.1.0 + better-call: 1.1.3 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.4.2': - resolution: {integrity: sha512-z9JiY1SNNSBcMXhE9ZY60DvXbdt6whfqZ5vSPQlvSXyyqCC/TeGM8suhHWA8/2qqm7i6FyrxO4UHkAWta2dPkw==} + '@better-auth/telemetry@1.4.4': + resolution: {integrity: sha512-Eh18QPznmu0gL1vImas4PqEOEx57JJZEWfSSCHsGzcHcAEgX+QSh0fGhMERm4uInfBmDm2niTbu+/pUvVD+2kQ==} peerDependencies: - '@better-auth/core': 1.4.2 + '@better-auth/core': 1.4.4 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} @@ -9062,22 +9071,25 @@ packages: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} - better-auth@1.4.2: - resolution: {integrity: sha512-0NlJL+wNdHWGcGs9+kLTbYLoN0Vhft+pwhadn2QRWY7gqNdkLgH+UqX4x+yvCRyACRFStOJULQyZXWmQ3u7wTQ==} + better-auth@1.4.4: + resolution: {integrity: sha512-YawWmrqva1BhBtJl0CgspuWI+5RrApWI/Q7Gs3KnSyJYOaux3pWOsx2Jb5gCloNdYgTZsgdr3r1mNk5eEyOvCg==} peerDependencies: '@lynx-js/react': '*' - '@sveltejs/kit': '*' - next: '*' - react: '*' - react-dom: '*' - solid-js: '*' - svelte: '*' - vue: '*' + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + next: ^14.0.0 || ^15.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': optional: true '@sveltejs/kit': optional: true + '@tanstack/react-start': + optional: true next: optional: true react: @@ -9091,8 +9103,13 @@ packages: vue: optional: true - better-call@1.1.0: - resolution: {integrity: sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w==} + better-call@1.1.3: + resolution: {integrity: sha512-zN4sgcl5sI4MZDx0l3HpJkhKbidPGWyR/9TjCUmNFWl2la5z7Mk7xT5TxqGdN33OaU7fzHj/kzDYz7ck3IXvsQ==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} @@ -9575,6 +9592,11 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -19155,20 +19177,20 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@better-auth/core@1.4.2(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)': + '@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 '@standard-schema/spec': 1.0.0 - better-call: 1.1.0 + better-call: 1.1.3(zod@4.1.13) jose: 6.1.2 kysely: 0.28.8 nanostores: 1.1.0 zod: 4.1.13 - '@better-auth/telemetry@1.4.2(@better-auth/core@1.4.2(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.2(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 @@ -21853,12 +21875,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs/axios@4.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': - dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - axios: 1.13.2 - rxjs: 7.8.2 - '@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -24479,6 +24495,28 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 + '@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.5.0 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + optional: true + '@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@standard-schema/spec': 1.0.0 @@ -24536,6 +24574,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + optional: true + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) @@ -24596,6 +24644,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + debug: 4.4.3 + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.44.0 + vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - supports-color + optional: true + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) @@ -26912,32 +26973,35 @@ snapshots: - encoding - supports-color - better-auth@1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0): + better-auth@1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0): dependencies: - '@better-auth/core': 1.4.2(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.2(@better-auth/core@1.4.2(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.0)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)) + '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 2.0.1 '@noble/hashes': 2.0.1 '@standard-schema/spec': 1.0.0 - better-call: 1.1.0 + better-call: 1.1.3(zod@4.1.13) defu: 6.1.4 jose: 6.1.2 kysely: 0.28.8 nanostores: 1.1.0 zod: 4.1.13 optionalDependencies: + '@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) svelte: 5.44.0 - better-call@1.1.0: + better-call@1.1.3(zod@4.1.13): dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 rou3: 0.5.1 set-cookie-parser: 2.7.2 + optionalDependencies: + zod: 4.1.13 better-opn@3.0.2: dependencies: @@ -27470,6 +27534,15 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -38625,6 +38698,24 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + 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': 22.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.1 + optional: true + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -38662,6 +38753,11 @@ snapshots: optionalDependencies: vite: 7.2.4(@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) + vitefu@1.1.1(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + optional: true + vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)