Compare commits
3 Commits
v0.0.4
...
58ba99f3eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 58ba99f3eb | |||
| 3c8e7fd6b2 | |||
| b86bfebe1f |
@@ -8,10 +8,11 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsx watch src/main.ts",
|
"dev": "tsx watch src/main.ts",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
||||||
"test": "vitest run --passWithNoTests"
|
"test": "vitest run --passWithNoTests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@mariozechner/pi-ai": "~0.57.1",
|
"@mariozechner/pi-ai": "~0.57.1",
|
||||||
"@mariozechner/pi-coding-agent": "~0.57.1",
|
"@mariozechner/pi-coding-agent": "~0.57.1",
|
||||||
"@mosaic/auth": "workspace:^",
|
"@mosaic/auth": "workspace:^",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
"@nestjs/platform-fastify": "^11.0.0",
|
"@nestjs/platform-fastify": "^11.0.0",
|
||||||
"@nestjs/platform-socket.io": "^11.0.0",
|
"@nestjs/platform-socket.io": "^11.0.0",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/websockets": "^11.0.0",
|
"@nestjs/websockets": "^11.0.0",
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
|
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
|
||||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
|
||||||
@@ -33,6 +35,8 @@
|
|||||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||||
"@sinclair/typebox": "^0.34.48",
|
"@sinclair/typebox": "^0.34.48",
|
||||||
"better-auth": "^1.5.5",
|
"better-auth": "^1.5.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
|
|||||||
89
apps/gateway/src/__tests__/resource-ownership.test.ts
Normal file
89
apps/gateway/src/__tests__/resource-ownership.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { ForbiddenException } from '@nestjs/common';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ConversationsController } from '../conversations/conversations.controller.js';
|
||||||
|
import { MissionsController } from '../missions/missions.controller.js';
|
||||||
|
import { ProjectsController } from '../projects/projects.controller.js';
|
||||||
|
import { TasksController } from '../tasks/tasks.controller.js';
|
||||||
|
|
||||||
|
function createBrain() {
|
||||||
|
return {
|
||||||
|
conversations: {
|
||||||
|
findAll: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
findMessages: vi.fn(),
|
||||||
|
addMessage: vi.fn(),
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
findAll: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
},
|
||||||
|
missions: {
|
||||||
|
findAll: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
findByProject: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
findAll: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
findByProject: vi.fn(),
|
||||||
|
findByMission: vi.fn(),
|
||||||
|
findByStatus: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Resource ownership checks', () => {
|
||||||
|
it('forbids access to another user conversation', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.conversations.findById.mockResolvedValue({ id: 'conv-1', userId: 'user-2' });
|
||||||
|
const controller = new ConversationsController(brain as never);
|
||||||
|
|
||||||
|
await expect(controller.findOne('conv-1', { id: 'user-1' })).rejects.toBeInstanceOf(
|
||||||
|
ForbiddenException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forbids access to another user project', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||||
|
const controller = new ProjectsController(brain as never);
|
||||||
|
|
||||||
|
await expect(controller.findOne('project-1', { id: 'user-1' })).rejects.toBeInstanceOf(
|
||||||
|
ForbiddenException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forbids access to a mission owned by another project owner', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.missions.findById.mockResolvedValue({ id: 'mission-1', projectId: 'project-1' });
|
||||||
|
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||||
|
const controller = new MissionsController(brain as never);
|
||||||
|
|
||||||
|
await expect(controller.findOne('mission-1', { id: 'user-1' })).rejects.toBeInstanceOf(
|
||||||
|
ForbiddenException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forbids access to a task owned by another project owner', async () => {
|
||||||
|
const brain = createBrain();
|
||||||
|
brain.tasks.findById.mockResolvedValue({ id: 'task-1', projectId: 'project-1' });
|
||||||
|
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||||
|
const controller = new TasksController(brain as never);
|
||||||
|
|
||||||
|
await expect(controller.findOne('task-1', { id: 'user-1' })).rejects.toBeInstanceOf(
|
||||||
|
ForbiddenException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -89,7 +89,7 @@ export class ProviderService implements OnModuleInit {
|
|||||||
const modelsEnv = process.env['OLLAMA_MODELS'] ?? 'llama3.2,codellama,mistral';
|
const modelsEnv = process.env['OLLAMA_MODELS'] ?? 'llama3.2,codellama,mistral';
|
||||||
const modelIds = modelsEnv
|
const modelIds = modelsEnv
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((m) => m.trim())
|
.map((modelId: string) => modelId.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
this.registerCustomProvider({
|
this.registerCustomProvider({
|
||||||
|
|||||||
@@ -145,8 +145,11 @@ export class RoutingService {
|
|||||||
|
|
||||||
private classifyTier(model: ModelInfo): CostTier {
|
private classifyTier(model: ModelInfo): CostTier {
|
||||||
const cost = model.cost.input;
|
const cost = model.cost.input;
|
||||||
if (cost <= COST_TIER_THRESHOLDS.cheap.maxInput) return 'cheap';
|
const cheapThreshold = COST_TIER_THRESHOLDS['cheap'];
|
||||||
if (cost <= COST_TIER_THRESHOLDS.standard.maxInput) return 'standard';
|
const standardThreshold = COST_TIER_THRESHOLDS['standard'];
|
||||||
|
|
||||||
|
if (cost <= cheapThreshold.maxInput) return 'cheap';
|
||||||
|
if (cost <= standardThreshold.maxInput) return 'standard';
|
||||||
return 'premium';
|
return 'premium';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { HealthController } from './health/health.controller.js';
|
import { HealthController } from './health/health.controller.js';
|
||||||
import { DatabaseModule } from './database/database.module.js';
|
import { DatabaseModule } from './database/database.module.js';
|
||||||
import { AuthModule } from './auth/auth.module.js';
|
import { AuthModule } from './auth/auth.module.js';
|
||||||
@@ -10,9 +11,11 @@ import { ProjectsModule } from './projects/projects.module.js';
|
|||||||
import { MissionsModule } from './missions/missions.module.js';
|
import { MissionsModule } from './missions/missions.module.js';
|
||||||
import { TasksModule } from './tasks/tasks.module.js';
|
import { TasksModule } from './tasks/tasks.module.js';
|
||||||
import { CoordModule } from './coord/coord.module.js';
|
import { CoordModule } from './coord/coord.module.js';
|
||||||
|
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BrainModule,
|
BrainModule,
|
||||||
@@ -25,5 +28,11 @@ import { CoordModule } from './coord/coord.module.js';
|
|||||||
CoordModule,
|
CoordModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: ThrottlerGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
11
apps/gateway/src/auth/resource-ownership.ts
Normal file
11
apps/gateway/src/auth/resource-ownership.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ForbiddenException } from '@nestjs/common';
|
||||||
|
|
||||||
|
export function assertOwner(
|
||||||
|
ownerId: string | null | undefined,
|
||||||
|
userId: string,
|
||||||
|
resourceName: string,
|
||||||
|
): void {
|
||||||
|
if (!ownerId || ownerId !== userId) {
|
||||||
|
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
apps/gateway/src/chat/__tests__/chat-security.test.ts
Normal file
80
apps/gateway/src/chat/__tests__/chat-security.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { validateSync } from 'class-validator';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { SendMessageDto } from '../../conversations/conversations.dto.js';
|
||||||
|
import { ChatRequestDto } from '../chat.dto.js';
|
||||||
|
import { validateSocketSession } from '../chat.gateway-auth.js';
|
||||||
|
|
||||||
|
describe('Chat controller source hardening', () => {
|
||||||
|
it('applies AuthGuard and reads the current user', () => {
|
||||||
|
const source = readFileSync(resolve('src/chat/chat.controller.ts'), 'utf8');
|
||||||
|
|
||||||
|
expect(source).toContain('@UseGuards(AuthGuard)');
|
||||||
|
expect(source).toContain('@CurrentUser() user: { id: string }');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WebSocket session authentication', () => {
|
||||||
|
it('returns null when the handshake does not resolve to a session', async () => {
|
||||||
|
const result = await validateSocketSession(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
api: {
|
||||||
|
getSession: vi.fn().mockResolvedValue(null),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the resolved session when Better Auth accepts the headers', async () => {
|
||||||
|
const session = { user: { id: 'user-1' }, session: { id: 'session-1' } };
|
||||||
|
|
||||||
|
const result = await validateSocketSession(
|
||||||
|
{ cookie: 'session=abc' },
|
||||||
|
{
|
||||||
|
api: {
|
||||||
|
getSession: vi.fn().mockResolvedValue(session),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Chat DTO validation', () => {
|
||||||
|
it('rejects unsupported message roles', () => {
|
||||||
|
const dto = Object.assign(new SendMessageDto(), {
|
||||||
|
content: 'hello',
|
||||||
|
role: 'moderator',
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = validateSync(dto);
|
||||||
|
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects oversized conversation message content above 10000 characters', () => {
|
||||||
|
const dto = Object.assign(new SendMessageDto(), {
|
||||||
|
content: 'x'.repeat(10_001),
|
||||||
|
role: 'user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = validateSync(dto);
|
||||||
|
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects oversized chat content above 10000 characters', () => {
|
||||||
|
const dto = Object.assign(new ChatRequestDto(), {
|
||||||
|
content: 'x'.repeat(10_001),
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = validateSync(dto);
|
||||||
|
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import { Controller, Post, Body, Logger, HttpException, HttpStatus, Inject } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Logger,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { AgentService } from '../agent/agent.service.js';
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { ChatRequestDto } from './chat.dto.js';
|
||||||
interface ChatRequest {
|
|
||||||
conversationId?: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatResponse {
|
interface ChatResponse {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
@@ -14,13 +22,18 @@ interface ChatResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller('api/chat')
|
@Controller('api/chat')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
export class ChatController {
|
export class ChatController {
|
||||||
private readonly logger = new Logger(ChatController.name);
|
private readonly logger = new Logger(ChatController.name);
|
||||||
|
|
||||||
constructor(@Inject(AgentService) private readonly agentService: AgentService) {}
|
constructor(@Inject(AgentService) private readonly agentService: AgentService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async chat(@Body() body: ChatRequest): Promise<ChatResponse> {
|
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
||||||
|
async chat(
|
||||||
|
@Body() body: ChatRequestDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
): Promise<ChatResponse> {
|
||||||
const conversationId = body.conversationId ?? uuid();
|
const conversationId = body.conversationId ?? uuid();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -36,6 +49,8 @@ export class ChatController {
|
|||||||
throw new HttpException('Agent session unavailable', HttpStatus.SERVICE_UNAVAILABLE);
|
throw new HttpException('Agent session unavailable', HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Handling chat request for user=${user.id}, conversation=${conversationId}`);
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|
||||||
const done = new Promise<void>((resolve, reject) => {
|
const done = new Promise<void>((resolve, reject) => {
|
||||||
|
|||||||
31
apps/gateway/src/chat/chat.dto.ts
Normal file
31
apps/gateway/src/chat/chat.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class ChatRequestDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
conversationId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
|
content!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatSocketMessageDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
conversationId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
provider?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
modelId?: string;
|
||||||
|
}
|
||||||
30
apps/gateway/src/chat/chat.gateway-auth.ts
Normal file
30
apps/gateway/src/chat/chat.gateway-auth.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { IncomingHttpHeaders } from 'node:http';
|
||||||
|
import { fromNodeHeaders } from 'better-auth/node';
|
||||||
|
|
||||||
|
export interface SocketSessionResult {
|
||||||
|
session: unknown;
|
||||||
|
user: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionAuth {
|
||||||
|
api: {
|
||||||
|
getSession(context: { headers: Headers }): Promise<SocketSessionResult | null>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateSocketSession(
|
||||||
|
headers: IncomingHttpHeaders,
|
||||||
|
auth: SessionAuth,
|
||||||
|
): Promise<SocketSessionResult | null> {
|
||||||
|
const sessionHeaders = fromNodeHeaders(headers);
|
||||||
|
const result = await auth.api.getSession({ headers: sessionHeaders });
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: result.session,
|
||||||
|
user: { id: result.user.id },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,18 +11,17 @@ import {
|
|||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
||||||
|
import type { Auth } from '@mosaic/auth';
|
||||||
import { AgentService } from '../agent/agent.service.js';
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { ChatSocketMessageDto } from './chat.dto.js';
|
||||||
interface ChatMessage {
|
import { validateSocketSession } from './chat.gateway-auth.js';
|
||||||
conversationId?: string;
|
|
||||||
content: string;
|
|
||||||
provider?: string;
|
|
||||||
modelId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: { origin: '*' },
|
cors: {
|
||||||
|
origin: process.env['GATEWAY_CORS_ORIGIN'] ?? 'http://localhost:3000',
|
||||||
|
},
|
||||||
namespace: '/chat',
|
namespace: '/chat',
|
||||||
})
|
})
|
||||||
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
||||||
@@ -35,13 +34,25 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
{ conversationId: string; cleanup: () => void }
|
{ conversationId: string; cleanup: () => void }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(@Inject(AgentService) private readonly agentService: AgentService) {}
|
constructor(
|
||||||
|
@Inject(AgentService) private readonly agentService: AgentService,
|
||||||
|
@Inject(AUTH) private readonly auth: Auth,
|
||||||
|
) {}
|
||||||
|
|
||||||
afterInit(): void {
|
afterInit(): void {
|
||||||
this.logger.log('Chat WebSocket gateway initialized');
|
this.logger.log('Chat WebSocket gateway initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnection(client: Socket): void {
|
async handleConnection(client: Socket): Promise<void> {
|
||||||
|
const session = await validateSocketSession(client.handshake.headers, this.auth);
|
||||||
|
if (!session) {
|
||||||
|
this.logger.warn(`Rejected unauthenticated WebSocket client: ${client.id}`);
|
||||||
|
client.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.data.user = session.user;
|
||||||
|
client.data.session = session.session;
|
||||||
this.logger.log(`Client connected: ${client.id}`);
|
this.logger.log(`Client connected: ${client.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +69,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
@SubscribeMessage('message')
|
@SubscribeMessage('message')
|
||||||
async handleMessage(
|
async handleMessage(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: Socket,
|
||||||
@MessageBody() data: ChatMessage,
|
@MessageBody() data: ChatSocketMessageDto,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const conversationId = data.conversationId ?? uuid();
|
const conversationId = data.conversationId ?? uuid();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import type { Brain } from '@mosaic/brain';
|
|||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
import type {
|
import { assertOwner } from '../auth/resource-ownership.js';
|
||||||
|
import {
|
||||||
CreateConversationDto,
|
CreateConversationDto,
|
||||||
UpdateConversationDto,
|
UpdateConversationDto,
|
||||||
SendMessageDto,
|
SendMessageDto,
|
||||||
@@ -33,10 +34,8 @@ export class ConversationsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
const conversation = await this.brain.conversations.findById(id);
|
return this.getOwnedConversation(id, user.id);
|
||||||
if (!conversation) throw new NotFoundException('Conversation not found');
|
|
||||||
return conversation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -49,7 +48,12 @@ export class ConversationsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateConversationDto) {
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateConversationDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
await this.getOwnedConversation(id, user.id);
|
||||||
const conversation = await this.brain.conversations.update(id, dto);
|
const conversation = await this.brain.conversations.update(id, dto);
|
||||||
if (!conversation) throw new NotFoundException('Conversation not found');
|
if (!conversation) throw new NotFoundException('Conversation not found');
|
||||||
return conversation;
|
return conversation;
|
||||||
@@ -57,22 +61,25 @@ export class ConversationsController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async remove(@Param('id') id: string) {
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
|
await this.getOwnedConversation(id, user.id);
|
||||||
const deleted = await this.brain.conversations.remove(id);
|
const deleted = await this.brain.conversations.remove(id);
|
||||||
if (!deleted) throw new NotFoundException('Conversation not found');
|
if (!deleted) throw new NotFoundException('Conversation not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/messages')
|
@Get(':id/messages')
|
||||||
async listMessages(@Param('id') id: string) {
|
async listMessages(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
const conversation = await this.brain.conversations.findById(id);
|
await this.getOwnedConversation(id, user.id);
|
||||||
if (!conversation) throw new NotFoundException('Conversation not found');
|
|
||||||
return this.brain.conversations.findMessages(id);
|
return this.brain.conversations.findMessages(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/messages')
|
@Post(':id/messages')
|
||||||
async addMessage(@Param('id') id: string, @Body() dto: SendMessageDto) {
|
async addMessage(
|
||||||
const conversation = await this.brain.conversations.findById(id);
|
@Param('id') id: string,
|
||||||
if (!conversation) throw new NotFoundException('Conversation not found');
|
@Body() dto: SendMessageDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
await this.getOwnedConversation(id, user.id);
|
||||||
return this.brain.conversations.addMessage({
|
return this.brain.conversations.addMessage({
|
||||||
conversationId: id,
|
conversationId: id,
|
||||||
role: dto.role,
|
role: dto.role,
|
||||||
@@ -80,4 +87,11 @@ export class ConversationsController {
|
|||||||
metadata: dto.metadata,
|
metadata: dto.metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOwnedConversation(id: string, userId: string) {
|
||||||
|
const conversation = await this.brain.conversations.findById(id);
|
||||||
|
if (!conversation) throw new NotFoundException('Conversation not found');
|
||||||
|
assertOwner(conversation.userId, userId, 'Conversation');
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,36 @@
|
|||||||
export interface CreateConversationDto {
|
import { IsIn, IsObject, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateConversationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateConversationDto {
|
export class UpdateConversationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendMessageDto {
|
export class SendMessageDto {
|
||||||
role: 'user' | 'assistant' | 'system';
|
@IsIn(['user', 'assistant', 'system'])
|
||||||
content: string;
|
role!: 'user' | 'assistant' | 'system';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
import './tracing.js';
|
import './tracing.js';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
|
import helmet from '@fastify/helmet';
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from './app.module.js';
|
||||||
import { mountAuthHandler } from './auth/auth.controller.js';
|
import { mountAuthHandler } from './auth/auth.controller.js';
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
|
if (!process.env['BETTER_AUTH_SECRET']) {
|
||||||
|
throw new Error('BETTER_AUTH_SECRET is required');
|
||||||
|
}
|
||||||
|
|
||||||
const logger = new Logger('Bootstrap');
|
const logger = new Logger('Bootstrap');
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
|
AppModule,
|
||||||
|
new FastifyAdapter({ bodyLimit: 1_048_576 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.register(helmet as never, { contentSecurityPolicy: false });
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
mountAuthHandler(app);
|
mountAuthHandler(app);
|
||||||
|
|
||||||
const port = process.env['GATEWAY_PORT'] ?? 4000;
|
const port = Number(process.env['GATEWAY_PORT'] ?? 4000);
|
||||||
await app.listen(port as number, '0.0.0.0');
|
await app.listen(port, '0.0.0.0');
|
||||||
logger.log(`Gateway listening on port ${port}`);
|
logger.log(`Gateway listening on port ${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
ForbiddenException,
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
@@ -15,7 +16,9 @@ import {
|
|||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaic/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import type { CreateMissionDto, UpdateMissionDto } from './missions.dto.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
import { assertOwner } from '../auth/resource-ownership.js';
|
||||||
|
import { CreateMissionDto, UpdateMissionDto } from './missions.dto.js';
|
||||||
|
|
||||||
@Controller('api/missions')
|
@Controller('api/missions')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@@ -28,10 +31,8 @@ export class MissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
const mission = await this.brain.missions.findById(id);
|
return this.getOwnedMission(id, user.id);
|
||||||
if (!mission) throw new NotFoundException('Mission not found');
|
|
||||||
return mission;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -45,7 +46,15 @@ export class MissionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateMissionDto) {
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateMissionDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
await this.getOwnedMission(id, user.id);
|
||||||
|
if (dto.projectId) {
|
||||||
|
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||||
|
}
|
||||||
const mission = await this.brain.missions.update(id, dto);
|
const mission = await this.brain.missions.update(id, dto);
|
||||||
if (!mission) throw new NotFoundException('Mission not found');
|
if (!mission) throw new NotFoundException('Mission not found');
|
||||||
return mission;
|
return mission;
|
||||||
@@ -53,8 +62,34 @@ export class MissionsController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async remove(@Param('id') id: string) {
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
|
await this.getOwnedMission(id, user.id);
|
||||||
const deleted = await this.brain.missions.remove(id);
|
const deleted = await this.brain.missions.remove(id);
|
||||||
if (!deleted) throw new NotFoundException('Mission not found');
|
if (!deleted) throw new NotFoundException('Mission not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOwnedMission(id: string, userId: string) {
|
||||||
|
const mission = await this.brain.missions.findById(id);
|
||||||
|
if (!mission) throw new NotFoundException('Mission not found');
|
||||||
|
await this.getOwnedProject(mission.projectId, userId, 'Mission');
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOwnedProject(
|
||||||
|
projectId: string | null | undefined,
|
||||||
|
userId: string,
|
||||||
|
resourceName: string,
|
||||||
|
) {
|
||||||
|
if (!projectId) {
|
||||||
|
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await this.brain.projects.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertOwner(project.ownerId, userId, resourceName);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,46 @@
|
|||||||
export interface CreateMissionDto {
|
import { IsIn, IsObject, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
|
||||||
name: string;
|
|
||||||
|
const missionStatuses = ['planning', 'active', 'paused', 'completed', 'failed'] as const;
|
||||||
|
|
||||||
|
export class CreateMissionDto {
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(missionStatuses)
|
||||||
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
|
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMissionDto {
|
export class UpdateMissionDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(missionStatuses)
|
||||||
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
|
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
metadata?: Record<string, unknown> | null;
|
metadata?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import type { Brain } from '@mosaic/brain';
|
|||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
import type { CreateProjectDto, UpdateProjectDto } from './projects.dto.js';
|
import { assertOwner } from '../auth/resource-ownership.js';
|
||||||
|
import { CreateProjectDto, UpdateProjectDto } from './projects.dto.js';
|
||||||
|
|
||||||
@Controller('api/projects')
|
@Controller('api/projects')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@@ -29,10 +30,8 @@ export class ProjectsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
const project = await this.brain.projects.findById(id);
|
return this.getOwnedProject(id, user.id);
|
||||||
if (!project) throw new NotFoundException('Project not found');
|
|
||||||
return project;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -46,7 +45,12 @@ export class ProjectsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateProjectDto) {
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateProjectDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
await this.getOwnedProject(id, user.id);
|
||||||
const project = await this.brain.projects.update(id, dto);
|
const project = await this.brain.projects.update(id, dto);
|
||||||
if (!project) throw new NotFoundException('Project not found');
|
if (!project) throw new NotFoundException('Project not found');
|
||||||
return project;
|
return project;
|
||||||
@@ -54,8 +58,16 @@ export class ProjectsController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async remove(@Param('id') id: string) {
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
|
await this.getOwnedProject(id, user.id);
|
||||||
const deleted = await this.brain.projects.remove(id);
|
const deleted = await this.brain.projects.remove(id);
|
||||||
if (!deleted) throw new NotFoundException('Project not found');
|
if (!deleted) throw new NotFoundException('Project not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOwnedProject(id: string, userId: string) {
|
||||||
|
const project = await this.brain.projects.findById(id);
|
||||||
|
if (!project) throw new NotFoundException('Project not found');
|
||||||
|
assertOwner(project.ownerId, userId, 'Project');
|
||||||
|
return project;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
export interface CreateProjectDto {
|
import { IsIn, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||||
name: string;
|
|
||||||
|
const projectStatuses = ['active', 'paused', 'completed', 'archived'] as const;
|
||||||
|
|
||||||
|
export class CreateProjectDto {
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(projectStatuses)
|
||||||
status?: 'active' | 'paused' | 'completed' | 'archived';
|
status?: 'active' | 'paused' | 'completed' | 'archived';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProjectDto {
|
export class UpdateProjectDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(projectStatuses)
|
||||||
status?: 'active' | 'paused' | 'completed' | 'archived';
|
status?: 'active' | 'paused' | 'completed' | 'archived';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
metadata?: Record<string, unknown> | null;
|
metadata?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
ForbiddenException,
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
@@ -16,7 +17,9 @@ import {
|
|||||||
import type { Brain } from '@mosaic/brain';
|
import type { Brain } from '@mosaic/brain';
|
||||||
import { BRAIN } from '../brain/brain.tokens.js';
|
import { BRAIN } from '../brain/brain.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import type { CreateTaskDto, UpdateTaskDto } from './tasks.dto.js';
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
|
import { assertOwner } from '../auth/resource-ownership.js';
|
||||||
|
import { CreateTaskDto, UpdateTaskDto } from './tasks.dto.js';
|
||||||
|
|
||||||
@Controller('api/tasks')
|
@Controller('api/tasks')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@@ -39,10 +42,8 @@ export class TasksController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
const task = await this.brain.tasks.findById(id);
|
return this.getOwnedTask(id, user.id);
|
||||||
if (!task) throw new NotFoundException('Task not found');
|
|
||||||
return task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -61,7 +62,18 @@ export class TasksController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateTaskDto) {
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateTaskDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
await this.getOwnedTask(id, user.id);
|
||||||
|
if (dto.projectId) {
|
||||||
|
await this.getOwnedProject(dto.projectId, user.id, 'Task');
|
||||||
|
}
|
||||||
|
if (dto.missionId) {
|
||||||
|
await this.getOwnedMission(dto.missionId, user.id, 'Task');
|
||||||
|
}
|
||||||
const task = await this.brain.tasks.update(id, {
|
const task = await this.brain.tasks.update(id, {
|
||||||
...dto,
|
...dto,
|
||||||
dueDate: dto.dueDate ? new Date(dto.dueDate) : dto.dueDate === null ? null : undefined,
|
dueDate: dto.dueDate ? new Date(dto.dueDate) : dto.dueDate === null ? null : undefined,
|
||||||
@@ -72,8 +84,46 @@ export class TasksController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async remove(@Param('id') id: string) {
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||||
|
await this.getOwnedTask(id, user.id);
|
||||||
const deleted = await this.brain.tasks.remove(id);
|
const deleted = await this.brain.tasks.remove(id);
|
||||||
if (!deleted) throw new NotFoundException('Task not found');
|
if (!deleted) throw new NotFoundException('Task not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOwnedTask(id: string, userId: string) {
|
||||||
|
const task = await this.brain.tasks.findById(id);
|
||||||
|
if (!task) throw new NotFoundException('Task not found');
|
||||||
|
|
||||||
|
if (task.projectId) {
|
||||||
|
await this.getOwnedProject(task.projectId, userId, 'Task');
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.missionId) {
|
||||||
|
await this.getOwnedMission(task.missionId, userId, 'Task');
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException('Task does not belong to the current user');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOwnedMission(missionId: string, userId: string, resourceName: string) {
|
||||||
|
const mission = await this.brain.missions.findById(missionId);
|
||||||
|
if (!mission?.projectId) {
|
||||||
|
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.getOwnedProject(mission.projectId, userId, resourceName);
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOwnedProject(projectId: string, userId: string, resourceName: string) {
|
||||||
|
const project = await this.brain.projects.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertOwner(project.ownerId, userId, resourceName);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,103 @@
|
|||||||
export interface CreateTaskDto {
|
import {
|
||||||
title: string;
|
ArrayMaxSize,
|
||||||
|
IsArray,
|
||||||
|
IsIn,
|
||||||
|
IsISO8601,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
const taskStatuses = ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'] as const;
|
||||||
|
const taskPriorities = ['critical', 'high', 'medium', 'low'] as const;
|
||||||
|
|
||||||
|
export class CreateTaskDto {
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(taskStatuses)
|
||||||
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(taskPriorities)
|
||||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
missionId?: string;
|
missionId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMaxSize(50)
|
||||||
|
@IsString({ each: true })
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO8601()
|
||||||
dueDate?: string;
|
dueDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTaskDto {
|
export class UpdateTaskDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10_000)
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(taskStatuses)
|
||||||
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(taskPriorities)
|
||||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
missionId?: string | null;
|
missionId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
assignee?: string | null;
|
assignee?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMaxSize(50)
|
||||||
|
@IsString({ each: true })
|
||||||
tags?: string[] | null;
|
tags?: string[] | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO8601()
|
||||||
dueDate?: string | null;
|
dueDate?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
metadata?: Record<string, unknown> | null;
|
metadata?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|||||||
14
apps/gateway/tsconfig.typecheck.json
Normal file
14
apps/gateway/tsconfig.typecheck.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "../..",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@mosaic/auth": ["../../packages/auth/src/index.ts"],
|
||||||
|
"@mosaic/brain": ["../../packages/brain/src/index.ts"],
|
||||||
|
"@mosaic/coord": ["../../packages/coord/src/index.ts"],
|
||||||
|
"@mosaic/db": ["../../packages/db/src/index.ts"],
|
||||||
|
"@mosaic/types": ["../../packages/types/src/index.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import './.next/types/routes.d.ts';
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { NextConfig } from 'next';
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
transpilePackages: ['@mosaic/design-tokens'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -11,17 +11,11 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mosaic/design-tokens": "workspace:^",
|
|
||||||
"better-auth": "^1.5.5",
|
|
||||||
"clsx": "^2.1.0",
|
|
||||||
"next": "^16.0.0",
|
"next": "^16.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0"
|
||||||
"socket.io-client": "^4.8.0",
|
|
||||||
"tailwind-merge": "^3.5.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { GuestGuard } from '@/components/guest-guard';
|
|
||||||
|
|
||||||
export default function AuthLayout({ children }: { children: ReactNode }): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<GuestGuard>
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-surface-bg">
|
|
||||||
<div className="w-full max-w-md rounded-xl border border-surface-border bg-surface-card p-8 shadow-lg">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GuestGuard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { signIn } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
export default function LoginPage(): React.ReactElement {
|
|
||||||
const router = useRouter();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
|
|
||||||
e.preventDefault();
|
|
||||||
setError(null);
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const form = new FormData(e.currentTarget);
|
|
||||||
const email = form.get('email') as string;
|
|
||||||
const password = form.get('password') as string;
|
|
||||||
|
|
||||||
const result = await signIn.email({ email, password });
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error.message ?? 'Sign in failed');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push('/chat');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold">Sign in</h1>
|
|
||||||
<p className="mt-1 text-sm text-text-secondary">Sign in to your Mosaic account</p>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
className="mt-4 rounded-lg border border-error/30 bg-error/10 px-4 py-3 text-sm text-error"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? 'Signing in...' : 'Sign in'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p className="mt-4 text-center text-sm text-text-muted">
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Link href="/register" className="text-blue-400 hover:text-blue-300">
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { signUp } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
export default function RegisterPage(): React.ReactElement {
|
|
||||||
const router = useRouter();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>): Promise<void> {
|
|
||||||
e.preventDefault();
|
|
||||||
setError(null);
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const form = new FormData(e.currentTarget);
|
|
||||||
const name = form.get('name') as string;
|
|
||||||
const email = form.get('email') as string;
|
|
||||||
const password = form.get('password') as string;
|
|
||||||
|
|
||||||
const result = await signUp.email({ name, email, password });
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error.message ?? 'Registration failed');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push('/chat');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold">Create account</h1>
|
|
||||||
<p className="mt-1 text-sm text-text-secondary">Get started with Mosaic</p>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
className="mt-4 rounded-lg border border-error/30 bg-error/10 px-4 py-3 text-sm text-error"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-text-secondary">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
autoComplete="name"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
placeholder="Your name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
required
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? 'Creating account...' : 'Create account'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p className="mt-4 text-center text-sm text-text-muted">
|
|
||||||
Already have an account?{' '}
|
|
||||||
<Link href="/login" className="text-blue-400 hover:text-blue-300">
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
|
|
||||||
interface SessionInfo {
|
|
||||||
id: string;
|
|
||||||
provider: string;
|
|
||||||
modelId: string;
|
|
||||||
createdAt: string;
|
|
||||||
promptCount: number;
|
|
||||||
channels: string[];
|
|
||||||
durationMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionsResponse {
|
|
||||||
sessions: SessionInfo[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminPage(): React.ReactElement {
|
|
||||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api<SessionsResponse>('/api/sessions')
|
|
||||||
.then((res) => setSessions(res.sessions))
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto max-w-4xl space-y-8">
|
|
||||||
<h1 className="text-2xl font-semibold">Admin</h1>
|
|
||||||
|
|
||||||
{/* User Management placeholder */}
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">User Management</h2>
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
|
|
||||||
<p className="text-sm text-text-muted">
|
|
||||||
User management will be available when the admin API is implemented
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Active Agent Sessions */}
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Active Agent Sessions</h2>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-sm text-text-muted">Loading sessions...</p>
|
|
||||||
) : sessions.length === 0 ? (
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
||||||
<p className="text-sm text-text-muted">No active sessions</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
|
||||||
<th className="px-4 py-2 font-medium">Session ID</th>
|
|
||||||
<th className="px-4 py-2 font-medium">Provider</th>
|
|
||||||
<th className="px-4 py-2 font-medium">Model</th>
|
|
||||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Prompts</th>
|
|
||||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Duration</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sessions.map((s) => (
|
|
||||||
<tr key={s.id} className="border-b border-surface-border last:border-b-0">
|
|
||||||
<td className="max-w-[200px] truncate px-4 py-2 font-mono text-xs text-text-primary">
|
|
||||||
{s.id}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-xs text-text-muted">{s.provider}</td>
|
|
||||||
<td className="px-4 py-2 text-xs text-text-muted">{s.modelId}</td>
|
|
||||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
||||||
{s.promptCount}
|
|
||||||
</td>
|
|
||||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
||||||
{formatDuration(s.durationMs)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
const seconds = Math.floor(ms / 1000);
|
|
||||||
if (seconds < 60) return `${seconds}s`;
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
return `${hours}h ${minutes % 60}m`;
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
import { getSocket } from '@/lib/socket';
|
|
||||||
import type { Conversation, Message } from '@/lib/types';
|
|
||||||
import { ConversationList } from '@/components/chat/conversation-list';
|
|
||||||
import { MessageBubble } from '@/components/chat/message-bubble';
|
|
||||||
import { ChatInput } from '@/components/chat/chat-input';
|
|
||||||
import { StreamingMessage } from '@/components/chat/streaming-message';
|
|
||||||
|
|
||||||
export default function ChatPage(): React.ReactElement {
|
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [streamingText, setStreamingText] = useState('');
|
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Load conversations on mount
|
|
||||||
useEffect(() => {
|
|
||||||
api<Conversation[]>('/api/conversations')
|
|
||||||
.then(setConversations)
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Load messages when active conversation changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeId) {
|
|
||||||
setMessages([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
api<Message[]>(`/api/conversations/${activeId}/messages`)
|
|
||||||
.then(setMessages)
|
|
||||||
.catch(() => {});
|
|
||||||
}, [activeId]);
|
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
|
||||||
useEffect(() => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}, [messages, streamingText]);
|
|
||||||
|
|
||||||
// Socket.io setup
|
|
||||||
useEffect(() => {
|
|
||||||
const socket = getSocket();
|
|
||||||
socket.connect();
|
|
||||||
|
|
||||||
socket.on('agent:text', (data: { conversationId: string; text: string }) => {
|
|
||||||
setStreamingText((prev) => prev + data.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('agent:start', () => {
|
|
||||||
setIsStreaming(true);
|
|
||||||
setStreamingText('');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('agent:end', (data: { conversationId: string }) => {
|
|
||||||
setIsStreaming(false);
|
|
||||||
setStreamingText('');
|
|
||||||
// Reload messages to get the final persisted version
|
|
||||||
api<Message[]>(`/api/conversations/${data.conversationId}/messages`)
|
|
||||||
.then(setMessages)
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (data: { error: string }) => {
|
|
||||||
setIsStreaming(false);
|
|
||||||
setStreamingText('');
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: `error-${Date.now()}`,
|
|
||||||
conversationId: '',
|
|
||||||
role: 'system',
|
|
||||||
content: `Error: ${data.error}`,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off('agent:text');
|
|
||||||
socket.off('agent:start');
|
|
||||||
socket.off('agent:end');
|
|
||||||
socket.off('error');
|
|
||||||
socket.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleNewConversation = useCallback(async () => {
|
|
||||||
const conv = await api<Conversation>('/api/conversations', {
|
|
||||||
method: 'POST',
|
|
||||||
body: { title: 'New conversation' },
|
|
||||||
});
|
|
||||||
setConversations((prev) => [conv, ...prev]);
|
|
||||||
setActiveId(conv.id);
|
|
||||||
setMessages([]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSend = useCallback(
|
|
||||||
async (content: string) => {
|
|
||||||
let convId = activeId;
|
|
||||||
|
|
||||||
// Auto-create conversation if none selected
|
|
||||||
if (!convId) {
|
|
||||||
const conv = await api<Conversation>('/api/conversations', {
|
|
||||||
method: 'POST',
|
|
||||||
body: { title: content.slice(0, 50) },
|
|
||||||
});
|
|
||||||
setConversations((prev) => [conv, ...prev]);
|
|
||||||
setActiveId(conv.id);
|
|
||||||
convId = conv.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimistic user message
|
|
||||||
const userMsg: Message = {
|
|
||||||
id: `temp-${Date.now()}`,
|
|
||||||
conversationId: convId,
|
|
||||||
role: 'user',
|
|
||||||
content,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
setMessages((prev) => [...prev, userMsg]);
|
|
||||||
|
|
||||||
// Persist user message
|
|
||||||
await api<Message>(`/api/conversations/${convId}/messages`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: { role: 'user', content },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send to WebSocket for streaming response
|
|
||||||
const socket = getSocket();
|
|
||||||
socket.emit('message', { conversationId: convId, content });
|
|
||||||
},
|
|
||||||
[activeId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="-m-6 flex h-[calc(100vh-3.5rem)]">
|
|
||||||
<ConversationList
|
|
||||||
conversations={conversations}
|
|
||||||
activeId={activeId}
|
|
||||||
onSelect={setActiveId}
|
|
||||||
onNew={handleNewConversation}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
{activeId ? (
|
|
||||||
<>
|
|
||||||
<div className="flex-1 space-y-4 overflow-y-auto p-6">
|
|
||||||
{messages.map((msg) => (
|
|
||||||
<MessageBubble key={msg.id} message={msg} />
|
|
||||||
))}
|
|
||||||
{isStreaming && <StreamingMessage text={streamingText} />}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
<ChatInput onSend={handleSend} disabled={isStreaming} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-1 items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-lg font-medium text-text-secondary">Welcome to Mosaic Chat</h2>
|
|
||||||
<p className="mt-1 text-sm text-text-muted">
|
|
||||||
Select a conversation or start a new one
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleNewConversation}
|
|
||||||
className="mt-4 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Start new conversation
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { AppShell } from '@/components/layout/app-shell';
|
|
||||||
import { AuthGuard } from '@/components/auth-guard';
|
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: ReactNode }): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<AuthGuard>
|
|
||||||
<AppShell>{children}</AppShell>
|
|
||||||
</AuthGuard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
import type { Project } from '@/lib/types';
|
|
||||||
import { ProjectCard } from '@/components/projects/project-card';
|
|
||||||
|
|
||||||
export default function ProjectsPage(): React.ReactElement {
|
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api<Project[]>('/api/projects')
|
|
||||||
.then(setProjects)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleProjectClick = useCallback((project: Project) => {
|
|
||||||
console.log('Project clicked:', project.id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-6 flex items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-semibold">Projects</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<p className="py-8 text-center text-sm text-text-muted">Loading projects...</p>
|
|
||||||
) : projects.length === 0 ? (
|
|
||||||
<div className="py-12 text-center">
|
|
||||||
<h2 className="text-lg font-medium text-text-secondary">No projects yet</h2>
|
|
||||||
<p className="mt-1 text-sm text-text-muted">
|
|
||||||
Projects will appear here when created via the gateway API
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{projects.map((project) => (
|
|
||||||
<ProjectCard key={project.id} project={project} onClick={handleProjectClick} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mission status section */}
|
|
||||||
<MissionStatus />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MissionStatus(): React.ReactElement {
|
|
||||||
const [mission, setMission] = useState<Record<string, unknown> | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api<Record<string, unknown>>('/api/coord/status')
|
|
||||||
.then(setMission)
|
|
||||||
.catch(() => setMission(null))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="mt-8">
|
|
||||||
<h2 className="mb-4 text-lg font-semibold">Active Mission</h2>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-sm text-text-muted">Loading mission status...</p>
|
|
||||||
) : !mission ? (
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-6 text-center">
|
|
||||||
<p className="text-sm text-text-muted">No active mission detected</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<StatCard label="Mission" value={String(mission['missionId'] ?? 'Unknown')} />
|
|
||||||
<StatCard label="Phase" value={String(mission['currentPhase'] ?? '—')} />
|
|
||||||
<StatCard
|
|
||||||
label="Tasks"
|
|
||||||
value={`${mission['completedTasks'] ?? 0} / ${mission['totalTasks'] ?? 0}`}
|
|
||||||
/>
|
|
||||||
<StatCard label="Status" value={String(mission['status'] ?? '—')} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatCard({ label, value }: { label: string; value: string }): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg bg-surface-elevated p-3">
|
|
||||||
<p className="text-xs text-text-muted">{label}</p>
|
|
||||||
<p className="mt-1 text-sm font-medium text-text-primary">{value}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
import { useSession } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
interface ProviderInfo {
|
|
||||||
name: string;
|
|
||||||
enabled: boolean;
|
|
||||||
modelCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModelInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
provider: string;
|
|
||||||
contextWindow: number;
|
|
||||||
reasoning: boolean;
|
|
||||||
cost: { input: number; output: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage(): React.ReactElement {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
||||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Promise.all([
|
|
||||||
api<ProviderInfo[]>('/api/providers').catch(() => []),
|
|
||||||
api<ModelInfo[]>('/api/providers/models').catch(() => []),
|
|
||||||
])
|
|
||||||
.then(([p, m]) => {
|
|
||||||
setProviders(p);
|
|
||||||
setModels(m);
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto max-w-3xl space-y-8">
|
|
||||||
<h1 className="text-2xl font-semibold">Settings</h1>
|
|
||||||
|
|
||||||
{/* Profile */}
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Profile</h2>
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
||||||
{session?.user ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Field label="Name" value={session.user.name ?? '—'} />
|
|
||||||
<Field label="Email" value={session.user.email} />
|
|
||||||
<Field label="User ID" value={session.user.id} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-text-muted">Not signed in</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Providers */}
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">LLM Providers</h2>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-sm text-text-muted">Loading providers...</p>
|
|
||||||
) : providers.length === 0 ? (
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
||||||
<p className="text-sm text-text-muted">No providers configured</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{providers.map((p) => (
|
|
||||||
<div
|
|
||||||
key={p.name}
|
|
||||||
className="flex items-center justify-between rounded-lg border border-surface-border bg-surface-card p-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-text-primary">{p.name}</p>
|
|
||||||
<p className="text-xs text-text-muted">{p.modelCount} models available</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`rounded-full px-2 py-0.5 text-xs ${
|
|
||||||
p.enabled ? 'bg-success/20 text-success' : 'bg-gray-600/20 text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{p.enabled ? 'Active' : 'Disabled'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Models */}
|
|
||||||
<section>
|
|
||||||
<h2 className="mb-4 text-lg font-medium text-text-secondary">Available Models</h2>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-sm text-text-muted">Loading models...</p>
|
|
||||||
) : models.length === 0 ? (
|
|
||||||
<div className="rounded-lg border border-surface-border bg-surface-card p-4">
|
|
||||||
<p className="text-sm text-text-muted">No models available</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
|
||||||
<th className="px-4 py-2 font-medium">Model</th>
|
|
||||||
<th className="px-4 py-2 font-medium">Provider</th>
|
|
||||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Context</th>
|
|
||||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Cost (in/out)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{models.map((m) => (
|
|
||||||
<tr
|
|
||||||
key={`${m.provider}-${m.id}`}
|
|
||||||
className="border-b border-surface-border last:border-b-0"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-2 text-sm text-text-primary">
|
|
||||||
{m.name}
|
|
||||||
{m.reasoning && (
|
|
||||||
<span className="ml-2 text-xs text-purple-400">reasoning</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-xs text-text-muted">{m.provider}</td>
|
|
||||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
||||||
{(m.contextWindow / 1000).toFixed(0)}k
|
|
||||||
</td>
|
|
||||||
<td className="hidden px-4 py-2 text-xs text-text-muted md:table-cell">
|
|
||||||
${m.cost.input} / ${m.cost.output}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Field({ label, value }: { label: string; value: string }): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm text-text-muted">{label}</span>
|
|
||||||
<span className="text-sm text-text-primary">{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { api } from '@/lib/api';
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Task } from '@/lib/types';
|
|
||||||
import { KanbanBoard } from '@/components/tasks/kanban-board';
|
|
||||||
import { TaskListView } from '@/components/tasks/task-list-view';
|
|
||||||
|
|
||||||
type ViewMode = 'list' | 'kanban';
|
|
||||||
|
|
||||||
export default function TasksPage(): React.ReactElement {
|
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
|
||||||
const [view, setView] = useState<ViewMode>('kanban');
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api<Task[]>('/api/tasks')
|
|
||||||
.then(setTasks)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleTaskClick = useCallback((task: Task) => {
|
|
||||||
// Task detail view will be added in future iteration
|
|
||||||
console.log('Task clicked:', task.id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-6 flex items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-semibold">Tasks</h1>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex rounded-lg border border-surface-border">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setView('list')}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-1.5 text-xs transition-colors',
|
|
||||||
view === 'list'
|
|
||||||
? 'bg-surface-elevated text-text-primary'
|
|
||||||
: 'text-text-muted hover:text-text-secondary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
List
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setView('kanban')}
|
|
||||||
className={cn(
|
|
||||||
'px-3 py-1.5 text-xs transition-colors',
|
|
||||||
view === 'kanban'
|
|
||||||
? 'bg-surface-elevated text-text-primary'
|
|
||||||
: 'text-text-muted hover:text-text-secondary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Kanban
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<p className="py-8 text-center text-sm text-text-muted">Loading tasks...</p>
|
|
||||||
) : view === 'kanban' ? (
|
|
||||||
<KanbanBoard tasks={tasks} onTaskClick={handleTaskClick} />
|
|
||||||
) : (
|
|
||||||
<TaskListView tasks={tasks} onTaskClick={handleTaskClick} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Mosaic Stack design tokens mapped to Tailwind v4 theme.
|
|
||||||
* Source: @mosaic/design-tokens (AD-13)
|
|
||||||
* Fonts: Outfit (sans), Fira Code (mono)
|
|
||||||
* Palette: deep blue-grays + blue/purple/teal accents
|
|
||||||
* Default: dark theme
|
|
||||||
*/
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
/* ─── Fonts ─── */
|
|
||||||
--font-sans: 'Outfit', system-ui, -apple-system, sans-serif;
|
|
||||||
--font-mono: 'Fira Code', ui-monospace, Menlo, monospace;
|
|
||||||
|
|
||||||
/* ─── Neutral blue-gray scale ─── */
|
|
||||||
--color-gray-50: #f0f2f5;
|
|
||||||
--color-gray-100: #dce0e8;
|
|
||||||
--color-gray-200: #b8c0cc;
|
|
||||||
--color-gray-300: #8e99a9;
|
|
||||||
--color-gray-400: #6b7a8d;
|
|
||||||
--color-gray-500: #4e5d70;
|
|
||||||
--color-gray-600: #3b4859;
|
|
||||||
--color-gray-700: #2a3544;
|
|
||||||
--color-gray-800: #1c2433;
|
|
||||||
--color-gray-900: #111827;
|
|
||||||
--color-gray-950: #0a0f1a;
|
|
||||||
|
|
||||||
/* ─── Primary — blue ─── */
|
|
||||||
--color-blue-50: #eff4ff;
|
|
||||||
--color-blue-100: #dae5ff;
|
|
||||||
--color-blue-200: #bdd1ff;
|
|
||||||
--color-blue-300: #8fb4ff;
|
|
||||||
--color-blue-400: #5b8bff;
|
|
||||||
--color-blue-500: #3b6cf7;
|
|
||||||
--color-blue-600: #2551e0;
|
|
||||||
--color-blue-700: #1d40c0;
|
|
||||||
--color-blue-800: #1e369c;
|
|
||||||
--color-blue-900: #1e317b;
|
|
||||||
--color-blue-950: #162050;
|
|
||||||
|
|
||||||
/* ─── Accent — purple ─── */
|
|
||||||
--color-purple-50: #f3f0ff;
|
|
||||||
--color-purple-100: #e7dfff;
|
|
||||||
--color-purple-200: #d2c3ff;
|
|
||||||
--color-purple-300: #b49aff;
|
|
||||||
--color-purple-400: #9466ff;
|
|
||||||
--color-purple-500: #7c3aed;
|
|
||||||
--color-purple-600: #6d28d9;
|
|
||||||
--color-purple-700: #5b21b6;
|
|
||||||
--color-purple-800: #4c1d95;
|
|
||||||
--color-purple-900: #3b1578;
|
|
||||||
--color-purple-950: #230d4d;
|
|
||||||
|
|
||||||
/* ─── Accent — teal ─── */
|
|
||||||
--color-teal-50: #effcf9;
|
|
||||||
--color-teal-100: #d0f7ef;
|
|
||||||
--color-teal-200: #a4eddf;
|
|
||||||
--color-teal-300: #6fddcb;
|
|
||||||
--color-teal-400: #3ec5b2;
|
|
||||||
--color-teal-500: #25aa99;
|
|
||||||
--color-teal-600: #1c897e;
|
|
||||||
--color-teal-700: #1b6e66;
|
|
||||||
--color-teal-800: #1a5853;
|
|
||||||
--color-teal-900: #194945;
|
|
||||||
--color-teal-950: #082d2b;
|
|
||||||
|
|
||||||
/* ─── Semantic surface tokens ─── */
|
|
||||||
--color-surface-bg: #0a0f1a;
|
|
||||||
--color-surface-card: #111827;
|
|
||||||
--color-surface-elevated: #1c2433;
|
|
||||||
--color-surface-border: #2a3544;
|
|
||||||
|
|
||||||
/* ─── Semantic text tokens ─── */
|
|
||||||
--color-text-primary: #f0f2f5;
|
|
||||||
--color-text-secondary: #8e99a9;
|
|
||||||
--color-text-muted: #6b7a8d;
|
|
||||||
|
|
||||||
/* ─── Status colors ─── */
|
|
||||||
--color-success: #22c55e;
|
|
||||||
--color-warning: #f59e0b;
|
|
||||||
--color-error: #ef4444;
|
|
||||||
--color-info: #3b82f6;
|
|
||||||
|
|
||||||
/* ─── Sidebar width ─── */
|
|
||||||
--spacing-sidebar: 16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Base styles ─── */
|
|
||||||
body {
|
|
||||||
background-color: var(--color-surface-bg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Scrollbar styling ─── */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--color-gray-600);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--color-gray-500);
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Outfit, Fira_Code } from 'next/font/google';
|
|
||||||
import './globals.css';
|
|
||||||
|
|
||||||
const outfit = Outfit({
|
|
||||||
subsets: ['latin'],
|
|
||||||
variable: '--font-sans',
|
|
||||||
display: 'swap',
|
|
||||||
weight: ['300', '400', '500', '600', '700'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const firaCode = Fira_Code({
|
|
||||||
subsets: ['latin'],
|
|
||||||
variable: '--font-mono',
|
|
||||||
display: 'swap',
|
|
||||||
weight: ['400', '500', '700'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Mosaic',
|
title: 'Mosaic',
|
||||||
description: 'Mosaic Stack Dashboard',
|
description: 'Mosaic Stack Dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }): React.ReactElement {
|
export default function RootLayout({ children }: { children: ReactNode }): ReactNode {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`dark ${outfit.variable} ${firaCode.variable}`}>
|
<html lang="en">
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { redirect } from 'next/navigation';
|
export default function HomePage(): React.ReactElement {
|
||||||
|
return (
|
||||||
export default function HomePage(): never {
|
<main>
|
||||||
redirect('/chat');
|
<h1>Mosaic Stack</h1>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useSession } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
interface AuthGuardProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthGuard({ children }: AuthGuardProps): React.ReactElement | null {
|
|
||||||
const { data: session, isPending } = useSession();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPending && !session) {
|
|
||||||
router.replace('/login');
|
|
||||||
}
|
|
||||||
}, [isPending, session, router]);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
|
||||||
<div className="text-sm text-text-muted">Loading...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
|
||||||
onSend: (content: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatInput({ onSend, disabled }: ChatInputProps): React.ReactElement {
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent): void {
|
|
||||||
e.preventDefault();
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed || disabled) return;
|
|
||||||
onSend(trimmed);
|
|
||||||
setValue('');
|
|
||||||
textareaRef.current?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>): void {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="border-t border-surface-border bg-surface-card p-4">
|
|
||||||
<div className="flex items-end gap-3">
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
disabled={disabled}
|
|
||||||
rows={1}
|
|
||||||
placeholder="Type a message... (Enter to send, Shift+Enter for newline)"
|
|
||||||
className="max-h-32 min-h-[2.5rem] flex-1 resize-none rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={disabled || !value.trim()}
|
|
||||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Conversation } from '@/lib/types';
|
|
||||||
|
|
||||||
interface ConversationListProps {
|
|
||||||
conversations: Conversation[];
|
|
||||||
activeId: string | null;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
onNew: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConversationList({
|
|
||||||
conversations,
|
|
||||||
activeId,
|
|
||||||
onSelect,
|
|
||||||
onNew,
|
|
||||||
}: ConversationListProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-64 flex-col border-r border-surface-border bg-surface-card">
|
|
||||||
<div className="flex items-center justify-between p-3">
|
|
||||||
<h2 className="text-sm font-medium text-text-secondary">Conversations</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onNew}
|
|
||||||
className="rounded-md px-2 py-1 text-xs text-blue-400 transition-colors hover:bg-surface-elevated"
|
|
||||||
>
|
|
||||||
+ New
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{conversations.length === 0 && (
|
|
||||||
<p className="px-3 py-2 text-xs text-text-muted">No conversations yet</p>
|
|
||||||
)}
|
|
||||||
{conversations.map((conv) => (
|
|
||||||
<button
|
|
||||||
key={conv.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelect(conv.id)}
|
|
||||||
className={cn(
|
|
||||||
'w-full px-3 py-2 text-left text-sm transition-colors',
|
|
||||||
activeId === conv.id
|
|
||||||
? 'bg-blue-600/20 text-blue-400'
|
|
||||||
: 'text-text-secondary hover:bg-surface-elevated',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="block truncate">{conv.title ?? 'Untitled'}</span>
|
|
||||||
<span className="block text-xs text-text-muted">
|
|
||||||
{new Date(conv.updatedAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Message } from '@/lib/types';
|
|
||||||
|
|
||||||
interface MessageBubbleProps {
|
|
||||||
message: Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MessageBubble({ message }: MessageBubbleProps): React.ReactElement {
|
|
||||||
const isUser = message.role === 'user';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex', isUser ? 'justify-end' : 'justify-start')}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'max-w-[75%] rounded-xl px-4 py-3 text-sm',
|
|
||||||
isUser
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'border border-surface-border bg-surface-elevated text-text-primary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="whitespace-pre-wrap break-words">{message.content}</div>
|
|
||||||
<div
|
|
||||||
className={cn('mt-1 text-right text-xs', isUser ? 'text-blue-200' : 'text-text-muted')}
|
|
||||||
>
|
|
||||||
{new Date(message.createdAt).toLocaleTimeString([], {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
/** Renders an in-progress assistant message from streaming text. */
|
|
||||||
interface StreamingMessageProps {
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StreamingMessage({ text }: StreamingMessageProps): React.ReactElement | null {
|
|
||||||
if (!text) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="max-w-[75%] rounded-xl border border-surface-border bg-surface-elevated px-4 py-3 text-sm text-text-primary">
|
|
||||||
<div className="whitespace-pre-wrap break-words">{text}</div>
|
|
||||||
<div className="mt-1 flex items-center gap-1 text-xs text-text-muted">
|
|
||||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-blue-500" />
|
|
||||||
Thinking...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useSession } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
interface GuestGuardProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Redirects authenticated users away from auth pages. */
|
|
||||||
export function GuestGuard({ children }: GuestGuardProps): React.ReactElement | null {
|
|
||||||
const { data: session, isPending } = useSession();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPending && session) {
|
|
||||||
router.replace('/chat');
|
|
||||||
}
|
|
||||||
}, [isPending, session, router]);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
|
||||||
<div className="text-sm text-text-muted">Loading...</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { Sidebar } from './sidebar';
|
|
||||||
import { Topbar } from './topbar';
|
|
||||||
|
|
||||||
interface AppShellProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppShell({ children }: AppShellProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen">
|
|
||||||
<Sidebar />
|
|
||||||
<div className="pl-sidebar">
|
|
||||||
<Topbar />
|
|
||||||
<main className="p-6">{children}</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
|
|
||||||
interface NavItem {
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
|
||||||
{ label: 'Chat', href: '/chat', icon: '💬' },
|
|
||||||
{ label: 'Tasks', href: '/tasks', icon: '📋' },
|
|
||||||
{ label: 'Projects', href: '/projects', icon: '📁' },
|
|
||||||
{ label: 'Settings', href: '/settings', icon: '⚙️' },
|
|
||||||
{ label: 'Admin', href: '/admin', icon: '🛡️' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar(): React.ReactElement {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="fixed left-0 top-0 z-30 flex h-screen w-sidebar flex-col border-r border-surface-border bg-surface-card">
|
|
||||||
<div className="flex h-14 items-center px-4">
|
|
||||||
<Link href="/" className="text-lg font-semibold text-text-primary">
|
|
||||||
Mosaic
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="flex-1 space-y-1 px-2 py-2">
|
|
||||||
{navItems.map((item) => {
|
|
||||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.href}
|
|
||||||
href={item.href}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
|
|
||||||
isActive
|
|
||||||
? 'bg-blue-600/20 text-blue-400'
|
|
||||||
: 'text-text-secondary hover:bg-surface-elevated hover:text-text-primary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-base" aria-hidden="true">
|
|
||||||
{item.icon}
|
|
||||||
</span>
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="border-t border-surface-border p-4">
|
|
||||||
<p className="text-xs text-text-muted">Mosaic Stack v0.0.4</p>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useSession, signOut } from '@/lib/auth-client';
|
|
||||||
|
|
||||||
export function Topbar(): React.ReactElement {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
async function handleSignOut(): Promise<void> {
|
|
||||||
await signOut();
|
|
||||||
router.replace('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-20 flex h-14 items-center justify-between border-b border-surface-border bg-surface-card/80 px-6 backdrop-blur-sm">
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{session?.user ? (
|
|
||||||
<>
|
|
||||||
<span className="text-sm text-text-secondary">
|
|
||||||
{session.user.name ?? session.user.email}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSignOut}
|
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-text-muted transition-colors hover:bg-surface-elevated hover:text-text-primary"
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-text-muted">Not signed in</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Project } from '@/lib/types';
|
|
||||||
|
|
||||||
interface ProjectCardProps {
|
|
||||||
project: Project;
|
|
||||||
onClick: (project: Project) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
|
||||||
active: 'bg-success/20 text-success',
|
|
||||||
paused: 'bg-warning/20 text-warning',
|
|
||||||
completed: 'bg-blue-600/20 text-blue-400',
|
|
||||||
archived: 'bg-gray-600/20 text-gray-400',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProjectCard({ project, onClick }: ProjectCardProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onClick(project)}
|
|
||||||
className="w-full rounded-lg border border-surface-border bg-surface-card p-4 text-left transition-colors hover:border-gray-500"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<h3 className="text-sm font-medium text-text-primary">{project.name}</h3>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'rounded-full px-2 py-0.5 text-xs',
|
|
||||||
statusColors[project.status] ?? 'bg-gray-600/20 text-gray-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{project.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{project.description && (
|
|
||||||
<p className="mt-2 line-clamp-2 text-xs text-text-muted">{project.description}</p>
|
|
||||||
)}
|
|
||||||
<p className="mt-3 text-xs text-text-muted">
|
|
||||||
Created {new Date(project.createdAt).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { Task, TaskStatus } from '@/lib/types';
|
|
||||||
import { TaskCard } from './task-card';
|
|
||||||
|
|
||||||
interface KanbanBoardProps {
|
|
||||||
tasks: Task[];
|
|
||||||
onTaskClick: (task: Task) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: { id: TaskStatus; label: string }[] = [
|
|
||||||
{ id: 'not-started', label: 'Not Started' },
|
|
||||||
{ id: 'in-progress', label: 'In Progress' },
|
|
||||||
{ id: 'blocked', label: 'Blocked' },
|
|
||||||
{ id: 'done', label: 'Done' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function KanbanBoard({ tasks, onTaskClick }: KanbanBoardProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
||||||
{columns.map((col) => {
|
|
||||||
const columnTasks = tasks.filter((t) => t.status === col.id);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={col.id}
|
|
||||||
className="flex w-72 shrink-0 flex-col rounded-lg border border-surface-border bg-surface-elevated"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between border-b border-surface-border px-3 py-2">
|
|
||||||
<h3 className="text-sm font-medium text-text-secondary">{col.label}</h3>
|
|
||||||
<span className="rounded-full bg-surface-card px-2 py-0.5 text-xs text-text-muted">
|
|
||||||
{columnTasks.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-2 p-2">
|
|
||||||
{columnTasks.length === 0 && (
|
|
||||||
<p className="py-4 text-center text-xs text-text-muted">No tasks</p>
|
|
||||||
)}
|
|
||||||
{columnTasks.map((task) => (
|
|
||||||
<TaskCard key={task.id} task={task} onClick={onTaskClick} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Task } from '@/lib/types';
|
|
||||||
|
|
||||||
interface TaskCardProps {
|
|
||||||
task: Task;
|
|
||||||
onClick: (task: Task) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
|
||||||
critical: 'text-error',
|
|
||||||
high: 'text-warning',
|
|
||||||
medium: 'text-blue-400',
|
|
||||||
low: 'text-text-muted',
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusBadgeColors: Record<string, string> = {
|
|
||||||
'not-started': 'bg-gray-600/20 text-gray-300',
|
|
||||||
'in-progress': 'bg-blue-600/20 text-blue-400',
|
|
||||||
blocked: 'bg-error/20 text-error',
|
|
||||||
done: 'bg-success/20 text-success',
|
|
||||||
cancelled: 'bg-gray-600/20 text-gray-500',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TaskCard({ task, onClick }: TaskCardProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onClick(task)}
|
|
||||||
className="w-full rounded-lg border border-surface-border bg-surface-card p-3 text-left transition-colors hover:border-gray-500"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<span className="text-sm font-medium text-text-primary">{task.title}</span>
|
|
||||||
<span className={cn('text-xs', priorityColors[task.priority])}>{task.priority}</span>
|
|
||||||
</div>
|
|
||||||
{task.description && (
|
|
||||||
<p className="mt-1 line-clamp-2 text-xs text-text-muted">{task.description}</p>
|
|
||||||
)}
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'rounded-full px-2 py-0.5 text-xs',
|
|
||||||
statusBadgeColors[task.status] ?? 'bg-gray-600/20 text-gray-400',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{task.status}
|
|
||||||
</span>
|
|
||||||
{task.dueDate && (
|
|
||||||
<span className="text-xs text-text-muted">
|
|
||||||
{new Date(task.dueDate).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/cn';
|
|
||||||
import type { Task } from '@/lib/types';
|
|
||||||
|
|
||||||
interface TaskListViewProps {
|
|
||||||
tasks: Task[];
|
|
||||||
onTaskClick: (task: Task) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
|
||||||
critical: 'text-error',
|
|
||||||
high: 'text-warning',
|
|
||||||
medium: 'text-blue-400',
|
|
||||||
low: 'text-text-muted',
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
|
||||||
'not-started': 'text-gray-400',
|
|
||||||
'in-progress': 'text-blue-400',
|
|
||||||
blocked: 'text-error',
|
|
||||||
done: 'text-success',
|
|
||||||
cancelled: 'text-gray-500',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TaskListView({ tasks, onTaskClick }: TaskListViewProps): React.ReactElement {
|
|
||||||
if (tasks.length === 0) {
|
|
||||||
return <p className="py-8 text-center text-sm text-text-muted">No tasks found</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden rounded-lg border border-surface-border">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-surface-border bg-surface-elevated text-left text-xs text-text-muted">
|
|
||||||
<th className="px-4 py-2 font-medium">Title</th>
|
|
||||||
<th className="px-4 py-2 font-medium">Status</th>
|
|
||||||
<th className="px-4 py-2 font-medium">Priority</th>
|
|
||||||
<th className="hidden px-4 py-2 font-medium md:table-cell">Due</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{tasks.map((task) => (
|
|
||||||
<tr
|
|
||||||
key={task.id}
|
|
||||||
onClick={() => onTaskClick(task)}
|
|
||||||
className="cursor-pointer border-b border-surface-border transition-colors last:border-b-0 hover:bg-surface-elevated"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">{task.title}</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className={cn('text-xs', statusColors[task.status])}>{task.status}</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className={cn('text-xs', priorityColors[task.priority])}>
|
|
||||||
{task.priority}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="hidden px-4 py-3 text-xs text-text-muted md:table-cell">
|
|
||||||
{task.dueDate ? new Date(task.dueDate).toLocaleDateString() : '—'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
|
||||||
|
|
||||||
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
|
|
||||||
body?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiError {
|
|
||||||
statusCode: number;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch wrapper for the Mosaic gateway API.
|
|
||||||
* Sends credentials (cookies) and JSON body automatically.
|
|
||||||
*/
|
|
||||||
export async function api<T>(path: string, init?: ApiRequestInit): Promise<T> {
|
|
||||||
const { body, headers: customHeaders, ...rest } = init ?? {};
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
Accept: 'application/json',
|
|
||||||
...(customHeaders as Record<string, string>),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (body !== undefined) {
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${GATEWAY_URL}${path}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
...rest,
|
|
||||||
headers,
|
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const errorBody = (await res.json().catch(() => ({
|
|
||||||
statusCode: res.status,
|
|
||||||
message: res.statusText,
|
|
||||||
}))) as ApiError;
|
|
||||||
throw Object.assign(new Error(errorBody.message), {
|
|
||||||
statusCode: errorBody.statusCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status === 204) return undefined as T;
|
|
||||||
return (await res.json()) as T;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createAuthClient } from 'better-auth/react';
|
|
||||||
|
|
||||||
export const authClient: ReturnType<typeof createAuthClient> = createAuthClient({
|
|
||||||
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { useSession, signIn, signUp, signOut } = authClient;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
/** Merge and deduplicate Tailwind class names. */
|
|
||||||
export function cn(...inputs: ClassValue[]): string {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { io, type Socket } from 'socket.io-client';
|
|
||||||
|
|
||||||
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
|
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
|
||||||
|
|
||||||
export function getSocket(): Socket {
|
|
||||||
if (!socket) {
|
|
||||||
socket = io(`${GATEWAY_URL}/chat`, {
|
|
||||||
withCredentials: true,
|
|
||||||
autoConnect: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/** Conversation returned by the gateway API. */
|
|
||||||
export interface Conversation {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
title: string | null;
|
|
||||||
projectId: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Message within a conversation. */
|
|
||||||
export interface Message {
|
|
||||||
id: string;
|
|
||||||
conversationId: string;
|
|
||||||
role: 'user' | 'assistant' | 'system';
|
|
||||||
content: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Task statuses. */
|
|
||||||
export type TaskStatus = 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
||||||
|
|
||||||
/** Task priorities. */
|
|
||||||
export type TaskPriority = 'critical' | 'high' | 'medium' | 'low';
|
|
||||||
|
|
||||||
/** Task returned by the gateway API. */
|
|
||||||
export interface Task {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
status: TaskStatus;
|
|
||||||
priority: TaskPriority;
|
|
||||||
projectId: string | null;
|
|
||||||
missionId: string | null;
|
|
||||||
assignee: string | null;
|
|
||||||
tags: string[] | null;
|
|
||||||
dueDate: string | null;
|
|
||||||
metadata: Record<string, unknown> | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Project statuses. */
|
|
||||||
export type ProjectStatus = 'active' | 'paused' | 'completed' | 'archived';
|
|
||||||
|
|
||||||
/** Project returned by the gateway API. */
|
|
||||||
export interface Project {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
status: ProjectStatus;
|
|
||||||
userId: string;
|
|
||||||
metadata: Record<string, unknown> | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
11
apps/web/tailwind.config.ts
Normal file
11
apps/web/tailwind.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: ['./src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
**ID:** mvp-20260312
|
**ID:** mvp-20260312
|
||||||
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
||||||
**Phase:** Execution
|
**Phase:** Execution
|
||||||
**Current Milestone:** Phase 4: Memory & Intelligence (v0.0.5)
|
**Current Milestone:** Phase 3: Web Dashboard (v0.0.4)
|
||||||
**Progress:** 4 / 8 milestones
|
**Progress:** 3 / 8 milestones
|
||||||
**Status:** active
|
**Status:** active
|
||||||
**Last Updated:** 2026-03-13 UTC
|
**Last Updated:** 2026-03-12 UTC
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
| 0 | ms-157 | Phase 0: Foundation (v0.0.1) | done | — | — | 2026-03-13 | 2026-03-13 |
|
| 0 | ms-157 | Phase 0: Foundation (v0.0.1) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||||
| 1 | ms-158 | Phase 1: Core API (v0.0.2) | done | — | — | 2026-03-13 | 2026-03-13 |
|
| 1 | ms-158 | Phase 1: Core API (v0.0.2) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||||
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
||||||
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | done | — | — | 2026-03-12 | 2026-03-13 |
|
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | not-started | — | — | — | — |
|
||||||
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | not-started | — | — | — | — |
|
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | not-started | — | — | — | — |
|
||||||
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | not-started | — | — | — | — |
|
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | not-started | — | — | — | — |
|
||||||
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
||||||
@@ -66,9 +66,7 @@
|
|||||||
| 5 | claude-opus-4-6 | 2026-03-12 | — | context limit | P1-009 |
|
| 5 | claude-opus-4-6 | 2026-03-12 | — | context limit | P1-009 |
|
||||||
| 6 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-006, FIX-01 |
|
| 6 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-006, FIX-01 |
|
||||||
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
||||||
| 8 | claude-opus-4-6 | 2026-03-12 | — | context limit | Phase 2 complete |
|
| 8 | claude-opus-4-6 | 2026-03-12 | — | active | Phase 2 complete |
|
||||||
| 9 | claude-opus-4-6 | 2026-03-12 | — | context limit | P3-007 |
|
|
||||||
| 10 | claude-opus-4-6 | 2026-03-13 | — | active | P3-008 |
|
|
||||||
|
|
||||||
## Scratchpad
|
## Scratchpad
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,14 @@
|
|||||||
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
| P2-005 | done | Phase 2 | @mosaic/coord — migrate from v0, gateway integration | #77 | #23 |
|
||||||
| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 |
|
| P2-006 | done | Phase 2 | Agent session management — tmux + monitoring | #78 | #24 |
|
||||||
| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 |
|
| P2-007 | done | Phase 2 | Verify Phase 2 — multi-provider routing works | #79 | #25 |
|
||||||
| P3-001 | done | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | #82 | #26 |
|
| P3-001 | not-started | Phase 3 | apps/web scaffold — Next.js 16 + BetterAuth + Tailwind | — | #26 |
|
||||||
| P3-002 | done | Phase 3 | Auth pages — login, registration, SSO redirect | #83 | #27 |
|
| P3-002 | not-started | Phase 3 | Auth pages — login, registration, SSO redirect | — | #27 |
|
||||||
| P3-003 | done | Phase 3 | Chat UI — conversations, messages, streaming | #84 | #28 |
|
| P3-003 | not-started | Phase 3 | Chat UI — conversations, messages, streaming | — | #28 |
|
||||||
| P3-004 | done | Phase 3 | Task management — list view + kanban board | #86 | #29 |
|
| P3-004 | not-started | Phase 3 | Task management — list view + kanban board | — | #29 |
|
||||||
| P3-005 | done | Phase 3 | Project & mission views — dashboard + PRD viewer | #87 | #30 |
|
| P3-005 | not-started | Phase 3 | Project & mission views — dashboard + PRD viewer | — | #30 |
|
||||||
| P3-006 | done | Phase 3 | Settings — provider config, profile, integrations | #88 | #31 |
|
| P3-006 | not-started | Phase 3 | Settings — provider config, profile, integrations | — | #31 |
|
||||||
| P3-007 | done | Phase 3 | Admin panel — user management, RBAC | #89 | #32 |
|
| P3-007 | not-started | Phase 3 | Admin panel — user management, RBAC | — | #32 |
|
||||||
| P3-008 | done | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
| P3-008 | not-started | Phase 3 | Verify Phase 3 — web dashboard functional E2E | — | #33 |
|
||||||
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
| P4-001 | not-started | Phase 4 | @mosaic/memory — preference + insight stores | — | #34 |
|
||||||
| P4-002 | not-started | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
| P4-002 | not-started | Phase 4 | Semantic search — pgvector embeddings + search API | — | #35 |
|
||||||
| P4-003 | not-started | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
| P4-003 | not-started | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 |
|
||||||
|
|||||||
98
docs/plans/2026-03-13-gateway-security-hardening.md
Normal file
98
docs/plans/2026-03-13-gateway-security-hardening.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Gateway Security Hardening Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Finish the requested gateway security hardening fixes in the existing `fix/gateway-security` worktree and produce a PR-ready branch.
|
||||||
|
|
||||||
|
**Architecture:** Tighten NestJS gateway boundaries in-place by enforcing auth guards, session validation, ownership checks, DTO validation, and Fastify security defaults. Preserve the current module structure and existing ESM import conventions.
|
||||||
|
|
||||||
|
**Tech Stack:** NestJS 11, Fastify, Socket.IO, Better Auth, class-validator, Vitest, pnpm, TypeScript ESM
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Reconcile Security Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/gateway/src/chat/__tests__/chat-security.test.ts`
|
||||||
|
- Modify: `apps/gateway/src/__tests__/resource-ownership.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
- Encode the requested DTO constraints and socket-auth contract exactly.
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `pnpm --filter @mosaic/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts`
|
||||||
|
|
||||||
|
Expected: FAIL on current DTO/helper mismatch.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
- Update DTO/helper/controller code only where tests prove a gap.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run the same command and require green.
|
||||||
|
|
||||||
|
### Task 2: Align Gateway Runtime Hardening
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `apps/gateway/src/conversations/conversations.dto.ts`
|
||||||
|
- Modify: `apps/gateway/src/chat/chat.dto.ts`
|
||||||
|
- Modify: `apps/gateway/src/chat/chat.gateway-auth.ts`
|
||||||
|
- Modify: `apps/gateway/src/chat/chat.gateway.ts`
|
||||||
|
- Modify: `apps/gateway/src/main.ts`
|
||||||
|
- Modify: `apps/gateway/src/app.module.ts`
|
||||||
|
|
||||||
|
**Step 1: Verify remaining requested deltas**
|
||||||
|
|
||||||
|
- Confirm code matches requested guard, rate limit, helmet, body limit, env validation, and CORS settings.
|
||||||
|
|
||||||
|
**Step 2: Apply minimal patch**
|
||||||
|
|
||||||
|
- Keep changes scoped to requested behavior only.
|
||||||
|
|
||||||
|
**Step 3: Run targeted tests**
|
||||||
|
|
||||||
|
Run: `pnpm --filter @mosaic/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Verification, Review, and Delivery
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `docs/reports/code-review/gateway-security-20260313.md`
|
||||||
|
- Create: `docs/reports/qa/gateway-security-20260313.md`
|
||||||
|
- Modify: `docs/scratchpads/gateway-security-20260313.md`
|
||||||
|
|
||||||
|
**Step 1: Run baseline gates**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm lint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Perform manual code review**
|
||||||
|
|
||||||
|
- Record correctness/security/testing/doc findings.
|
||||||
|
|
||||||
|
**Step 3: Commit and publish**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix(gateway): security hardening — auth guards, ownership checks, validation, rate limiting"
|
||||||
|
git push origin fix/gateway-security
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Open PR and notify**
|
||||||
|
|
||||||
|
- Open PR titled `fix(gateway): security hardening — auth guards, ownership checks, validation, rate limiting`
|
||||||
|
- Run `openclaw system event --text "PR ready: mosaic-mono-v1 fix/gateway-security — 7 security fixes" --mode now`
|
||||||
|
- Remove worktree after PR is created.
|
||||||
23
docs/reports/code-review/gateway-security-20260313.md
Normal file
23
docs/reports/code-review/gateway-security-20260313.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Code Review Report — Gateway Security Hardening
|
||||||
|
|
||||||
|
## Scope Reviewed
|
||||||
|
|
||||||
|
- `apps/gateway/src/chat/chat.gateway-auth.ts`
|
||||||
|
- `apps/gateway/src/chat/chat.gateway.ts`
|
||||||
|
- `apps/gateway/src/conversations/conversations.dto.ts`
|
||||||
|
- `apps/gateway/src/chat/__tests__/chat-security.test.ts`
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
- No blocker findings in the final changed surface.
|
||||||
|
|
||||||
|
## Review Summary
|
||||||
|
|
||||||
|
- Correctness: socket auth helper now returns Better Auth session data unchanged, and gateway disconnects clients whose handshake does not narrow to a valid session payload
|
||||||
|
- Security: conversation role validation now rejects `system`; conversation content ceiling is 32k; chat request ceiling remains 10k
|
||||||
|
- Testing: targeted auth, ownership, and DTO regression tests pass
|
||||||
|
- Quality: `pnpm typecheck`, `pnpm lint`, and `pnpm format:check` all pass after the final edits
|
||||||
|
|
||||||
|
## Residual Risk
|
||||||
|
|
||||||
|
- `chat.gateway.ts` uses local narrowing around an `unknown` session result because the requested helper contract intentionally returns `unknown`.
|
||||||
39
docs/reports/qa/gateway-security-20260313.md
Normal file
39
docs/reports/qa/gateway-security-20260313.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# QA Report — Gateway Security Hardening
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Chat HTTP auth guard hardening
|
||||||
|
- Chat WebSocket session validation
|
||||||
|
- DTO validation rules for chat and conversation payloads
|
||||||
|
- Ownership regression coverage for by-id routes
|
||||||
|
|
||||||
|
## TDD
|
||||||
|
|
||||||
|
- Required: yes
|
||||||
|
- Applied: yes
|
||||||
|
- Red step: targeted tests failed on socket session reshaping and DTO role/length mismatches
|
||||||
|
- Green step: targeted tests passed after runtime and DTO alignment
|
||||||
|
|
||||||
|
## Baseline Verification
|
||||||
|
|
||||||
|
| Command | Result | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pnpm --filter @mosaic/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts` | pass | 3 test files passed, 20 tests passed |
|
||||||
|
| `pnpm typecheck` | pass | turbo completed 18/18 package typecheck tasks |
|
||||||
|
| `pnpm lint` | pass | turbo completed 18/18 package lint tasks |
|
||||||
|
| `pnpm format:check` | pass | `All matched files use Prettier code style!` |
|
||||||
|
|
||||||
|
## Situational Verification
|
||||||
|
|
||||||
|
| Acceptance Criterion | Verification Method | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Chat controller requires auth and current-user context | source assertion test | `chat-security.test.ts` checks `@UseGuards(AuthGuard)` and `@CurrentUser() user: { id: string }` |
|
||||||
|
| WebSocket handshake requires Better Auth session | unit tests for `validateSocketSession()` | null handshake returns `null`; valid handshake returns original session object |
|
||||||
|
| Conversation messages reject non-user/assistant roles | class-validator test | `system` role fails validation |
|
||||||
|
| Conversation messages enforce a 32k max length | class-validator test | `32_001` chars fail validation |
|
||||||
|
| Chat request payload enforces a 10k max length | class-validator test | `10_001` chars fail validation |
|
||||||
|
| By-id routes reject cross-user access | ownership regression tests | conversations, projects, missions, tasks each raise `ForbiddenException` for non-owner access |
|
||||||
|
|
||||||
|
## Residual Risk
|
||||||
|
|
||||||
|
- No live HTTP or WebSocket smoke test against a running gateway process was executed in this session.
|
||||||
68
docs/scratchpads/gateway-security-20260313.md
Normal file
68
docs/scratchpads/gateway-security-20260313.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Gateway Security Hardening Scratchpad
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- Date: 2026-03-13
|
||||||
|
- Worktree: `/home/jwoltje/src/mosaic-mono-v1-worktrees/sec-remediation`
|
||||||
|
- Branch: `fix/gateway-security`
|
||||||
|
- Scope: Finish 7 requested gateway security fixes without switching branches or worktrees
|
||||||
|
- Related tracker: worker task only; `docs/TASKS.md` is orchestrator-owned and left unchanged
|
||||||
|
- Budget assumption: no explicit token cap; keep scope limited to requested gateway/auth/validation hardening
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Complete the remaining gateway security hardening work:
|
||||||
|
|
||||||
|
1. Chat HTTP auth guard enforcement
|
||||||
|
2. Chat WebSocket session validation
|
||||||
|
3. Ownership checks on by-id CRUD routes
|
||||||
|
4. Global validation pipe and DTO enforcement
|
||||||
|
5. Rate limiting
|
||||||
|
6. Helmet security headers
|
||||||
|
7. Body limit and env validation
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Reconcile current worktree state against requested fixes.
|
||||||
|
2. Patch or extend tests first for DTO/auth behavior mismatches.
|
||||||
|
3. Implement minimal code changes to satisfy tests and requested behavior.
|
||||||
|
4. Run targeted gateway tests.
|
||||||
|
5. Run baseline gates: `pnpm typecheck`, `pnpm lint`.
|
||||||
|
6. Perform manual code review and record findings.
|
||||||
|
7. Commit, push branch, open PR, send OpenClaw event, remove worktree.
|
||||||
|
|
||||||
|
## Progress Log
|
||||||
|
|
||||||
|
### 2026-03-13T00:00 local
|
||||||
|
|
||||||
|
- Loaded required Mosaic/global/runtime instructions and applicable skills.
|
||||||
|
- Confirmed active worktree is `sec-remediation` and branch is already dirty with prior session changes.
|
||||||
|
- Identified remaining gaps: DTO validation mismatch and non-requested socket auth helper typing/behavior drift.
|
||||||
|
|
||||||
|
## TDD Notes
|
||||||
|
|
||||||
|
- Required: yes. This is security/auth/permission logic.
|
||||||
|
- Approach: update targeted unit tests first, verify failure, then patch code minimally.
|
||||||
|
|
||||||
|
## Verification Log
|
||||||
|
|
||||||
|
- `pnpm --filter @mosaic/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts`
|
||||||
|
- Red: failed on socket session reshaping and DTO role/length mismatches.
|
||||||
|
- Green: passed with 3 test files and 20 tests passing.
|
||||||
|
- `pnpm typecheck`
|
||||||
|
- Pass on 2026-03-13 with 18/18 package typecheck tasks successful.
|
||||||
|
- `pnpm lint`
|
||||||
|
- Pass on 2026-03-13 with 18/18 package lint tasks successful.
|
||||||
|
- `pnpm format:check`
|
||||||
|
- Pass on 2026-03-13 with `All matched files use Prettier code style!`
|
||||||
|
|
||||||
|
## Review Log
|
||||||
|
|
||||||
|
- Manual review completed against auth, authorization, validation, and runtime hardening requirements.
|
||||||
|
- No blocker findings remained after remediation.
|
||||||
|
|
||||||
|
## Risks / Blockers
|
||||||
|
|
||||||
|
- Repository instructions conflict on PR merge behavior; user explicitly instructed PR-only, no merge. Follow user instruction.
|
||||||
|
- Existing worktree contains prior-session modifications; do not revert unrelated changes.
|
||||||
|
- `missions` and `tasks` currently depend on project ownership because the schema does not carry a direct user owner column.
|
||||||
@@ -102,15 +102,3 @@ User confirmed: start the planning gate.
|
|||||||
| 9 | P5-001: Plugin host (channel plugin interface) | Plugin arch works |
|
| 9 | P5-001: Plugin host (channel plugin interface) | Plugin arch works |
|
||||||
| 10 | P5-002: Discord plugin (bot + channel) | Discord ↔ Gateway proven |
|
| 10 | P5-002: Discord plugin (bot + channel) | Discord ↔ Gateway proven |
|
||||||
| — | Then backfill: auth, brain, db, queue, OTEL, CI, web dashboard, etc. |
|
| — | Then backfill: auth, brain, db, queue, OTEL, CI, web dashboard, etc. |
|
||||||
|
|
||||||
### Session 9 — Phase 3 Web Dashboard (P3-001 through P3-007)
|
|
||||||
|
|
||||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
|
||||||
| ------- | ---------- | --------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| 9 | 2026-03-12 | Phase 3 | P3-001 through P3-007 | Full web dashboard: Next.js 16 scaffold, auth pages, chat UI, tasks (list+kanban), projects, settings, admin. PRs #82-#89 merged. |
|
|
||||||
|
|
||||||
### Session 10 — Phase 3 verification (P3-008)
|
|
||||||
|
|
||||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
|
||||||
| ------- | ---------- | --------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| 10 | 2026-03-13 | Phase 3 | P3-008 | Phase 3 verification: typecheck 18/18, lint 18/18, format clean, build green (10 routes), 10 tests pass. Phase 3 complete. |
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaic/design-tokens",
|
"name": "@mosaic/design-tokens",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mosaic Stack color palette.
|
|
||||||
* Deep blue-grays with blue/purple/teal accents.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const colors = {
|
|
||||||
/** Neutral blue-gray scale */
|
|
||||||
gray: {
|
|
||||||
50: '#f0f2f5',
|
|
||||||
100: '#dce0e8',
|
|
||||||
200: '#b8c0cc',
|
|
||||||
300: '#8e99a9',
|
|
||||||
400: '#6b7a8d',
|
|
||||||
500: '#4e5d70',
|
|
||||||
600: '#3b4859',
|
|
||||||
700: '#2a3544',
|
|
||||||
800: '#1c2433',
|
|
||||||
900: '#111827',
|
|
||||||
950: '#0a0f1a',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Primary — blue */
|
|
||||||
blue: {
|
|
||||||
50: '#eff4ff',
|
|
||||||
100: '#dae5ff',
|
|
||||||
200: '#bdd1ff',
|
|
||||||
300: '#8fb4ff',
|
|
||||||
400: '#5b8bff',
|
|
||||||
500: '#3b6cf7',
|
|
||||||
600: '#2551e0',
|
|
||||||
700: '#1d40c0',
|
|
||||||
800: '#1e369c',
|
|
||||||
900: '#1e317b',
|
|
||||||
950: '#162050',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Accent — purple */
|
|
||||||
purple: {
|
|
||||||
50: '#f3f0ff',
|
|
||||||
100: '#e7dfff',
|
|
||||||
200: '#d2c3ff',
|
|
||||||
300: '#b49aff',
|
|
||||||
400: '#9466ff',
|
|
||||||
500: '#7c3aed',
|
|
||||||
600: '#6d28d9',
|
|
||||||
700: '#5b21b6',
|
|
||||||
800: '#4c1d95',
|
|
||||||
900: '#3b1578',
|
|
||||||
950: '#230d4d',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Accent — teal */
|
|
||||||
teal: {
|
|
||||||
50: '#effcf9',
|
|
||||||
100: '#d0f7ef',
|
|
||||||
200: '#a4eddf',
|
|
||||||
300: '#6fddcb',
|
|
||||||
400: '#3ec5b2',
|
|
||||||
500: '#25aa99',
|
|
||||||
600: '#1c897e',
|
|
||||||
700: '#1b6e66',
|
|
||||||
800: '#1a5853',
|
|
||||||
900: '#194945',
|
|
||||||
950: '#082d2b',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Semantic */
|
|
||||||
success: '#22c55e',
|
|
||||||
warning: '#f59e0b',
|
|
||||||
error: '#ef4444',
|
|
||||||
info: '#3b82f6',
|
|
||||||
|
|
||||||
/** Surface — dark theme defaults */
|
|
||||||
surface: {
|
|
||||||
background: '#0a0f1a',
|
|
||||||
card: '#111827',
|
|
||||||
elevated: '#1c2433',
|
|
||||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
border: '#2a3544',
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Text */
|
|
||||||
text: {
|
|
||||||
primary: '#f0f2f5',
|
|
||||||
secondary: '#8e99a9',
|
|
||||||
muted: '#6b7a8d',
|
|
||||||
inverse: '#0a0f1a',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Colors = typeof colors;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mosaic Stack font definitions.
|
|
||||||
* Outfit (sans) — headings and body.
|
|
||||||
* Fira Code (mono) — code and terminal.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const fonts = {
|
|
||||||
sans: {
|
|
||||||
family: 'Outfit',
|
|
||||||
fallback: 'system-ui, -apple-system, sans-serif',
|
|
||||||
stack: "'Outfit', system-ui, -apple-system, sans-serif",
|
|
||||||
weights: {
|
|
||||||
light: 300,
|
|
||||||
regular: 400,
|
|
||||||
medium: 500,
|
|
||||||
semibold: 600,
|
|
||||||
bold: 700,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mono: {
|
|
||||||
family: 'Fira Code',
|
|
||||||
fallback: 'ui-monospace, Menlo, monospace',
|
|
||||||
stack: "'Fira Code', ui-monospace, Menlo, monospace",
|
|
||||||
weights: {
|
|
||||||
regular: 400,
|
|
||||||
medium: 500,
|
|
||||||
bold: 700,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Fonts = typeof fonts;
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
export const VERSION = '0.0.1';
|
export const VERSION = '0.0.0';
|
||||||
|
|
||||||
export { colors, type Colors } from './colors.js';
|
|
||||||
export { fonts, type Fonts } from './fonts.js';
|
|
||||||
export { spacing, radius, type Spacing, type Radius } from './spacing.js';
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mosaic Stack spacing scale (in rem).
|
|
||||||
* Based on a 4px grid (0.25rem increments).
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const spacing = {
|
|
||||||
px: '1px',
|
|
||||||
0: '0',
|
|
||||||
0.5: '0.125rem',
|
|
||||||
1: '0.25rem',
|
|
||||||
1.5: '0.375rem',
|
|
||||||
2: '0.5rem',
|
|
||||||
2.5: '0.625rem',
|
|
||||||
3: '0.75rem',
|
|
||||||
4: '1rem',
|
|
||||||
5: '1.25rem',
|
|
||||||
6: '1.5rem',
|
|
||||||
8: '2rem',
|
|
||||||
10: '2.5rem',
|
|
||||||
12: '3rem',
|
|
||||||
16: '4rem',
|
|
||||||
20: '5rem',
|
|
||||||
24: '6rem',
|
|
||||||
32: '8rem',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const radius = {
|
|
||||||
none: '0',
|
|
||||||
sm: '0.25rem',
|
|
||||||
md: '0.375rem',
|
|
||||||
lg: '0.5rem',
|
|
||||||
xl: '0.75rem',
|
|
||||||
'2xl': '1rem',
|
|
||||||
full: '9999px',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Spacing = typeof spacing;
|
|
||||||
export type Radius = typeof radius;
|
|
||||||
523
pnpm-lock.yaml
generated
523
pnpm-lock.yaml
generated
@@ -10,13 +10,13 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.39.4(jiti@2.6.1)
|
version: 9.39.4
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
@@ -34,13 +34,16 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
apps/gateway:
|
apps/gateway:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fastify/helmet':
|
||||||
|
specifier: ^13.0.2
|
||||||
|
version: 13.0.2
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ~0.57.1
|
specifier: ~0.57.1
|
||||||
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
|
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
|
||||||
@@ -74,6 +77,9 @@ importers:
|
|||||||
'@nestjs/platform-socket.io':
|
'@nestjs/platform-socket.io':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.16)(rxjs@7.8.2)
|
version: 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.16)(rxjs@7.8.2)
|
||||||
|
'@nestjs/throttler':
|
||||||
|
specifier: ^6.5.0
|
||||||
|
version: 6.5.0(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/websockets':
|
'@nestjs/websockets':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-socket.io@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-socket.io@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -103,7 +109,13 @@ importers:
|
|||||||
version: 0.34.48
|
version: 0.34.48
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: ^1.5.5
|
specifier: ^1.5.5
|
||||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15))
|
||||||
|
class-transformer:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
|
class-validator:
|
||||||
|
specifier: ^0.15.1
|
||||||
|
version: 0.15.1
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.8.2
|
version: 5.8.2
|
||||||
@@ -134,19 +146,10 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mosaic/design-tokens':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../../packages/design-tokens
|
|
||||||
better-auth:
|
|
||||||
specifier: ^1.5.5
|
|
||||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
|
||||||
clsx:
|
|
||||||
specifier: ^2.1.0
|
|
||||||
version: 2.1.1
|
|
||||||
next:
|
next:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -156,16 +159,7 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.4(react@19.2.4)
|
version: 19.2.4(react@19.2.4)
|
||||||
socket.io-client:
|
|
||||||
specifier: ^4.8.0
|
|
||||||
version: 4.8.3
|
|
||||||
tailwind-merge:
|
|
||||||
specifier: ^3.5.0
|
|
||||||
version: 3.5.0
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
|
||||||
specifier: ^4.0.0
|
|
||||||
version: 4.2.1
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
version: 22.19.15
|
version: 22.19.15
|
||||||
@@ -183,7 +177,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/agent:
|
packages/agent:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -196,7 +190,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/auth:
|
packages/auth:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -205,7 +199,7 @@ importers:
|
|||||||
version: link:../db
|
version: link:../db
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: ^1.5.5
|
specifier: ^1.5.5
|
||||||
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1))
|
version: 1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
@@ -218,7 +212,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/brain:
|
packages/brain:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -234,7 +228,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/cli:
|
packages/cli:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -268,7 +262,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/coord:
|
packages/coord:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -284,7 +278,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/db:
|
packages/db:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -309,7 +303,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/design-tokens:
|
packages/design-tokens:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -318,7 +312,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/log:
|
packages/log:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -327,7 +321,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/memory:
|
packages/memory:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -340,7 +334,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/mosaic:
|
packages/mosaic:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -349,7 +343,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/prdy:
|
packages/prdy:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -358,7 +352,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/quality-rails:
|
packages/quality-rails:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -367,7 +361,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/queue:
|
packages/queue:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -383,7 +377,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages/types:
|
packages/types:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -399,7 +393,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
plugins/discord:
|
plugins/discord:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -418,7 +412,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
plugins/telegram:
|
plugins/telegram:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -427,7 +421,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -435,10 +429,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
|
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
|
||||||
engines: {node: '>=14.13.1'}
|
engines: {node: '>=14.13.1'}
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
'@anthropic-ai/sdk@0.73.0':
|
'@anthropic-ai/sdk@0.73.0':
|
||||||
resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==}
|
resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1336,6 +1326,9 @@ packages:
|
|||||||
'@fastify/forwarded@3.0.1':
|
'@fastify/forwarded@3.0.1':
|
||||||
resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==}
|
resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==}
|
||||||
|
|
||||||
|
'@fastify/helmet@13.0.2':
|
||||||
|
resolution: {integrity: sha512-tO1QMkOfNeCt9l4sG/FiWErH4QMm+RjHzbMTrgew1DYOQ2vb/6M1G2iNABBrD7Xq6dUk+HLzWW8u+rmmhQHifA==}
|
||||||
|
|
||||||
'@fastify/merge-json-schemas@0.2.1':
|
'@fastify/merge-json-schemas@0.2.1':
|
||||||
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
|
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
|
||||||
|
|
||||||
@@ -1520,22 +1513,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
|
||||||
|
|
||||||
'@jridgewell/remapping@2.3.5':
|
|
||||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5':
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
|
||||||
|
|
||||||
'@js-sdsl/ordered-map@4.4.2':
|
'@js-sdsl/ordered-map@4.4.2':
|
||||||
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
||||||
|
|
||||||
@@ -1685,6 +1665,13 @@ packages:
|
|||||||
'@nestjs/websockets': ^11.0.0
|
'@nestjs/websockets': ^11.0.0
|
||||||
rxjs: ^7.1.0
|
rxjs: ^7.1.0
|
||||||
|
|
||||||
|
'@nestjs/throttler@6.5.0':
|
||||||
|
resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
reflect-metadata: ^0.1.13 || ^0.2.0
|
||||||
|
|
||||||
'@nestjs/websockets@11.1.16':
|
'@nestjs/websockets@11.1.16':
|
||||||
resolution: {integrity: sha512-kfLhCFsq6139JVFCQpbFB6LOEjZzdpE7JzXsZtRbVjqmsgTKVSIh8gKRgzpcq27rbLNqHhhZavboOltOfSxZow==}
|
resolution: {integrity: sha512-kfLhCFsq6139JVFCQpbFB6LOEjZzdpE7JzXsZtRbVjqmsgTKVSIh8gKRgzpcq27rbLNqHhhZavboOltOfSxZow==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2603,94 +2590,6 @@ packages:
|
|||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.2.1':
|
|
||||||
resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==}
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-android-arm64@4.2.1':
|
|
||||||
resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-arm64@4.2.1':
|
|
||||||
resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-x64@4.2.1':
|
|
||||||
resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-freebsd-x64@4.2.1':
|
|
||||||
resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
|
|
||||||
resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
|
|
||||||
resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
|
||||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
|
||||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
|
||||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
|
||||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
cpu: [wasm32]
|
|
||||||
bundledDependencies:
|
|
||||||
- '@napi-rs/wasm-runtime'
|
|
||||||
- '@emnapi/core'
|
|
||||||
- '@emnapi/runtime'
|
|
||||||
- '@tybys/wasm-util'
|
|
||||||
- '@emnapi/wasi-threads'
|
|
||||||
- tslib
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
|
|
||||||
resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-x64-msvc@4.2.1':
|
|
||||||
resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide@4.2.1':
|
|
||||||
resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
|
|
||||||
'@tailwindcss/postcss@4.2.1':
|
|
||||||
resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==}
|
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3152,10 +3051,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
cluster-key-slot@1.1.2:
|
cluster-key-slot@1.1.2:
|
||||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3387,10 +3282,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==}
|
resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==}
|
||||||
engines: {node: '>=10.2.0'}
|
engines: {node: '>=10.2.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.20.0:
|
|
||||||
resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
|
|
||||||
engines: {node: '>=10.13.0'}
|
|
||||||
|
|
||||||
environment@1.1.0:
|
environment@1.1.0:
|
||||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3681,6 +3572,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
helmet@8.1.0:
|
||||||
|
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
highlight.js@10.7.3:
|
highlight.js@10.7.3:
|
||||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||||
|
|
||||||
@@ -3814,10 +3709,6 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jiti@2.6.1:
|
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
jose@6.2.1:
|
jose@6.2.1:
|
||||||
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||||
|
|
||||||
@@ -3876,76 +3767,6 @@ packages:
|
|||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.31.1:
|
|
||||||
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.31.1:
|
|
||||||
resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
lightningcss-darwin-x64@1.31.1:
|
|
||||||
resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
lightningcss-freebsd-x64@1.31.1:
|
|
||||||
resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
lightningcss-linux-arm-gnueabihf@1.31.1:
|
|
||||||
resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-gnu@1.31.1:
|
|
||||||
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.31.1:
|
|
||||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.31.1:
|
|
||||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.31.1:
|
|
||||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.31.1:
|
|
||||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
lightningcss-win32-x64-msvc@1.31.1:
|
|
||||||
resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
lightningcss@1.31.1:
|
|
||||||
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
|
||||||
engines: {node: '>= 12.0.0'}
|
|
||||||
|
|
||||||
lilconfig@3.1.3:
|
lilconfig@3.1.3:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -4668,16 +4489,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
tailwind-merge@3.5.0:
|
|
||||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
|
||||||
|
|
||||||
tailwindcss@4.2.1:
|
tailwindcss@4.2.1:
|
||||||
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
||||||
|
|
||||||
tapable@2.3.0:
|
|
||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -5027,8 +4841,6 @@ snapshots:
|
|||||||
ansi-styles: 6.2.3
|
ansi-styles: 6.2.3
|
||||||
is-fullwidth-code-point: 4.0.0
|
is-fullwidth-code-point: 4.0.0
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
|
||||||
|
|
||||||
'@anthropic-ai/sdk@0.73.0(zod@4.3.6)':
|
'@anthropic-ai/sdk@0.73.0(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema-to-ts: 3.1.1
|
json-schema-to-ts: 3.1.1
|
||||||
@@ -5822,9 +5634,9 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.4':
|
'@esbuild/win32-x64@0.27.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
@@ -5892,6 +5704,11 @@ snapshots:
|
|||||||
|
|
||||||
'@fastify/forwarded@3.0.1': {}
|
'@fastify/forwarded@3.0.1': {}
|
||||||
|
|
||||||
|
'@fastify/helmet@13.0.2':
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: 5.1.0
|
||||||
|
helmet: 8.1.0
|
||||||
|
|
||||||
'@fastify/merge-json-schemas@0.2.1':
|
'@fastify/merge-json-schemas@0.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@@ -6043,25 +5860,8 @@ snapshots:
|
|||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
|
|
||||||
'@jridgewell/remapping@2.3.5':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
|
|
||||||
'@js-sdsl/ordered-map@4.4.2': {}
|
'@js-sdsl/ordered-map@4.4.2': {}
|
||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
@@ -6262,6 +6062,12 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@nestjs/throttler@6.5.0(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(reflect-metadata@0.2.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
|
||||||
'@nestjs/websockets@11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-socket.io@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
'@nestjs/websockets@11.1.16(@nestjs/common@11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-socket.io@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.16(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -7421,75 +7227,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@tailwindcss/node@4.2.1':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/remapping': 2.3.5
|
|
||||||
enhanced-resolve: 5.20.0
|
|
||||||
jiti: 2.6.1
|
|
||||||
lightningcss: 1.31.1
|
|
||||||
magic-string: 0.30.21
|
|
||||||
source-map-js: 1.2.1
|
|
||||||
tailwindcss: 4.2.1
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-android-arm64@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-arm64@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-x64@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-freebsd-x64@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-x64-msvc@4.2.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide@4.2.1':
|
|
||||||
optionalDependencies:
|
|
||||||
'@tailwindcss/oxide-android-arm64': 4.2.1
|
|
||||||
'@tailwindcss/oxide-darwin-arm64': 4.2.1
|
|
||||||
'@tailwindcss/oxide-darwin-x64': 4.2.1
|
|
||||||
'@tailwindcss/oxide-freebsd-x64': 4.2.1
|
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.1
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl': 4.2.1
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu': 4.2.1
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl': 4.2.1
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi': 4.2.1
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
|
||||||
|
|
||||||
'@tailwindcss/postcss@4.2.1':
|
|
||||||
dependencies:
|
|
||||||
'@alloc/quick-lru': 5.2.0
|
|
||||||
'@tailwindcss/node': 4.2.1
|
|
||||||
'@tailwindcss/oxide': 4.2.1
|
|
||||||
postcss: 8.5.8
|
|
||||||
tailwindcss: 4.2.1
|
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -7587,15 +7324,15 @@ snapshots:
|
|||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.57.0
|
'@typescript-eslint/scope-manager': 8.57.0
|
||||||
'@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.57.0
|
'@typescript-eslint/visitor-keys': 8.57.0
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
@@ -7603,14 +7340,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.57.0
|
'@typescript-eslint/scope-manager': 8.57.0
|
||||||
'@typescript-eslint/types': 8.57.0
|
'@typescript-eslint/types': 8.57.0
|
||||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.57.0
|
'@typescript-eslint/visitor-keys': 8.57.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -7633,13 +7370,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.57.0
|
'@typescript-eslint/types': 8.57.0
|
||||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7662,13 +7399,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
|
||||||
'@typescript-eslint/scope-manager': 8.57.0
|
'@typescript-eslint/scope-manager': 8.57.0
|
||||||
'@typescript-eslint/types': 8.57.0
|
'@typescript-eslint/types': 8.57.0
|
||||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -7685,13 +7422,13 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 1.2.0
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1))':
|
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
vite: 5.4.21(@types/node@22.19.15)
|
||||||
|
|
||||||
'@vitest/pretty-format@2.1.9':
|
'@vitest/pretty-format@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7802,7 +7539,7 @@ snapshots:
|
|||||||
|
|
||||||
basic-ftp@5.2.0: {}
|
basic-ftp@5.2.0: {}
|
||||||
|
|
||||||
better-auth@1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)):
|
better-auth@1.5.5(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))(mongodb@7.1.0(socks@2.8.7))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@2.1.9(@types/node@22.19.15)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
|
'@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)
|
||||||
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))
|
'@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(kysely@0.28.11)(postgres@3.4.8))
|
||||||
@@ -7828,7 +7565,7 @@ snapshots:
|
|||||||
next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
next: 16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
vitest: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
vitest: 2.1.9(@types/node@22.19.15)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@cloudflare/workers-types'
|
- '@cloudflare/workers-types'
|
||||||
|
|
||||||
@@ -7943,8 +7680,6 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
|
||||||
|
|
||||||
cluster-key-slot@1.1.2: {}
|
cluster-key-slot@1.1.2: {}
|
||||||
|
|
||||||
code-excerpt@4.0.0:
|
code-excerpt@4.0.0:
|
||||||
@@ -8008,7 +7743,8 @@ snapshots:
|
|||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
diff@8.0.3: {}
|
diff@8.0.3: {}
|
||||||
|
|
||||||
@@ -8096,11 +7832,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
enhanced-resolve@5.20.0:
|
|
||||||
dependencies:
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
tapable: 2.3.0
|
|
||||||
|
|
||||||
environment@1.1.0: {}
|
environment@1.1.0: {}
|
||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
@@ -8248,9 +7979,9 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@5.0.1: {}
|
eslint-visitor-keys@5.0.1: {}
|
||||||
|
|
||||||
eslint@9.39.4(jiti@2.6.1):
|
eslint@9.39.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@eslint/config-array': 0.21.2
|
'@eslint/config-array': 0.21.2
|
||||||
'@eslint/config-helpers': 0.4.2
|
'@eslint/config-helpers': 0.4.2
|
||||||
@@ -8284,8 +8015,6 @@ snapshots:
|
|||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
optionator: 0.9.4
|
optionator: 0.9.4
|
||||||
optionalDependencies:
|
|
||||||
jiti: 2.6.1
|
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -8556,6 +8285,8 @@ snapshots:
|
|||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
|
helmet@8.1.0: {}
|
||||||
|
|
||||||
highlight.js@10.7.3: {}
|
highlight.js@10.7.3: {}
|
||||||
|
|
||||||
hosted-git-info@9.0.2:
|
hosted-git-info@9.0.2:
|
||||||
@@ -8696,8 +8427,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
|
||||||
|
|
||||||
jose@6.2.1: {}
|
jose@6.2.1: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -8760,55 +8489,6 @@ snapshots:
|
|||||||
process-warning: 4.0.1
|
process-warning: 4.0.1
|
||||||
set-cookie-parser: 2.7.2
|
set-cookie-parser: 2.7.2
|
||||||
|
|
||||||
lightningcss-android-arm64@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-darwin-x64@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-freebsd-x64@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-linux-arm-gnueabihf@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-gnu@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss-win32-x64-msvc@1.31.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
lightningcss@1.31.1:
|
|
||||||
dependencies:
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
optionalDependencies:
|
|
||||||
lightningcss-android-arm64: 1.31.1
|
|
||||||
lightningcss-darwin-arm64: 1.31.1
|
|
||||||
lightningcss-darwin-x64: 1.31.1
|
|
||||||
lightningcss-freebsd-x64: 1.31.1
|
|
||||||
lightningcss-linux-arm-gnueabihf: 1.31.1
|
|
||||||
lightningcss-linux-arm64-gnu: 1.31.1
|
|
||||||
lightningcss-linux-arm64-musl: 1.31.1
|
|
||||||
lightningcss-linux-x64-gnu: 1.31.1
|
|
||||||
lightningcss-linux-x64-musl: 1.31.1
|
|
||||||
lightningcss-win32-arm64-msvc: 1.31.1
|
|
||||||
lightningcss-win32-x64-msvc: 1.31.1
|
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
lint-staged@15.5.2:
|
lint-staged@15.5.2:
|
||||||
@@ -9533,12 +9213,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
tailwind-merge@3.5.0: {}
|
|
||||||
|
|
||||||
tailwindcss@4.2.1: {}
|
tailwindcss@4.2.1: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@@ -9632,13 +9308,13 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@4.41.0: {}
|
type-fest@4.41.0: {}
|
||||||
|
|
||||||
typescript-eslint@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.57.0(eslint@9.39.4)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -9667,13 +9343,13 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite-node@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1):
|
vite-node@2.1.9(@types/node@22.19.15):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
vite: 5.4.21(@types/node@22.19.15)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- less
|
- less
|
||||||
@@ -9685,7 +9361,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1):
|
vite@5.4.21(@types/node@22.19.15):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
@@ -9693,12 +9369,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
lightningcss: 1.31.1
|
|
||||||
|
|
||||||
vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.31.1):
|
vitest@2.1.9(@types/node@22.19.15):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 2.1.9
|
'@vitest/expect': 2.1.9
|
||||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.31.1))
|
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15))
|
||||||
'@vitest/pretty-format': 2.1.9
|
'@vitest/pretty-format': 2.1.9
|
||||||
'@vitest/runner': 2.1.9
|
'@vitest/runner': 2.1.9
|
||||||
'@vitest/snapshot': 2.1.9
|
'@vitest/snapshot': 2.1.9
|
||||||
@@ -9714,8 +9389,8 @@ snapshots:
|
|||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 1.2.0
|
tinyrainbow: 1.2.0
|
||||||
vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.31.1)
|
vite: 5.4.21(@types/node@22.19.15)
|
||||||
vite-node: 2.1.9(@types/node@22.19.15)(lightningcss@1.31.1)
|
vite-node: 2.1.9(@types/node@22.19.15)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
|
|||||||
Reference in New Issue
Block a user