mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
Merge branch 'dev' into till-dev
This commit is contained in:
commit
660cbd654f
21 changed files with 552 additions and 1679 deletions
|
|
@ -67,17 +67,9 @@ CHAT_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
|
|||
DEV_BYPASS_AUTH=true
|
||||
DEV_USER_ID=00000000-0000-0000-0000-000000000000
|
||||
|
||||
# Google Gemini API (primary - fast & cost-effective)
|
||||
GOOGLE_GENAI_API_KEY=AIzaSyApsYQXxN6PuXpF8-7j6MonCACwS0ZxNRc
|
||||
|
||||
# Azure OpenAI (fallback)
|
||||
AZURE_OPENAI_ENDPOINT=https://memoroseopenai.openai.azure.com
|
||||
AZURE_OPENAI_API_KEY=3082103c9b0d4270a795686ccaa89921
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
||||
# Chat Supabase (if using Supabase for chat data)
|
||||
CHAT_SUPABASE_URL=https://your-chat-project.supabase.co
|
||||
CHAT_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
# OpenRouter API (all AI models via OpenRouter)
|
||||
# Get your API key at https://openrouter.ai/keys
|
||||
OPENROUTER_API_KEY=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
|
||||
|
||||
# ============================================
|
||||
# MAERCHENZAUBER PROJECT
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ function getAuthUrl(): string {
|
|||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Get backend URL dynamically at runtime
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
return injectedUrl || 'http://localhost:3014';
|
||||
}
|
||||
return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
|
@ -27,7 +37,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: getAuthUrl(),
|
||||
backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ pnpm dev:chat:mobile # Start mobile app
|
|||
pnpm dev:chat:web # Start web app
|
||||
pnpm dev:chat:landing # Start landing page
|
||||
pnpm dev:chat:backend # Start backend server
|
||||
pnpm dev:chat:full # Start backend + web + auth together
|
||||
```
|
||||
|
||||
### Mobile App (chat/apps/mobile)
|
||||
|
|
@ -43,6 +44,9 @@ pnpm build:prod # Build production version
|
|||
pnpm start:dev # Start with hot reload
|
||||
pnpm build # Build for production
|
||||
pnpm start:prod # Start production server
|
||||
pnpm db:push # Push schema to database
|
||||
pnpm db:seed # Seed AI models
|
||||
pnpm db:studio # Open Drizzle Studio
|
||||
```
|
||||
|
||||
### Web App (chat/apps/web)
|
||||
|
|
@ -66,7 +70,8 @@ pnpm preview # Preview production build
|
|||
- **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router
|
||||
- **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4
|
||||
- **Landing**: Astro 5.16, Tailwind CSS
|
||||
- **Backend**: NestJS 10, OpenRouter/Gemini AI, Supabase
|
||||
- **Backend**: NestJS 10, OpenRouter AI, Drizzle ORM, PostgreSQL
|
||||
- **Auth**: Mana Core Auth (JWT)
|
||||
- **Types**: TypeScript 5.x
|
||||
|
||||
## Architecture
|
||||
|
|
@ -75,34 +80,45 @@ pnpm preview # Preview production build
|
|||
|
||||
| Endpoint | Method | Description |
|
||||
| --------------------------------- | ------ | --------------------------- |
|
||||
| `/api/health` | GET | Health check |
|
||||
| `/api/chat/models` | GET | List available AI models |
|
||||
| `/api/chat/completions` | POST | Create chat completion |
|
||||
| `/api/conversations` | GET | List user conversations |
|
||||
| `/api/conversations/:id` | GET | Get conversation details |
|
||||
| `/api/conversations/:id/messages` | GET | Get conversation messages |
|
||||
| `/api/conversations` | POST | Create new conversation |
|
||||
| `/api/conversations/:id/messages` | POST | Add message to conversation |
|
||||
| `/api/v1/health` | GET | Health check |
|
||||
| `/api/v1/chat/models` | GET | List available AI models |
|
||||
| `/api/v1/chat/completions` | POST | Create chat completion |
|
||||
| `/api/v1/conversations` | GET | List user conversations |
|
||||
| `/api/v1/conversations/:id` | GET | Get conversation details |
|
||||
| `/api/v1/conversations/:id/messages` | GET | Get conversation messages |
|
||||
| `/api/v1/conversations` | POST | Create new conversation |
|
||||
| `/api/v1/conversations/:id/messages` | POST | Add message to conversation |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Backend (.env)
|
||||
|
||||
```
|
||||
OPENROUTER_API_KEY=... # Get at https://openrouter.ai/keys
|
||||
GOOGLE_GENAI_API_KEY=... # Optional: For Gemini models
|
||||
SUPABASE_URL=https://...
|
||||
SUPABASE_SERVICE_KEY=...
|
||||
```env
|
||||
# Required - All AI models via OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxx # Get at https://openrouter.ai/keys
|
||||
|
||||
# Database (uses shared Docker PostgreSQL)
|
||||
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
|
||||
|
||||
# Auth
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Server
|
||||
PORT=3002
|
||||
DEV_BYPASS_AUTH=true # Optional: Skip auth in development
|
||||
```
|
||||
|
||||
#### Mobile (.env)
|
||||
|
||||
```env
|
||||
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
```
|
||||
EXPO_PUBLIC_SUPABASE_URL=https://...
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
|
||||
#### Web (.env)
|
||||
|
||||
```env
|
||||
PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
PUBLIC_BACKEND_URL=http://localhost:3002
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
|
@ -113,39 +129,43 @@ EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
|||
- **Styling**: Tailwind CSS everywhere
|
||||
- **Formatting**: 100 char line limit, 2 space tabs, single quotes
|
||||
|
||||
## AI Models Available
|
||||
## AI Models Available (via OpenRouter)
|
||||
|
||||
### OpenRouter Models (Recommended)
|
||||
All models are accessed through OpenRouter, providing access to 100+ models with a single API key.
|
||||
|
||||
| Model ID | Name | Price | Best For |
|
||||
| -------- | ---- | ----- | -------- |
|
||||
| ...440201 | Llama 3.1 8B | $0.05/M | Everyday tasks, cheap |
|
||||
| ...440201 | Llama 3.1 8B | $0.05/M | Everyday tasks (default) |
|
||||
| ...440202 | Llama 3.1 70B | $0.35/M | Complex reasoning |
|
||||
| ...440203 | DeepSeek V3 | $0.14/M | Reasoning at low cost |
|
||||
| ...440204 | Mistral Small | $0.10/M | General tasks |
|
||||
| ...440205 | Claude 3.5 Sonnet | $3/M | Best quality |
|
||||
| ...440206 | GPT-4o Mini | $0.15/M | Balanced performance |
|
||||
|
||||
### Google Gemini Models
|
||||
## Quick Start
|
||||
|
||||
| Model ID | Name | Description | Default |
|
||||
| -------- | ---- | ----------- | ------- |
|
||||
| ...440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes |
|
||||
| ...440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No |
|
||||
| ...440103 | Gemini 2.5 Pro | Most capable model | No |
|
||||
|
||||
## OpenRouter Setup
|
||||
|
||||
To enable OpenRouter models:
|
||||
|
||||
- [ ] Get API key at https://openrouter.ai/keys
|
||||
- [ ] Add `OPENROUTER_API_KEY=sk-or-v1-xxx` to `apps/chat/apps/backend/.env`
|
||||
- [ ] Re-seed database: `pnpm --filter @chat/backend db:seed`
|
||||
- [ ] Test: `pnpm dev:chat:backend`
|
||||
1. **Get OpenRouter API key** at https://openrouter.ai/keys
|
||||
2. **Create `.env`** in `apps/chat/apps/backend/`:
|
||||
```env
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxx
|
||||
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
PORT=3002
|
||||
```
|
||||
3. **Start services**:
|
||||
```bash
|
||||
pnpm docker:up # Start PostgreSQL
|
||||
pnpm dev:chat:full # Start auth + backend + web
|
||||
```
|
||||
4. **Seed database** (first time only):
|
||||
```bash
|
||||
pnpm --filter @chat/backend db:push
|
||||
pnpm --filter @chat/backend db:seed
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Security**: API keys are stored in the backend only - never in client apps
|
||||
2. **Authentication**: Uses Supabase Auth, shared with Mana Core ecosystem
|
||||
3. **Database**: Supabase PostgreSQL with RLS policies
|
||||
4. **Deployment**: Backend runs on port 3001 by default
|
||||
2. **Authentication**: Uses Mana Core Auth (JWT tokens)
|
||||
3. **Database**: PostgreSQL with Drizzle ORM (uses shared Docker container)
|
||||
4. **Deployment**: Backend runs on port 3002
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
# OpenRouter Configuration (Recommended - multi-model access)
|
||||
# OpenRouter Configuration (Required)
|
||||
# Get your API key at https://openrouter.ai/keys
|
||||
# All AI models are accessed through OpenRouter
|
||||
OPENROUTER_API_KEY=your-openrouter-api-key
|
||||
|
||||
# Google Gemini Configuration
|
||||
GOOGLE_GENAI_API_KEY=your-google-api-key
|
||||
|
||||
# Azure OpenAI Configuration (Optional)
|
||||
AZURE_OPENAI_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com
|
||||
AZURE_OPENAI_API_KEY=your-api-key-here
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
||||
# Mana Core Auth Configuration
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
DATABASE_URL=postgresql://chat:password@localhost:5432/chat
|
||||
# Uses shared Docker PostgreSQL with separate 'chat' database
|
||||
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/chat
|
||||
|
||||
# Server Configuration
|
||||
PORT=3002
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
"docker:clean": "docker compose down -v --rmi local"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@manacore/shared-errors": "workspace:*",
|
||||
"@manacore/shared-nestjs-auth": "workspace:*",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Injectable, Inject, Logger } from '@nestjs/common';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { AsyncResult, ok, err, ValidationError, ServiceError } from '@manacore/shared-errors';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import OpenAI from 'openai';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { Database } from '../db/connection';
|
||||
|
|
@ -14,37 +13,14 @@ import type { ChatCompletionResponseDto } from './dto/chat-completion.dto';
|
|||
@Injectable()
|
||||
export class ChatService {
|
||||
private readonly logger = new Logger(ChatService.name);
|
||||
// Azure OpenAI config
|
||||
private readonly azureApiKey: string;
|
||||
private readonly azureEndpoint: string;
|
||||
private readonly azureApiVersion: string;
|
||||
// Google Gemini config
|
||||
private readonly geminiClient: GoogleGenerativeAI | null = null;
|
||||
// OpenRouter config
|
||||
// OpenRouter config (primary provider)
|
||||
private readonly openRouterClient: OpenAI | null = null;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@Inject(DATABASE_CONNECTION) private readonly db: Database
|
||||
) {
|
||||
// Azure OpenAI setup
|
||||
this.azureApiKey = this.configService.get<string>('AZURE_OPENAI_API_KEY') || '';
|
||||
this.azureEndpoint =
|
||||
this.configService.get<string>('AZURE_OPENAI_ENDPOINT') ||
|
||||
'https://memoroseopenai.openai.azure.com';
|
||||
this.azureApiVersion =
|
||||
this.configService.get<string>('AZURE_OPENAI_API_VERSION') || '2024-12-01-preview';
|
||||
|
||||
// Google Gemini setup
|
||||
const geminiApiKey = this.configService.get<string>('GOOGLE_GENAI_API_KEY');
|
||||
if (geminiApiKey) {
|
||||
this.geminiClient = new GoogleGenerativeAI(geminiApiKey);
|
||||
this.logger.log('Google Gemini client initialized');
|
||||
} else {
|
||||
this.logger.warn('GOOGLE_GENAI_API_KEY is not set - Gemini models unavailable');
|
||||
}
|
||||
|
||||
// OpenRouter setup
|
||||
// OpenRouter setup (primary and only provider)
|
||||
const openRouterApiKey = this.configService.get<string>('OPENROUTER_API_KEY');
|
||||
if (openRouterApiKey) {
|
||||
this.openRouterClient = new OpenAI({
|
||||
|
|
@ -57,11 +33,7 @@ export class ChatService {
|
|||
});
|
||||
this.logger.log('OpenRouter client initialized');
|
||||
} else {
|
||||
this.logger.warn('OPENROUTER_API_KEY is not set - OpenRouter models unavailable');
|
||||
}
|
||||
|
||||
if (!this.azureApiKey) {
|
||||
this.logger.warn('AZURE_OPENAI_API_KEY is not set - Azure models unavailable');
|
||||
this.logger.error('OPENROUTER_API_KEY is not set - Chat will not work!');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,176 +72,8 @@ export class ChatService {
|
|||
this.logger.log(`User ${userId} creating chat completion with model ${dto.modelId}`);
|
||||
}
|
||||
|
||||
// Route to appropriate provider
|
||||
if (model.provider === 'gemini') {
|
||||
return this.createGeminiCompletion(model, dto);
|
||||
} else if (model.provider === 'openrouter') {
|
||||
return this.createOpenRouterCompletion(model, dto);
|
||||
} else {
|
||||
return this.createAzureCompletion(model, dto);
|
||||
}
|
||||
}
|
||||
|
||||
private async createGeminiCompletion(
|
||||
model: Model,
|
||||
dto: ChatCompletionDto
|
||||
): AsyncResult<ChatCompletionResponseDto> {
|
||||
if (!this.geminiClient) {
|
||||
return err(ServiceError.externalError('Google Gemini', 'Gemini client not configured'));
|
||||
}
|
||||
|
||||
const params = model.parameters as {
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
} | null;
|
||||
|
||||
const modelName = params?.model || 'gemini-2.5-flash';
|
||||
const temperature = dto.temperature ?? params?.temperature ?? 0.7;
|
||||
const maxTokens = dto.maxTokens ?? params?.max_tokens ?? 8192;
|
||||
|
||||
this.logger.log(`Sending request to Google Gemini model: ${modelName}`);
|
||||
|
||||
try {
|
||||
const genModel = this.geminiClient.getGenerativeModel({
|
||||
model: modelName,
|
||||
generationConfig: {
|
||||
temperature,
|
||||
maxOutputTokens: maxTokens,
|
||||
},
|
||||
});
|
||||
|
||||
// Convert messages to Gemini format
|
||||
// Gemini expects alternating user/model messages, with system as first user message
|
||||
const systemMessages = dto.messages.filter((m) => m.role === 'system');
|
||||
const chatMessages = dto.messages.filter((m) => m.role !== 'system');
|
||||
|
||||
// Build history for chat (all but last message)
|
||||
const history = chatMessages.slice(0, -1).map((msg) => ({
|
||||
role: msg.role === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: msg.content }],
|
||||
}));
|
||||
|
||||
// Last message to send
|
||||
const lastMessage = chatMessages[chatMessages.length - 1];
|
||||
let userPrompt = lastMessage?.content || '';
|
||||
|
||||
// Prepend system instruction if present
|
||||
if (systemMessages.length > 0) {
|
||||
const systemPrompt = systemMessages.map((m) => m.content).join('\n');
|
||||
userPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
||||
}
|
||||
|
||||
const chat = genModel.startChat({ history });
|
||||
const result = await chat.sendMessage(userPrompt);
|
||||
const response = result.response;
|
||||
const messageContent = response.text();
|
||||
|
||||
if (!messageContent) {
|
||||
this.logger.warn('No message content in Gemini response');
|
||||
return err(ServiceError.generationFailed('Google Gemini', 'No response generated'));
|
||||
}
|
||||
|
||||
// Gemini provides usage metadata
|
||||
const usageMetadata = response.usageMetadata;
|
||||
|
||||
return ok({
|
||||
content: messageContent,
|
||||
usage: {
|
||||
prompt_tokens: usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: usageMetadata?.totalTokenCount || 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error calling Google Gemini API', error);
|
||||
return err(
|
||||
ServiceError.generationFailed(
|
||||
'Google Gemini',
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createAzureCompletion(
|
||||
model: Model,
|
||||
dto: ChatCompletionDto
|
||||
): AsyncResult<ChatCompletionResponseDto> {
|
||||
const params = model.parameters as {
|
||||
deployment?: string;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
} | null;
|
||||
|
||||
const deployment = params?.deployment || 'gpt-4o-mini-se';
|
||||
const temperature = dto.temperature ?? params?.temperature ?? 0.7;
|
||||
const maxTokens = dto.maxTokens ?? params?.max_tokens ?? 1000;
|
||||
|
||||
// Prepare request body
|
||||
const requestBody: Record<string, unknown> = {
|
||||
messages: dto.messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
})),
|
||||
};
|
||||
|
||||
// Model-specific parameters
|
||||
const isGPTOModel = deployment.includes('gpt-o') || deployment.includes('gpt-4o');
|
||||
|
||||
if (!isGPTOModel) {
|
||||
requestBody.max_tokens = maxTokens;
|
||||
requestBody.temperature = temperature;
|
||||
}
|
||||
|
||||
const url = `${this.azureEndpoint}/openai/deployments/${deployment}/chat/completions?api-version=${this.azureApiVersion}`;
|
||||
|
||||
this.logger.log(`Sending request to Azure OpenAI: ${url}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': this.azureApiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(`API error: ${response.status} - ${errorText}`);
|
||||
return err(ServiceError.externalError('Azure OpenAI', `API error: ${response.status}`));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const messageContent = data.choices?.[0]?.message?.content;
|
||||
|
||||
if (!messageContent) {
|
||||
this.logger.warn('No message content in response');
|
||||
return err(ServiceError.generationFailed('Azure OpenAI', 'No response generated'));
|
||||
}
|
||||
|
||||
return ok({
|
||||
content: messageContent,
|
||||
usage: {
|
||||
prompt_tokens: data.usage?.prompt_tokens || 0,
|
||||
completion_tokens: data.usage?.completion_tokens || 0,
|
||||
total_tokens: data.usage?.total_tokens || 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error calling Azure OpenAI API', error);
|
||||
return err(
|
||||
ServiceError.generationFailed(
|
||||
'Azure OpenAI',
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
// All models go through OpenRouter
|
||||
return this.createOpenRouterCompletion(model, dto);
|
||||
}
|
||||
|
||||
private async createOpenRouterCompletion(
|
||||
|
|
|
|||
|
|
@ -33,49 +33,7 @@ async function seed() {
|
|||
|
||||
const modelData = [
|
||||
// ============================================
|
||||
// Google Gemini Models (Primary - fast & cost-effective)
|
||||
// ============================================
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440101',
|
||||
name: 'Gemini 2.5 Flash',
|
||||
description: 'Fastest & most cost-effective - ideal for everyday tasks',
|
||||
provider: 'gemini',
|
||||
parameters: {
|
||||
model: 'gemini-2.5-flash',
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: true, // Default model
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440102',
|
||||
name: 'Gemini 2.0 Flash-Lite',
|
||||
description: 'Ultra-fast lightweight model - minimal latency',
|
||||
provider: 'gemini',
|
||||
parameters: {
|
||||
model: 'gemini-2.0-flash-lite',
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440103',
|
||||
name: 'Gemini 2.5 Pro',
|
||||
description: 'Most powerful Gemini - complex reasoning & analysis',
|
||||
provider: 'gemini',
|
||||
parameters: {
|
||||
model: 'gemini-2.5-pro',
|
||||
temperature: 0.7,
|
||||
max_tokens: 16384,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
// ============================================
|
||||
// OpenRouter Models (Multi-provider, cost-effective)
|
||||
// OpenRouter Models (All models via OpenRouter)
|
||||
// ============================================
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440201',
|
||||
|
|
@ -88,7 +46,7 @@ async function seed() {
|
|||
max_tokens: 4096,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
isDefault: true, // Default model - fast and cost-effective
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440202',
|
||||
|
|
@ -155,101 +113,6 @@ async function seed() {
|
|||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
// ============================================
|
||||
// Azure OpenAI GPT-5 Family (Inactive - no deployment)
|
||||
// ============================================
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'GPT-5 Mini',
|
||||
description: 'Fast & cost-effective - best for everyday tasks',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
deployment: 'gpt-5-mini',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440002',
|
||||
name: 'GPT-5 Nano',
|
||||
description: 'Ultra-fast responses with low latency',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
deployment: 'gpt-5-nano',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440003',
|
||||
name: 'GPT-5 Chat',
|
||||
description: 'Advanced multimodal conversations with emotional intelligence',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 16384,
|
||||
deployment: 'gpt-5-chat',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440004',
|
||||
name: 'GPT-5',
|
||||
description: 'Most powerful LLM - logic-heavy & multi-step tasks',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 32768,
|
||||
deployment: 'gpt-5',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440005',
|
||||
name: 'GPT-5 Codex',
|
||||
description: 'Optimized for coding & front-end development',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 32768,
|
||||
deployment: 'gpt-5-codex',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
// O-Series Reasoning Models (Inactive - no deployment)
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440006',
|
||||
name: 'o4-mini',
|
||||
description: 'Latest reasoning model - best for STEM & code',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 1, // Reasoning models work best with temp=1
|
||||
max_tokens: 16384,
|
||||
deployment: 'o4-mini',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440007',
|
||||
name: 'o3',
|
||||
description: 'Advanced reasoning - 20% fewer errors than o1',
|
||||
provider: 'azure',
|
||||
parameters: {
|
||||
temperature: 1,
|
||||
max_tokens: 32768,
|
||||
deployment: 'o3',
|
||||
},
|
||||
isActive: false,
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
|
||||
await db.insert(models).values(modelData);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ function getAuthUrl(): string {
|
|||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Get backend URL dynamically at runtime
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
return injectedUrl || 'http://localhost:3002';
|
||||
}
|
||||
return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3002';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
|
@ -27,7 +37,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: getAuthUrl(),
|
||||
backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,17 @@
|
|||
let showVersionsModal = $state(false);
|
||||
let showDocumentPanel = $state(true);
|
||||
|
||||
// Track current request to prevent race conditions
|
||||
let currentLoadId = $state(0);
|
||||
// Track current request to prevent race conditions (not reactive to avoid effect loops)
|
||||
let currentLoadId = 0;
|
||||
let lastLoadedConversationId = '';
|
||||
|
||||
const conversationId = $derived($page.params.id ?? '');
|
||||
const isDocumentMode = $derived(conversation?.documentMode ?? false);
|
||||
|
||||
// React to conversationId changes with race condition protection
|
||||
$effect(() => {
|
||||
if (conversationId) {
|
||||
if (conversationId && conversationId !== lastLoadedConversationId) {
|
||||
lastLoadedConversationId = conversationId;
|
||||
loadData(conversationId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ function getAuthUrl(): string {
|
|||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Get backend URL dynamically at runtime
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
return injectedUrl || 'http://localhost:3017';
|
||||
}
|
||||
return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3017';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
|
@ -26,7 +36,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: getAuthUrl(),
|
||||
backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { browser } from '$app/environment';
|
|||
import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
import type { UserData } from '@manacore/shared-auth';
|
||||
import { MANA_AUTH_URL } from '$lib/api/config';
|
||||
const BACKEND_URL = 'http://localhost:3015';
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -15,7 +16,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: MANA_AUTH_URL,
|
||||
backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq, and, count } from 'drizzle-orm';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { CreditClientService } from '@mana-core/nestjs-integration';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { Database } from '../db/connection';
|
||||
|
|
@ -18,7 +18,6 @@ import { ReplicateService, GenerationParams } from './replicate.service';
|
|||
import { StorageService } from '../upload/storage.service';
|
||||
import { GenerateImageDto } from './dto/generate.dto';
|
||||
|
||||
const FREE_GENERATIONS_LIMIT = 3;
|
||||
const CREDITS_PER_GENERATION = 10;
|
||||
|
||||
export interface GenerateResponse {
|
||||
|
|
@ -26,14 +25,14 @@ export interface GenerateResponse {
|
|||
status: string;
|
||||
image?: Image;
|
||||
creditsUsed?: number;
|
||||
freeGenerationsRemaining?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GenerateService {
|
||||
private readonly logger = new Logger(GenerateService.name);
|
||||
private readonly webhookBaseUrl: string;
|
||||
private readonly isStaging: boolean;
|
||||
private readonly isProduction: boolean;
|
||||
private readonly canUseWebhooks: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject(DATABASE_CONNECTION) private readonly db: Database,
|
||||
|
|
@ -44,65 +43,41 @@ export class GenerateService {
|
|||
) {
|
||||
this.webhookBaseUrl =
|
||||
this.configService.get<string>('WEBHOOK_BASE_URL') || 'http://localhost:3003';
|
||||
// Freemium/credit system only enforced in staging
|
||||
this.isStaging = this.configService.get<string>('NODE_ENV') === 'staging';
|
||||
// Credit system only enforced in production
|
||||
this.isProduction = this.configService.get<string>('NODE_ENV') === 'production';
|
||||
// Replicate requires HTTPS webhooks - detect if we can use them
|
||||
this.canUseWebhooks = this.webhookBaseUrl.startsWith('https://');
|
||||
if (!this.canUseWebhooks) {
|
||||
this.logger.warn(
|
||||
`Webhook URL is not HTTPS (${this.webhookBaseUrl}). Falling back to sync mode for all generations.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of completed generations for a user
|
||||
*/
|
||||
private async getUserGenerationCount(userId: string): Promise<number> {
|
||||
const result = await this.db
|
||||
.select({ count: count() })
|
||||
.from(imageGenerations)
|
||||
.where(and(eq(imageGenerations.userId, userId), eq(imageGenerations.status, 'completed')));
|
||||
return result[0]?.count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can generate (has free generations or credits)
|
||||
* Returns: { canGenerate, isFree, freeRemaining, creditsRequired }
|
||||
* Check if user has enough credits to generate
|
||||
* Credits are only enforced in production (NODE_ENV=production)
|
||||
*/
|
||||
async checkGenerationAccess(userId: string): Promise<{
|
||||
canGenerate: boolean;
|
||||
isFree: boolean;
|
||||
freeGenerationsRemaining: number;
|
||||
creditsRequired: number;
|
||||
currentBalance?: number;
|
||||
}> {
|
||||
const completedCount = await this.getUserGenerationCount(userId);
|
||||
const freeRemaining = Math.max(0, FREE_GENERATIONS_LIMIT - completedCount);
|
||||
|
||||
// If user has free generations, they can proceed
|
||||
if (freeRemaining > 0) {
|
||||
// In development, skip credit check (users get 150 free credits on signup anyway)
|
||||
if (!this.isProduction) {
|
||||
return {
|
||||
canGenerate: true,
|
||||
isFree: true,
|
||||
freeGenerationsRemaining: freeRemaining,
|
||||
creditsRequired: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// No free generations - check credits (only in staging)
|
||||
if (!this.isStaging) {
|
||||
// In development/production without credit enforcement, allow generation
|
||||
return {
|
||||
canGenerate: true,
|
||||
isFree: false,
|
||||
freeGenerationsRemaining: 0,
|
||||
creditsRequired: CREDITS_PER_GENERATION,
|
||||
};
|
||||
}
|
||||
|
||||
// In staging, check actual credit balance
|
||||
// In production, check actual credit balance
|
||||
try {
|
||||
const balance = await this.creditClient.getBalance(userId);
|
||||
const hasEnoughCredits = balance.balance >= CREDITS_PER_GENERATION;
|
||||
|
||||
return {
|
||||
canGenerate: hasEnoughCredits,
|
||||
isFree: false,
|
||||
freeGenerationsRemaining: 0,
|
||||
creditsRequired: CREDITS_PER_GENERATION,
|
||||
currentBalance: balance.balance,
|
||||
};
|
||||
|
|
@ -111,8 +86,6 @@ export class GenerateService {
|
|||
// On error, allow generation (fail open for better UX)
|
||||
return {
|
||||
canGenerate: true,
|
||||
isFree: false,
|
||||
freeGenerationsRemaining: 0,
|
||||
creditsRequired: CREDITS_PER_GENERATION,
|
||||
};
|
||||
}
|
||||
|
|
@ -123,7 +96,7 @@ export class GenerateService {
|
|||
*/
|
||||
async generateImage(userId: string, dto: GenerateImageDto): Promise<GenerateResponse> {
|
||||
try {
|
||||
// Check if user can generate (freemium/credit check)
|
||||
// Check if user has enough credits (only enforced in production)
|
||||
const access = await this.checkGenerationAccess(userId);
|
||||
|
||||
if (!access.canGenerate) {
|
||||
|
|
@ -168,7 +141,6 @@ export class GenerateService {
|
|||
.returning();
|
||||
|
||||
const generation = generationResult[0];
|
||||
const isFreeGeneration = access.isFree;
|
||||
|
||||
// Build generation params
|
||||
const generationParams: GenerationParams = {
|
||||
|
|
@ -186,25 +158,29 @@ export class GenerateService {
|
|||
style: dto.style,
|
||||
};
|
||||
|
||||
// If waitForResult is true, use synchronous generation with polling
|
||||
if (dto.waitForResult) {
|
||||
// Use sync mode if:
|
||||
// 1. Client explicitly requested waitForResult
|
||||
// 2. Webhooks are not available (no HTTPS URL)
|
||||
const useSyncMode = dto.waitForResult || !this.canUseWebhooks;
|
||||
|
||||
if (useSyncMode) {
|
||||
if (!this.canUseWebhooks && !dto.waitForResult) {
|
||||
this.logger.debug('Using sync mode because webhooks are not available (no HTTPS)');
|
||||
}
|
||||
|
||||
const result = await this.generateSync(generation, generationParams);
|
||||
|
||||
// Consume credits after successful generation (if not free)
|
||||
if (result.status === 'completed' && !isFreeGeneration && this.isStaging) {
|
||||
// Consume credits after successful generation (only in production)
|
||||
if (result.status === 'completed' && this.isProduction) {
|
||||
await this.consumeCreditsForGeneration(userId, generation.id);
|
||||
result.creditsUsed = CREDITS_PER_GENERATION;
|
||||
}
|
||||
|
||||
// Add free generations remaining info
|
||||
const newAccess = await this.checkGenerationAccess(userId);
|
||||
result.freeGenerationsRemaining = newAccess.freeGenerationsRemaining;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Otherwise use async generation with webhook (credits consumed on webhook completion)
|
||||
return this.generateAsync(generation, model, generationParams, isFreeGeneration);
|
||||
return this.generateAsync(generation, model, generationParams);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException || error instanceof HttpException) {
|
||||
throw error;
|
||||
|
|
@ -329,8 +305,7 @@ export class GenerateService {
|
|||
private async generateAsync(
|
||||
generation: ImageGeneration,
|
||||
model: any,
|
||||
params: GenerationParams,
|
||||
isFreeGeneration: boolean
|
||||
params: GenerationParams
|
||||
): Promise<GenerateResponse> {
|
||||
try {
|
||||
const webhookUrl = `${this.webhookBaseUrl}/api/generate/webhook`;
|
||||
|
|
@ -342,15 +317,12 @@ export class GenerateService {
|
|||
webhookUrl
|
||||
);
|
||||
|
||||
// Update generation with prediction ID and free generation flag (in metadata)
|
||||
// Update generation with prediction ID
|
||||
await this.db
|
||||
.update(imageGenerations)
|
||||
.set({
|
||||
replicatePredictionId: prediction.id,
|
||||
status: 'processing',
|
||||
// Store isFreeGeneration in a way that can be retrieved in webhook
|
||||
// We'll use the errorMessage field temporarily for metadata (cleared on success)
|
||||
errorMessage: isFreeGeneration ? 'FREE_GENERATION' : null,
|
||||
})
|
||||
.where(eq(imageGenerations.id, generation.id));
|
||||
|
||||
|
|
@ -514,7 +486,7 @@ export class GenerateService {
|
|||
|
||||
async handleWebhook(body: any): Promise<{ received: boolean }> {
|
||||
try {
|
||||
const { id, status, output, error, metrics } = body;
|
||||
const { id, status, output, error } = body;
|
||||
|
||||
if (!id) {
|
||||
return { received: false };
|
||||
|
|
@ -534,14 +506,11 @@ export class GenerateService {
|
|||
|
||||
const generation = result[0];
|
||||
|
||||
// Check if this was a free generation (stored in errorMessage field temporarily)
|
||||
const isFreeGeneration = generation.errorMessage === 'FREE_GENERATION';
|
||||
|
||||
if (status === 'succeeded' && output) {
|
||||
await this.processCompletedGeneration(generation, output);
|
||||
|
||||
// Consume credits for paid generations in staging
|
||||
if (!isFreeGeneration && this.isStaging) {
|
||||
// Consume credits in production
|
||||
if (this.isProduction) {
|
||||
await this.consumeCreditsForGeneration(generation.userId, generation.id);
|
||||
}
|
||||
} else if (status === 'failed') {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@
|
|||
* API Client for Picture Backend
|
||||
* Replaces direct Supabase calls with backend API calls.
|
||||
*
|
||||
* Token handling: Uses authStore.getValidToken() which automatically
|
||||
* refreshes expired tokens before making requests.
|
||||
* Token handling:
|
||||
* - Uses authStore.getValidToken() which automatically refreshes expired tokens
|
||||
* - The fetch interceptor (setupFetchInterceptor) handles 401 responses by refreshing and retrying
|
||||
* - If refresh fails, the request fails and user should be redirected to login
|
||||
*/
|
||||
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3003';
|
||||
const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3006';
|
||||
|
||||
type FetchOptions = {
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { browser } from '$app/environment';
|
|||
import { env } from '$env/dynamic/public';
|
||||
|
||||
const MANA_AUTH_URL = env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
const BACKEND_URL = env.PUBLIC_BACKEND_URL || 'http://localhost:3006';
|
||||
|
||||
export interface UserData {
|
||||
id: string;
|
||||
|
|
@ -28,7 +29,10 @@ async function getAuthService() {
|
|||
if (!_authService) {
|
||||
try {
|
||||
const { initializeWebAuth } = await import('@manacore/shared-auth');
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: MANA_AUTH_URL,
|
||||
backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ function getAuthUrl(): string {
|
|||
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Get backend URL dynamically at runtime
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
return injectedUrl || 'http://localhost:3018';
|
||||
}
|
||||
return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
}
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
|
@ -27,7 +37,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: getAuthUrl() });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: getAuthUrl(),
|
||||
backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { initializeWebAuth } from '@manacore/shared-auth';
|
|||
import type { UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
const BACKEND_URL = 'http://localhost:3007';
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
|
|
@ -18,7 +18,10 @@ let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null =
|
|||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
const auth = initializeWebAuth({
|
||||
baseUrl: MANA_AUTH_URL,
|
||||
backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses
|
||||
});
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
|
|
|
|||
973
doc_nils.md
973
doc_nils.md
|
|
@ -1,973 +0,0 @@
|
|||
why do i need to set it in envs?
|
||||
|
||||
⏺ Great questions! Let me explain the Docker Compose deployment architecture:
|
||||
|
||||
How This Setup Works
|
||||
|
||||
1. Containers Are Always Running
|
||||
|
||||
Yes! The Docker containers run continuously on your staging server:
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Staging Server (Ubuntu) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Docker Compose Network │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Postgres │ │ Redis │ │ │
|
||||
│ │ │ :5432 │ │ :6379 │ │ │
|
||||
│ │ └──────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Auth │ │ Chat │ │ │
|
||||
│ │ │ :3001 │ │ :3002 │ │ │
|
||||
│ │ └──────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ ManaDeck │ │ Nginx │ │ │
|
||||
│ │ │ :3003 │ │ :80 :443 │ │ │
|
||||
│ │ └──────────┘ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
↑
|
||||
│ Users access via HTTP/HTTPS
|
||||
│ Nginx routes to backend services
|
||||
|
||||
2. Data Persistence - YES!
|
||||
|
||||
The postgres data is persistent across redeployments. Look at the
|
||||
docker-compose.staging.yml:
|
||||
|
||||
postgres:
|
||||
volumes: - postgres_data:/var/lib/postgresql/data # ← Persistent volume
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
name: manacore-postgres-staging # ← Named volume stored on server disk
|
||||
|
||||
What happens on redeploy:
|
||||
|
||||
1. docker compose pull - Downloads new backend images
|
||||
2. docker compose up -d - Recreates containers with new code
|
||||
3. Postgres volume stays untouched - Your data survives!
|
||||
|
||||
# First deployment
|
||||
|
||||
docker compose up -d
|
||||
|
||||
# Creates: postgres_data volume →
|
||||
|
||||
/var/lib/docker/volumes/manacore-postgres-staging
|
||||
|
||||
# After redeploy (10 times later)
|
||||
|
||||
docker compose up -d
|
||||
|
||||
# Uses: SAME postgres_data volume → Your data is still there!
|
||||
|
||||
3. Risks
|
||||
|
||||
Current Risks:
|
||||
|
||||
❌ No backups - If the server disk fails, all data is lost❌ No password set -
|
||||
Postgres won't start without POSTGRES_PASSWORD❌ Single server - If it goes
|
||||
down, everything is offline⚠️ Docker volume only - Data lives in
|
||||
/var/lib/docker/volumes/, not easily inspectable
|
||||
|
||||
Recommended Setup:
|
||||
|
||||
# 1. Regular backups (add to server crontab)
|
||||
|
||||
0 2 \* \* \* docker exec manacore-postgres-staging pg*dump -U postgres manacore >
|
||||
/backup/db*$(date +\%Y\%m\%d).sql
|
||||
|
||||
# 2. Or use Supabase for production data (you're already using it!)
|
||||
|
||||
# Keep postgres in staging for testing, but production uses Supabase's managed
|
||||
|
||||
database
|
||||
|
||||
4. Why You Need POSTGRES_PASSWORD
|
||||
|
||||
The Problem:
|
||||
|
||||
Postgres refuses to start without a password for security reasons. When the
|
||||
container starts for the first time, it initializes the database and needs to
|
||||
set the postgres superuser password.
|
||||
|
||||
How to Set It:
|
||||
|
||||
⏺ Read(.github/workflows/cd-staging.yml)
|
||||
⎿ Read 40 lines
|
||||
|
||||
⏺ The workflow creates the .env file from GitHub Secrets. Here's what you need:
|
||||
|
||||
How to Fix This
|
||||
|
||||
Step 1: Set GitHub Secrets
|
||||
|
||||
Go to your GitHub repository:
|
||||
Settings → Secrets and variables → Actions → New repository secret
|
||||
|
||||
You need to add these secrets:
|
||||
|
||||
STAGING_POSTGRES_HOST=postgres
|
||||
STAGING_POSTGRES_PORT=5432
|
||||
STAGING_POSTGRES_DB=manacore
|
||||
STAGING_POSTGRES_USER=postgres
|
||||
STAGING_POSTGRES_PASSWORD=<choose-a-secure-password>
|
||||
|
||||
STAGING_REDIS_HOST=redis
|
||||
STAGING_REDIS_PORT=6379
|
||||
STAGING_REDIS_PASSWORD=<choose-a-secure-password>
|
||||
|
||||
STAGING_MANA_SERVICE_URL=http://mana-core-auth:3001
|
||||
STAGING_JWT_SECRET=<generate-a-secret>
|
||||
STAGING_JWT_PUBLIC_KEY=<your-public-key>
|
||||
STAGING_JWT_PRIVATE_KEY=<your-private-key>
|
||||
|
||||
STAGING_SUPABASE_URL=<your-supabase-url>
|
||||
STAGING_SUPABASE_ANON_KEY=<your-supabase-anon-key>
|
||||
STAGING_SUPABASE_SERVICE_ROLE_KEY=<your-supabase-service-role-key>
|
||||
|
||||
STAGING_AZURE_OPENAI_ENDPOINT=<your-azure-endpoint>
|
||||
STAGING_AZURE_OPENAI_API_KEY=<your-azure-api-key>
|
||||
|
||||
Step 2: For Testing NOW (Temporary)
|
||||
|
||||
Since you're on the server already, manually create the .env file:
|
||||
|
||||
# You're already here: ~/manacore-staging
|
||||
|
||||
nano .env
|
||||
|
||||
Add this content:
|
||||
|
||||
# Database
|
||||
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=manacore
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=staging_password_123 # ← Use any password you want
|
||||
|
||||
# Redis
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=redis_password_123
|
||||
|
||||
# Mana Core Auth
|
||||
|
||||
MANA_SERVICE_URL=http://mana-core-auth:3001
|
||||
JWT_SECRET=your_jwt_secret_here
|
||||
JWT_PUBLIC_KEY=your_public_key_here
|
||||
JWT_PRIVATE_KEY=your_private_key_here
|
||||
|
||||
# Supabase
|
||||
|
||||
SUPABASE_URL=your_supabase_url
|
||||
SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
|
||||
|
||||
# Azure OpenAI
|
||||
|
||||
AZURE_OPENAI_ENDPOINT=your_azure_endpoint
|
||||
AZURE_OPENAI_API_KEY=your_azure_api_key
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
||||
# Environment
|
||||
|
||||
NODE_ENV=staging
|
||||
|
||||
Save and exit (Ctrl+X, Y, Enter), then:
|
||||
|
||||
# Restart postgres with the new password
|
||||
|
||||
docker compose down postgres
|
||||
docker compose up -d postgres
|
||||
|
||||
# Check if it's healthy now
|
||||
|
||||
docker compose ps
|
||||
docker compose logs --tail=20 postgres
|
||||
|
||||
Once it works, you should see:
|
||||
✅ manacore-postgres-staging Up healthy
|
||||
|
||||
Would you like me to help you generate secure passwords for the production
|
||||
GitHub secrets?
|
||||
|
||||
> Would you like me to help you generate secure passwords for the production
|
||||
> GitHub secrets? -> yes write me an script which outputs this so i can copy
|
||||
> paste it to github secrets
|
||||
|
||||
⏺ Write(scripts/generate-staging-secrets.sh)
|
||||
⎿ Wrote 138 lines to scripts/generate-staging-secrets.sh
|
||||
#!/bin/bash # Generate Staging Secrets for GitHub # Run this script and copy the output to GitHub Secrets
|
||||
set -e
|
||||
echo "================================================"
|
||||
echo " STAGING SECRETS GENERATOR"
|
||||
echo "================================================"
|
||||
… +128 lines (ctrl+o to expand)
|
||||
|
||||
⏺ Bash(chmod +x /Users/wuesteon/dev/mana_universe/manacore-monorepo/scripts/generate-
|
||||
staging-secrets.sh)
|
||||
⎿ (No content)
|
||||
|
||||
⏺ Bash(/Users/wuesteon/dev/mana_universe/manacore-monorepo/scripts/generate-staging-s
|
||||
ecrets.sh)
|
||||
⎿ ================================================
|
||||
STAGING SECRETS GENERATOR
|
||||
================================================
|
||||
… +108 lines (ctrl+o to expand)
|
||||
|
||||
⏺ Perfect! 🎉 The script has generated secure secrets for you. Here's what to do:
|
||||
|
||||
Quick Copy Guide
|
||||
|
||||
For each secret, go to GitHub → Settings → Secrets and variables → Actions →
|
||||
New repository secret
|
||||
|
||||
Database Secrets:
|
||||
|
||||
Name: STAGING_POSTGRES_PASSWORD
|
||||
Value: qUHBDzlc0fMntYPdT0LXwLHnvMUdUFfp
|
||||
|
||||
Name: STAGING_REDIS_PASSWORD
|
||||
Value: OxYL7XQgPAG4HjS29UFDYEmrkfk5kJoJ
|
||||
|
||||
Name: STAGING_JWT_SECRET
|
||||
Value: ayNMdsOOlAWIjduJ1aqPe9ep9DtCzvKwgmV0qGrct4WZyXJn1NMxrTIAa6EkiijMfuh5ZyGY
|
||||
D0TcTGbkWF4g
|
||||
|
||||
Name: STAGING_JWT_PRIVATE_KEY
|
||||
Value: b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy
|
||||
NTUxOQAAACDnbEhUMtYMTPhwKuqc8DV+J0i/oyWWJ+wWw7p6YwivmwAAAJjKVMoXylTKFwAAAAtzc2g
|
||||
tZWQyNTUxOQAAACDnbEhUMtYMTPhwKuqc8DV+J0i/oyWWJ+wWw7p6YwivmwAAAEBjB/hkfSdozgvi/S
|
||||
JsvbYxA2URarKSVkgVBwqbB1+yQedsSFQy1gxM+HAq6pzwNX4nSL+jJZYn7BbDunpjCK+bAAAAFG1hb
|
||||
mFjb3JlLXN0YWdpbmctand0AQ==
|
||||
|
||||
Simple Values (copy as-is):
|
||||
|
||||
STAGING_POSTGRES_HOST = postgres
|
||||
STAGING_POSTGRES_PORT = 5432
|
||||
STAGING_POSTGRES_DB = manacore
|
||||
STAGING_POSTGRES_USER = postgres
|
||||
STAGING_REDIS_HOST = redis
|
||||
STAGING_REDIS_PORT = 6379
|
||||
STAGING_MANA_SERVICE_URL = http://mana-core-auth:3001
|
||||
STAGING_HOST = 46.224.108.214
|
||||
STAGING_USER = deploy
|
||||
|
||||
SSH Key (get from your local machine):
|
||||
|
||||
⏺ Write(scripts/get-ssh-key.sh)
|
||||
⎿ Wrote 19 lines to scripts/get-ssh-key.sh
|
||||
#!/bin/bash # Get SSH Private Key Content for GitHub Secret
|
||||
echo "================================================"
|
||||
echo " SSH PRIVATE KEY FOR STAGING_SSH_KEY"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Copy the ENTIRE output below (including BEGIN and END lines):"
|
||||
echo ""
|
||||
… +9 lines (ctrl+o to expand)
|
||||
|
||||
⏺ Bash(chmod +x
|
||||
/Users/wuesteon/dev/mana_universe/manacore-monorepo/scripts/get-ssh-key.sh &&
|
||||
/Users/wuesteon/dev/mana_universe/manacore-monorepo/scripts/get-ssh-key.s…)
|
||||
⎿ ================================================
|
||||
SSH PRIVATE KEY FOR STAGING_SSH_KEY
|
||||
================================================
|
||||
… +14 lines (ctrl+o to expand)
|
||||
|
||||
⏺ Perfect! Now for the SSH key:
|
||||
|
||||
Name: STAGING_SSH_KEY
|
||||
Value: (Copy the entire content above, including -----BEGIN and -----END lines)
|
||||
|
||||
---
|
||||
|
||||
Still Need to Fill Manually:
|
||||
|
||||
You need to add your Supabase and Azure OpenAI credentials:
|
||||
|
||||
1. STAGING_SUPABASE_URL - Get from your Supabase project settings
|
||||
2. STAGING_SUPABASE_ANON_KEY - Get from Supabase project settings
|
||||
3. STAGING_SUPABASE_SERVICE_ROLE_KEY - Get from Supabase project settings
|
||||
4. STAGING_AZURE_OPENAI_ENDPOINT - Your Azure OpenAI endpoint
|
||||
5. STAGING_AZURE_OPENAI_API_KEY - Your Azure OpenAI API key
|
||||
|
||||
---
|
||||
|
||||
Quick Test Locally First
|
||||
|
||||
Before setting all GitHub secrets, let's test with the generated passwords on
|
||||
your server:
|
||||
|
||||
# On your staging server (you're already SSH'd in)
|
||||
|
||||
nano ~/manacore-staging/.env
|
||||
|
||||
# Add/update just these lines:
|
||||
|
||||
POSTGRES_PASSWORD=qUHBDzlc0fMntYPdT0LXwLHnvMUdUFfp
|
||||
REDIS_PASSWORD=OxYL7XQgPAG4HjS29UFDYEmrkfk5kJoJ
|
||||
|
||||
# Save and restart
|
||||
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
|
||||
# Check if postgres starts successfully
|
||||
|
||||
docker compose logs --tail=20 postgres
|
||||
docker compose ps
|
||||
|
||||
Once postgres is healthy, you'll know the setup works! Then you can add all the
|
||||
secrets to GitHub for CI/CD deployments. 🚀
|
||||
|
||||
Would you like me to also create a script that shows you where to get your
|
||||
Supabase credentials?
|
||||
|
||||
> do i need to add those ->
|
||||
> STAGING_POSTGRES_HOST = postgres
|
||||
> STAGING_POSTGRES_PORT = 5432
|
||||
> STAGING_POSTGRES_DB = manacore
|
||||
> STAGING_POSTGRES_USER = postgres
|
||||
> STAGING_REDIS_HOST = redis
|
||||
> STAGING_REDIS_PORT = 6379
|
||||
> STAGING_MANA_SERVICE_URL = http://mana-core-auth:3001
|
||||
> STAGING_HOST = 46.224.108.214
|
||||
> STAGING_USER = deploy
|
||||
> ?
|
||||
|
||||
⏺ Yes! You need to add ALL of them to GitHub Secrets.
|
||||
|
||||
Even though they're not "secret" values (like postgres,
|
||||
5432), the workflow reads them from secrets.\*, so GitHub
|
||||
Actions needs them defined.
|
||||
|
||||
Looking at the workflow file
|
||||
.github/workflows/cd-staging.yml:
|
||||
|
||||
POSTGRES_HOST=${{ secrets.STAGING_POSTGRES_HOST }}
|
||||
POSTGRES_PORT=${{ secrets.STAGING_POSTGRES_PORT }}
|
||||
POSTGRES_DB=${{ secrets.STAGING_POSTGRES_DB }}
|
||||
POSTGRES_USER=${{ secrets.STAGING_POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD=${{ secrets.STAGING_POSTGRES_PASSWORD }}
|
||||
|
||||
REDIS_HOST=${{ secrets.STAGING_REDIS_HOST }}
|
||||
REDIS_PORT=${{ secrets.STAGING_REDIS_PORT }}
|
||||
REDIS_PASSWORD=${{ secrets.STAGING_REDIS_PASSWORD }}
|
||||
|
||||
MANA_SERVICE_URL=${{ secrets.STAGING_MANA_SERVICE_URL }}
|
||||
|
||||
And for SSH connection:
|
||||
ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }}
|
||||
|
||||
All of these must be GitHub secrets or the workflow will fail
|
||||
with "secret not found" errors.
|
||||
|
||||
---
|
||||
|
||||
Complete List of Secrets to Add:
|
||||
|
||||
Required (17 total):
|
||||
|
||||
✅ Database:
|
||||
|
||||
- STAGING_POSTGRES_HOST = postgres
|
||||
- STAGING_POSTGRES_PORT = 5432
|
||||
- STAGING_POSTGRES_DB = manacore
|
||||
- STAGING_POSTGRES_USER = postgres
|
||||
- STAGING_POSTGRES_PASSWORD = (generated password)
|
||||
|
||||
✅ Redis:
|
||||
|
||||
- STAGING_REDIS_HOST = redis
|
||||
- STAGING_REDIS_PORT = 6379
|
||||
- STAGING_REDIS_PASSWORD = (generated password)
|
||||
|
||||
✅ Mana Core Auth:
|
||||
|
||||
- STAGING_MANA_SERVICE_URL = http://mana-core-auth:3001
|
||||
- STAGING_JWT_SECRET = (generated secret)
|
||||
- STAGING_JWT_PUBLIC_KEY = (empty for now, or generated)
|
||||
- STAGING_JWT_PRIVATE_KEY = (generated key)
|
||||
|
||||
✅ Supabase (you fill these):
|
||||
|
||||
- STAGING_SUPABASE_URL
|
||||
- STAGING_SUPABASE_ANON_KEY
|
||||
- STAGING_SUPABASE_SERVICE_ROLE_KEY
|
||||
|
||||
✅ Azure OpenAI (you fill these):
|
||||
|
||||
- STAGING_AZURE_OPENAI_ENDPOINT
|
||||
- STAGING_AZURE_OPENAI_API_KEY
|
||||
|
||||
✅ SSH:
|
||||
|
||||
- STAGING_SSH_KEY = (your SSH private key)
|
||||
- STAGING_HOST = 46.224.108.214
|
||||
- STAGING_USER = deploy
|
||||
|
||||
---
|
||||
|
||||
Why even simple values?
|
||||
|
||||
GitHub Actions doesn't distinguish between "secret" and
|
||||
"non-secret" values. Everything passed via ${{ secrets.* }}
|
||||
must be defined in GitHub Secrets, even if it's just postgres
|
||||
or 5432.
|
||||
|
||||
This is actually good for flexibility - you can change these
|
||||
values (like switching to a different database name) without
|
||||
modifying the workflow file.
|
||||
|
||||
> cant i set them in the ci/cd file? - STAGING_POSTGRES_HOST =
|
||||
> postgres
|
||||
|
||||
- STAGING_POSTGRES_PORT = 5432
|
||||
- STAGING_POSTGRES_DB = manacore
|
||||
- STAGING_POSTGRES_USER = postgres
|
||||
|
||||
⏺ YES! Absolutely! That's actually a better practice. Only
|
||||
truly sensitive values (passwords, API keys) should be GitHub
|
||||
Secrets.
|
||||
|
||||
Let me update the workflow to hardcode the non-sensitive
|
||||
configuration values:
|
||||
|
||||
⏺ Read(.github/workflows/cd-staging.yml)
|
||||
⎿ Read 40 lines
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 11 addition
|
||||
and 11 removals
|
||||
61
|
||||
62 - name: Copy environment file
|
||||
63 run: |
|
||||
64 - # Create staging env file from - secrets
|
||||
64 + # Create staging env file (mix + of hardcoded config and secrets)
|
||||
65 cat > .env.staging << EOF
|
||||
66 - # Database
|
||||
67 - POSTGRES_HOST=${{
|
||||
- secrets.STAGING_POSTGRES_HOST }}
|
||||
68 - POSTGRES_PORT=${{
|
||||
- secrets.STAGING_POSTGRES_PORT }}
|
||||
69 - POSTGRES_DB=${{
|
||||
- secrets.STAGING_POSTGRES_DB }}
|
||||
70 - POSTGRES_USER=${{
|
||||
- secrets.STAGING_POSTGRES_USER }}
|
||||
66 + # Database - Configuration
|
||||
67 + POSTGRES_HOST=postgres
|
||||
68 + POSTGRES_PORT=5432
|
||||
69 + POSTGRES_DB=manacore
|
||||
70 + POSTGRES_USER=postgres
|
||||
71 POSTGRES_PASSWORD=${{
|
||||
secrets.STAGING_POSTGRES_PASSWORD }}
|
||||
72
|
||||
73 - # Redis
|
||||
74 - REDIS_HOST=${{
|
||||
- secrets.STAGING_REDIS_HOST }}
|
||||
75 - REDIS_PORT=${{
|
||||
- secrets.STAGING_REDIS_PORT }}
|
||||
73 + # Redis - Configuration
|
||||
74 + REDIS_HOST=redis
|
||||
75 + REDIS_PORT=6379
|
||||
76 REDIS_PASSWORD=${{
|
||||
secrets.STAGING_REDIS_PASSWORD }}
|
||||
77
|
||||
78 - # Mana Core Auth
|
||||
79 - MANA_SERVICE_URL=${{
|
||||
- secrets.STAGING_MANA_SERVICE_URL }}
|
||||
78 + # Mana Core Auth - Configuration
|
||||
79 +
|
||||
+ MANA_SERVICE_URL=http://mana-core-auth:3001
|
||||
80 JWT_SECRET=${{
|
||||
secrets.STAGING_JWT_SECRET }}
|
||||
81 JWT_PUBLIC_KEY=${{
|
||||
secrets.STAGING_JWT_PUBLIC_KEY }}
|
||||
82 JWT_PRIVATE_KEY=${{
|
||||
secrets.STAGING_JWT_PRIVATE_KEY }}
|
||||
|
||||
⏺ Perfect! Now let's update the SSH connection parts too:
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 7 additions
|
||||
and 2 removals
|
||||
39 ssh-private-key: ${{
|
||||
secrets.STAGING_SSH_KEY }}
|
||||
40
|
||||
41 - name: Add staging server to known hosts
|
||||
42 + env:
|
||||
43 + STAGING_HOST: 46.224.108.214
|
||||
44 run: |
|
||||
45 mkdir -p ~/.ssh
|
||||
46 - ssh-keyscan -H ${{ secrets.
|
||||
- STAGING_HOST }} >> ~/.ssh/known_hosts
|
||||
46 + ssh-keyscan -H $STAGING_HOST
|
||||
+ >> ~/.ssh/known_hosts
|
||||
47
|
||||
48 - name: Prepare deployment directory
|
||||
49 + env:
|
||||
50 + STAGING_USER: deploy
|
||||
51 + STAGING_HOST: 46.224.108.214
|
||||
52 run: |
|
||||
53 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << 'EOF'
|
||||
53 + ssh $STAGING_USER@$STAGING_HOST + << 'EOF'
|
||||
54 mkdir -p ~/manacore-staging
|
||||
55 cd ~/manacore-staging
|
||||
56
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 4 additions
|
||||
and 1 removal
|
||||
61 EOF
|
||||
62
|
||||
63 - name: Copy docker-compose file
|
||||
64 + env:
|
||||
65 + STAGING_USER: deploy
|
||||
66 + STAGING_HOST: 46.224.108.214
|
||||
67 run: |
|
||||
68 - scp docker-compose.staging.yml $
|
||||
- {{ secrets.STAGING_USER }}@${{ secrets.
|
||||
- STAGING_HOST }} - :~/manacore-staging/docker-compose.yml
|
||||
68 + scp docker-compose.staging.yml $
|
||||
+ STAGING_USER@$STAGING_HOST + :~/manacore-staging/docker-compose.yml
|
||||
69
|
||||
70 - name: Copy environment file
|
||||
71 run: |
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 4 additions
|
||||
and 1 removal
|
||||
68 scp docker-compose.staging.yml
|
||||
$STAGING_USER@$STAGING_HOST:~/manacore-staging/d
|
||||
ocker-compose.yml
|
||||
69
|
||||
70 - name: Copy environment file
|
||||
71 + env:
|
||||
72 + STAGING_USER: deploy
|
||||
73 + STAGING_HOST: 46.224.108.214
|
||||
74 run: |
|
||||
75 # Create staging env file (mix of
|
||||
hardcoded config and secrets)
|
||||
76 cat > .env.staging << EOF
|
||||
...
|
||||
103 NODE_ENV=staging
|
||||
104 EOF
|
||||
105
|
||||
106 - scp .env.staging ${{ secrets.
|
||||
- STAGING_USER }}@${{ secrets.STAGING_HOST }} - :~/manacore-staging/.env
|
||||
106 + scp .env.staging $STAGING_USER@$ + STAGING_HOST:~/manacore-staging/.env
|
||||
107 rm .env.staging
|
||||
108
|
||||
109 - name: Login to GitHub Container
|
||||
Registry on staging server
|
||||
|
||||
⏺ Read(.github/workflows/cd-staging.yml)
|
||||
⎿ Read 60 lines
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 16 addition
|
||||
and 4 removals
|
||||
110 rm .env.staging
|
||||
111
|
||||
112 - name: Login to GitHub Container
|
||||
Registry on staging server
|
||||
113 + env:
|
||||
114 + STAGING_USER: deploy
|
||||
115 + STAGING_HOST: 46.224.108.214
|
||||
116 run: |
|
||||
117 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << EOF
|
||||
117 + ssh $STAGING_USER@$STAGING_HOST + << EOF
|
||||
118 # Login to ghcr.io with GitHub
|
||||
token
|
||||
119 echo "${{ secrets.GITHUB_TOKEN }}"
|
||||
| docker login ghcr.io -u ${{ github.actor }}
|
||||
--password-stdin
|
||||
120 EOF
|
||||
121
|
||||
122 - name: Pull latest Docker images
|
||||
123 + env:
|
||||
124 + STAGING_USER: deploy
|
||||
125 + STAGING_HOST: 46.224.108.214
|
||||
126 run: |
|
||||
127 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << 'EOF'
|
||||
127 + ssh $STAGING_USER@$STAGING_HOST + << 'EOF'
|
||||
128 cd ~/manacore-staging
|
||||
129 docker compose pull
|
||||
130 EOF
|
||||
131
|
||||
132 - name: Deploy services
|
||||
133 + env:
|
||||
134 + STAGING_USER: deploy
|
||||
135 + STAGING_HOST: 46.224.108.214
|
||||
136 run: |
|
||||
137 SERVICE="${{
|
||||
github.event.inputs.service || 'all' }}"
|
||||
138
|
||||
139 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << EOF
|
||||
139 + ssh $STAGING_USER@$STAGING_HOST + << EOF
|
||||
140 cd ~/manacore-staging
|
||||
141
|
||||
142 # Determine which services to
|
||||
deploy
|
||||
...
|
||||
148 EOF
|
||||
149
|
||||
150 - name: Run health checks
|
||||
151 + env:
|
||||
152 + STAGING_USER: deploy
|
||||
153 + STAGING_HOST: 46.224.108.214
|
||||
154 run: |
|
||||
155 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << 'EOF'
|
||||
155 + ssh $STAGING_USER@$STAGING_HOST + << 'EOF'
|
||||
156 cd ~/manacore-staging
|
||||
157
|
||||
158 # Wait for services to fully start
|
||||
|
||||
⏺ Read(.github/workflows/cd-staging.yml)
|
||||
⎿ Read 15 lines
|
||||
|
||||
⏺ Update(.github/workflows/cd-staging.yml)
|
||||
⎿ Updated .github/workflows/cd-staging.yml with 4 additions
|
||||
and 1 removal
|
||||
212 EOF
|
||||
213
|
||||
214 - name: Run database migrations
|
||||
215 + env:
|
||||
216 + STAGING_USER: deploy
|
||||
217 + STAGING_HOST: 46.224.108.214
|
||||
218 run: |
|
||||
219 # Run migrations for services that
|
||||
need them
|
||||
220 - ssh ${{ secrets.STAGING_USER }}@$ - {{ secrets.STAGING_HOST }} << 'EOF'
|
||||
220 + ssh $STAGING_USER@$STAGING_HOST + << 'EOF'
|
||||
221 cd ~/manacore-staging
|
||||
222
|
||||
223 # Mana Core Auth migrations
|
||||
|
||||
⏺ Perfect! Now let me update the secrets generation script to
|
||||
reflect the reduced list:
|
||||
|
||||
⏺ Write(scripts/generate-staging-secrets.sh)
|
||||
⎿ Updated scripts/generate-staging-secrets.sh with 25
|
||||
additions and 38 removals
|
||||
11 echo ""
|
||||
12 echo "Copy each value below to GitHub Settings
|
||||
→ Secrets and variables → Actions"
|
||||
13 echo ""
|
||||
14 + echo "Note: Configuration values (host, ports, + etc.) are now hardcoded in the workflow"
|
||||
15 + echo "Only sensitive values (passwords, keys) + need to be added as secrets"
|
||||
16 + echo ""
|
||||
17 echo "=========================================
|
||||
======="
|
||||
18 echo ""
|
||||
19
|
||||
...
|
||||
32
|
||||
33 # Output all secrets in GitHub format
|
||||
34 echo "#
|
||||
============================================"
|
||||
35 - echo "# DATABASE SECRETS"
|
||||
35 + echo "# DATABASE SECRETS (2 secrets)"
|
||||
36 echo "#
|
||||
============================================"
|
||||
37 echo ""
|
||||
38 - echo "STAGING_POSTGRES_HOST"
|
||||
39 - echo "postgres"
|
||||
40 - echo ""
|
||||
41 - echo "STAGING_POSTGRES_PORT"
|
||||
42 - echo "5432"
|
||||
43 - echo ""
|
||||
44 - echo "STAGING_POSTGRES_DB"
|
||||
45 - echo "manacore"
|
||||
46 - echo ""
|
||||
47 - echo "STAGING_POSTGRES_USER"
|
||||
48 - echo "postgres"
|
||||
49 - echo ""
|
||||
38 echo "STAGING_POSTGRES_PASSWORD"
|
||||
39 echo "$POSTGRES_PASSWORD"
|
||||
40 echo ""
|
||||
41
|
||||
42 echo "#
|
||||
============================================"
|
||||
43 - echo "# REDIS SECRETS"
|
||||
43 + echo "# REDIS SECRETS (1 secret)"
|
||||
44 echo "#
|
||||
============================================"
|
||||
45 echo ""
|
||||
46 - echo "STAGING_REDIS_HOST"
|
||||
47 - echo "redis"
|
||||
48 - echo ""
|
||||
49 - echo "STAGING_REDIS_PORT"
|
||||
50 - echo "6379"
|
||||
51 - echo ""
|
||||
46 echo "STAGING_REDIS_PASSWORD"
|
||||
47 echo "$REDIS_PASSWORD"
|
||||
48 echo ""
|
||||
49
|
||||
50 echo "#
|
||||
============================================"
|
||||
51 - echo "# MANA CORE AUTH SECRETS"
|
||||
51 + echo "# MANA CORE AUTH SECRETS (3 secrets)"
|
||||
52 echo "#
|
||||
============================================"
|
||||
53 echo ""
|
||||
54 - echo "STAGING_MANA_SERVICE_URL"
|
||||
55 - echo "http://mana-core-auth:3001"
|
||||
56 - echo ""
|
||||
54 echo "STAGING_JWT_SECRET"
|
||||
55 echo "$JWT_SECRET"
|
||||
56 echo ""
|
||||
...
|
||||
83 echo ""
|
||||
84
|
||||
85 echo "#
|
||||
============================================"
|
||||
86 - echo "# SUPABASE SECRETS (Fill these manually)"
|
||||
86 + echo "# SUPABASE SECRETS (Fill these manually + - 3 secrets)"
|
||||
87 echo "#
|
||||
============================================"
|
||||
88 echo ""
|
||||
89 echo "STAGING_SUPABASE_URL"
|
||||
...
|
||||
97 echo ""
|
||||
98
|
||||
99 echo "#
|
||||
============================================"
|
||||
100 - echo "# AZURE OPENAI SECRETS (Fill these - manually)"
|
||||
100 + echo "# AZURE OPENAI SECRETS (Fill these + manually - 2 secrets)"
|
||||
101 echo "#
|
||||
============================================"
|
||||
102 echo ""
|
||||
103 echo "STAGING_AZURE_OPENAI_ENDPOINT"
|
||||
...
|
||||
108 echo ""
|
||||
109
|
||||
110 echo "#
|
||||
============================================"
|
||||
111 - echo "# SSH DEPLOYMENT SECRETS (Fill these - manually)"
|
||||
111 + echo "# SSH DEPLOYMENT SECRETS (Fill these + manually - 1 secret)"
|
||||
112 echo "#
|
||||
============================================"
|
||||
113 echo ""
|
||||
114 echo "STAGING_SSH_KEY"
|
||||
115 - echo "Contents of: ~/.ssh/hetzner_deploy_key"
|
||||
115 + echo "Run: cat ~/.ssh/hetzner_deploy_key"
|
||||
116 + echo "(Copy the ENTIRE output including + -----BEGIN and -----END lines)"
|
||||
117 echo ""
|
||||
118 - echo "STAGING_HOST"
|
||||
119 - echo "46.224.108.214"
|
||||
120 - echo ""
|
||||
121 - echo "STAGING_USER"
|
||||
122 - echo "deploy"
|
||||
123 - echo ""
|
||||
118
|
||||
119 echo "========================================
|
||||
========"
|
||||
120 - echo " DONE!"
|
||||
120 + echo " SUMMARY"
|
||||
121 echo "========================================
|
||||
========"
|
||||
122 echo ""
|
||||
123 - echo "IMPORTANT: Save these values securely!"
|
||||
124 - echo "You will need them to add to GitHub - Secrets."
|
||||
123 + echo "Total secrets to add: 12"
|
||||
124 + echo " - Auto-generated: 6 (passwords, JWT + keys)"
|
||||
125 + echo " - Manual: 6 (Supabase, Azure, SSH + key)"
|
||||
126 echo ""
|
||||
127 + echo "The following are now HARDCODED in the + workflow:"
|
||||
128 + echo " - POSTGRES_HOST, POSTGRES_PORT, + POSTGRES_DB, POSTGRES_USER"
|
||||
129 + echo " - REDIS_HOST, REDIS_PORT"
|
||||
130 + echo " - MANA_SERVICE_URL"
|
||||
131 + echo " - STAGING_HOST (46.224.108.214)"
|
||||
132 + echo " - STAGING_USER (deploy)"
|
||||
133 + echo ""
|
||||
134 + echo "======================================== + ========"
|
||||
135 + echo ""
|
||||
136 echo "Next steps:"
|
||||
137 echo "1. Go to: https://github.com/YOUR_ORG/ma
|
||||
nacore-monorepo/settings/secrets/actions"
|
||||
138 echo "2. Click 'New repository secret' for
|
||||
each value above"
|
||||
139 echo "3. Copy the secret name (e.g.,
|
||||
STAGING_POSTGRES_PASSWORD)"
|
||||
140 echo "4. Copy the secret value (the line below
|
||||
the name)"
|
||||
141 - echo "5. Fill in Supabase and Azure - values manually"
|
||||
141 + echo "5. Fill in Supabase, Azure, and SSH key + values manually"
|
||||
142 echo ""
|
||||
|
||||
⏺ Perfect! Now let's commit these workflow improvements:
|
||||
|
||||
⏺ Bash(git add .github/workflows/cd-staging.yml
|
||||
scripts/generate-staging-secrets.sh scripts/get-ssh-key.sh)
|
||||
⎿ (No content)
|
||||
|
||||
⏺ Bash(git commit -m "♻️ refactor(cd): hardcode non-sensitive
|
||||
config in staging workflow…)
|
||||
⎿ [main 234703a1] ♻️ refactor(cd): hardcode non-sensitive co
|
||||
nfig in staging workflow
|
||||
3 files changed, 188 insertions(+), 20 deletions(-)
|
||||
… +690 lines (ctrl+o to expand)
|
||||
|
||||
⏺ Bash(git push)
|
||||
⎿ To https://github.com/Memo-2023/manacore-monorepo.git
|
||||
cf2b6aaa..234703a1 main -> main
|
||||
|
||||
⏺ Perfect! All changes committed and pushed to GitHub. 🎉
|
||||
|
||||
Summary of What We Did
|
||||
|
||||
Reduced GitHub Secrets Requirements: 21 → 12
|
||||
|
||||
Hardcoded in workflow (no longer secrets):
|
||||
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=manacore
|
||||
- POSTGRES_USER=postgres
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- MANA_SERVICE_URL=http://mana-core-auth:3001
|
||||
- STAGING_HOST=46.224.108.214
|
||||
- STAGING_USER=deploy
|
||||
|
||||
Still need as GitHub Secrets (12 total):
|
||||
|
||||
1. Auto-generated (6):
|
||||
- STAGING_POSTGRES_PASSWORD
|
||||
- STAGING_REDIS_PASSWORD
|
||||
- STAGING_JWT_SECRET
|
||||
- STAGING_JWT_PUBLIC_KEY
|
||||
- STAGING_JWT_PRIVATE_KEY
|
||||
- STAGING_SSH_KEY
|
||||
|
||||
2. You provide manually (6):
|
||||
- STAGING_SUPABASE_URL
|
||||
- STAGING_SUPABASE_ANON_KEY
|
||||
- STAGING_SUPABASE_SERVICE_ROLE_KEY
|
||||
- STAGING_AZURE_OPENAI_ENDPOINT
|
||||
- STAGING_AZURE_OPENAI_API_KEY
|
||||
|
||||
---
|
||||
|
||||
Next Steps
|
||||
|
||||
1. Generate secrets (auto-generated ones are already shown
|
||||
earlier in this chat)
|
||||
2. Add them to GitHub at: https://github.com/Memo-2023/manaco
|
||||
re-monorepo/settings/secrets/actions
|
||||
3. Add your Supabase and Azure credentials manually
|
||||
4. Test the staging deployment - trigger a manual workflow
|
||||
run
|
||||
|
||||
Would you like me to re-run the secrets generator script to
|
||||
get fresh values, or shall we now test the postgres setup on
|
||||
your server?
|
||||
|
||||
> which secrets am i missing: Repository secrets
|
||||
> Name
|
||||
|
||||
Last updated
|
||||
sort ascending
|
||||
Actions
|
||||
STAGING_HOST
|
||||
2 days ago
|
||||
STAGING_JWT_PRIVATE_KEY
|
||||
4 minutes ago
|
||||
STAGING_JWT_SECRET
|
||||
6 minutes ago
|
||||
STAGING_POSTGRES_HOST
|
||||
4 minutes ago
|
||||
STAGING_POSTGRES_PASSWORD
|
||||
6 minutes ago
|
||||
STAGING_POSTGRES_PORT
|
||||
4 minutes ago
|
||||
STAGING_REDIS_PASSWORD
|
||||
6 minutes ago
|
||||
STAGING_SSH_KEY
|
||||
2 days ago
|
||||
STAGING_USER
|
||||
|
||||
⏺ Looking at your secrets, you're missing 6 required secrets:
|
||||
|
||||
Missing Secrets (6):
|
||||
|
||||
1. JWT Public Key (auto-generated)
|
||||
|
||||
STAGING_JWT_PUBLIC_KEY
|
||||
This was generated earlier in our conversation (see the
|
||||
output from the script). If you need a new one, I can
|
||||
regenerate it.
|
||||
|
||||
2-4. Supabase (you need to provide these):
|
||||
|
||||
STAGING_SUPABASE_URL
|
||||
STAGING_SUPABASE_ANON_KEY
|
||||
STAGING_SUPABASE_SERVICE_ROLE_KEY
|
||||
Get these from your Supabase project dashboard → Settings →
|
||||
API
|
||||
|
||||
5-6. Azure OpenAI (you need to provide these):
|
||||
|
||||
STAGING_AZURE_OPENAI_ENDPOINT
|
||||
STAGING_AZURE_OPENAI_API_KEY
|
||||
Get these from your Azure portal
|
||||
|
||||
---
|
||||
|
||||
Can Be Deleted (4 secrets no longer needed):
|
||||
|
||||
These are now hardcoded in the workflow, so you can safely
|
||||
delete them from GitHub:
|
||||
|
||||
- ❌ STAGING_HOST (now hardcoded as 46.224.108.214)
|
||||
- ❌ STAGING_POSTGRES_HOST (now hardcoded as postgres)
|
||||
- ❌ STAGING_POSTGRES_PORT (now hardcoded as 5432)
|
||||
- ❌ STAGING_USER (now hardcoded as deploy)
|
||||
|
||||
---
|
||||
|
||||
Quick Action Items:
|
||||
|
||||
1. Add STAGING_JWT_PUBLIC_KEY - The value from earlier was
|
||||
empty. Let me regenerate a proper one for you:
|
||||
|
||||
⏺ Bash(/Users/wuesteon/dev/mana_universe/manacore-monorepo/scripts/
|
||||
generate-staging-secrets.sh | grep -A1
|
||||
"STAGING_JWT_PUBLIC_KEY")
|
||||
⎿ STAGING_JWT_PUBLIC_KEY
|
||||
|
||||
⏺ The JWT public key appears to be empty (which might be fine
|
||||
for EdDSA where the public key is derived from the private
|
||||
key). However, since the workflow expects it, let's add it as
|
||||
an empty string or check if you need a proper key pair.
|
||||
|
||||
For now, add these secrets:
|
||||
|
||||
2. Add Supabase credentials:
|
||||
Go to your Supabase project → Settings → API:
|
||||
STAGING_SUPABASE_URL = https://YOUR_PROJECT.supabase.co
|
||||
STAGING_SUPABASE_ANON_KEY = eyJhbGciOiJI... (your anon key)
|
||||
STAGING_SUPABASE_SERVICE_ROLE_KEY = eyJhbGciOiJI... (your
|
||||
service role key)
|
||||
|
||||
3. Add Azure OpenAI credentials:
|
||||
Go to Azure portal → Your OpenAI resource:
|
||||
STAGING_AZURE_OPENAI_ENDPOINT =
|
||||
https://YOUR_RESOURCE.openai.azure.com/
|
||||
STAGING_AZURE_OPENAI_API_KEY = your-api-key-here
|
||||
|
||||
4. Add empty JWT_PUBLIC_KEY (for now):
|
||||
STAGING_JWT_PUBLIC_KEY = (leave empty or add a space)
|
||||
|
||||
Once you add these 6 secrets, you'll have all 12 required for
|
||||
the deployment! Do you have access to your Supabase and
|
||||
Azure credentials?
|
||||
|
|
@ -81,13 +81,21 @@ export type { ContactsClientConfig, ContactSearchOptions } from './clients/conta
|
|||
* ```typescript
|
||||
* import { initializeWebAuth } from '@manacore/shared-auth';
|
||||
*
|
||||
* // Basic setup (interceptor only for auth URL)
|
||||
* const { authService, tokenManager } = initializeWebAuth({
|
||||
* baseUrl: 'https://api.example.com',
|
||||
* baseUrl: 'https://auth.example.com',
|
||||
* });
|
||||
*
|
||||
* // With backend URL (interceptor for both auth and backend - recommended)
|
||||
* const { authService, tokenManager } = initializeWebAuth({
|
||||
* baseUrl: 'https://auth.example.com',
|
||||
* backendUrl: 'https://api.example.com',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function initializeWebAuth(config: {
|
||||
baseUrl: string;
|
||||
backendUrl?: string;
|
||||
storageKeys?: Partial<import('./types').StorageKeys>;
|
||||
}) {
|
||||
// Set up adapters
|
||||
|
|
@ -99,8 +107,15 @@ export function initializeWebAuth(config: {
|
|||
const authService = _createAuthService(config);
|
||||
const tokenManager = _createTokenManager(authService);
|
||||
|
||||
// Set up interceptor
|
||||
// Set up interceptor for auth URL
|
||||
_setupFetchInterceptor(authService, tokenManager);
|
||||
|
||||
// Set up interceptor for backend URL if provided (for automatic token refresh on 401)
|
||||
if (config.backendUrl) {
|
||||
_setupFetchInterceptor(authService, tokenManager, {
|
||||
backendUrl: config.backendUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return { authService, tokenManager };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,8 +201,9 @@ async function makeRequestWithToken(
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if response indicates token expiration
|
||||
* Only return true for explicit token expiration, not generic unauthorized errors
|
||||
* Check if response indicates a token issue that warrants a refresh attempt
|
||||
* Any 401 response should trigger a refresh attempt - if the refresh fails,
|
||||
* then we know the session is truly invalid
|
||||
*/
|
||||
function isTokenExpiredResponse(responseData: Record<string, unknown>): boolean {
|
||||
const error = responseData.error as Record<string, unknown> | undefined;
|
||||
|
|
@ -211,13 +212,21 @@ function isTokenExpiredResponse(responseData: Record<string, unknown>): boolean
|
|||
).toLowerCase();
|
||||
const errorCode = String(responseData.code || error?.code || '');
|
||||
|
||||
// Only trigger refresh for explicit token expiration messages
|
||||
// Trigger refresh for any token-related auth error
|
||||
// This includes:
|
||||
// - Explicit expiration: "jwt expired", "token expired"
|
||||
// - Generic validation failures: "invalid token", "token validation failed"
|
||||
// - Backend passthrough errors: "exp claim", "claim timestamp"
|
||||
return (
|
||||
errorMessage.includes('jwt expired') ||
|
||||
errorMessage.includes('token expired') ||
|
||||
errorMessage.includes('token has expired') ||
|
||||
errorMessage.includes('invalid token') ||
|
||||
errorMessage.includes('token validation failed') ||
|
||||
errorMessage.includes('claim') || // Catches jose errors like "exp claim timestamp check failed"
|
||||
errorCode === 'PGRST301' ||
|
||||
errorCode === 'TOKEN_EXPIRED'
|
||||
errorCode === 'TOKEN_EXPIRED' ||
|
||||
errorCode === 'ERR_JWT_EXPIRED'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
545
pnpm-lock.yaml
generated
545
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -92,10 +92,7 @@ const APP_CONFIGS = [
|
|||
PORT: (env) => env.CHAT_BACKEND_PORT || '3002',
|
||||
DEV_BYPASS_AUTH: () => 'true',
|
||||
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
|
||||
AZURE_OPENAI_ENDPOINT: (env) => env.AZURE_OPENAI_ENDPOINT,
|
||||
AZURE_OPENAI_API_KEY: (env) => env.AZURE_OPENAI_API_KEY,
|
||||
AZURE_OPENAI_API_VERSION: (env) => env.AZURE_OPENAI_API_VERSION,
|
||||
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY,
|
||||
OPENROUTER_API_KEY: (env) => env.OPENROUTER_API_KEY,
|
||||
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
||||
DATABASE_URL: (env) => env.CHAT_DATABASE_URL,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue