🔒️ feat(auth): centralize JWT validation via mana-core-auth

- 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
This commit is contained in:
Wuesteon 2025-12-01 17:16:21 +01:00
parent 1d5f49a6d0
commit 942c588e15
20 changed files with 1126 additions and 314 deletions

164
CLAUDE.md
View file

@ -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:

View file

@ -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<string>(
'MANA_SERVICE_URL',
'https://mana-core-middleware-111768794939.europe-west3.run.app'
),
appId: configService.get<string>('APP_ID', 'cea4bfc6-a4de-4e17-91e2-54275940156e'),
serviceKey: configService.get<string>('MANA_SUPABASE_SECRET_KEY', ''), // Required for service-to-service communication
signupRedirectUrl: configService.get<string>(
'SIGNUP_REDIRECT_URL',
'https://manadeck.com/welcome'
),
serviceKey: configService.get<string>('MANA_CORE_SERVICE_KEY', ''),
debug: configService.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],

View file

@ -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<boolean> {
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<string>('NODE_ENV') === 'development';
const bypassAuth = this.configService.get<string>('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<string>('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<string>('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;
}
}

View file

@ -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",

View file

@ -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 };
}
}

View file

@ -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 };
}
}

View file

@ -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"
},

View file

@ -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<boolean> {
// Check if route is marked as public
if (this.reflector) {
const isPublic = this.reflector.getAllAndOverride<boolean>(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<string>('NODE_ENV') === 'development' ||
process.env.NODE_ENV === 'development';
const bypassAuth =
this.configService?.get<string>('DEV_BYPASS_AUTH') === 'true' ||
process.env.DEV_BYPASS_AUTH === 'true';
return isDev && bypassAuth;
}
/**
* Get development user data
*/
private getDevUser() {
const devUserId =
this.configService?.get<string>('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<any> {
const authUrl =
this.configService?.get<string>('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) {

View file

@ -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<boolean> {
@ -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<any | null> {
const authUrl =
this.configService?.get<string>('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) {

View file

@ -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;

View file

@ -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],
};

View file

@ -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<string>('MANA_CORE_AUTH_URL') ||
process.env.MANA_CORE_AUTH_URL ||
'http://localhost:3001'
);
}
private getServiceKey(): string {
return (
this.options?.serviceKey ||
this.configService?.get<string>('MANA_CORE_SERVICE_KEY') ||
process.env.MANA_CORE_SERVICE_KEY ||
''
);
}
private getAppId(): string {
return (
this.options?.appId ||
this.configService?.get<string>('APP_ID') ||
process.env.APP_ID ||
''
);
}
async validateCredits(
userId: string,
operation: string,
@ -56,10 +81,11 @@ export class CreditClientService {
}
async getBalance(userId: string): Promise<CreditBalance> {
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<CreditBalance>(
`${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<string, any>
): Promise<boolean> {
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<string, any>
): Promise<boolean> {
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,
};
}
}

View file

@ -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 <token>` 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

View file

@ -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"
}

View file

@ -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;
}
);

View file

@ -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<boolean> {
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<string>('NODE_ENV') === 'development';
const bypassAuth = this.configService.get<string>('DEV_BYPASS_AUTH') === 'true';
return isDev && bypassAuth;
}
/**
* Get development user data
*/
private getDevUser(): CurrentUserData {
return {
userId: this.configService.get<string>('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<CurrentUserData> {
const authUrl =
this.configService.get<string>('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;
}
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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"]
}

194
pnpm-lock.yaml generated
View file

@ -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)