fix(gateway): CORS, memory userId from session, pgvector auto-init (#110)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #110.
This commit is contained in:
2026-03-15 16:40:28 +00:00
committed by jason.woltje
parent 6d2b81f6e4
commit 72a73c859c
5 changed files with 25 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ BETTER_AUTH_URL=http://localhost:4000
# Gateway
GATEWAY_PORT=4000
GATEWAY_CORS_ORIGIN=http://localhost:3000
# Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable)
# DISCORD_BOT_TOKEN=

View File

@@ -35,6 +35,11 @@ async function bootstrap(): Promise<void> {
new FastifyAdapter({ bodyLimit: 1_048_576 }),
);
app.enableCors({
origin: process.env['GATEWAY_CORS_ORIGIN'] ?? 'http://localhost:3000',
credentials: true,
});
await app.register(helmet as never, { contentSecurityPolicy: false });
app.useGlobalPipes(
new ValidationPipe({

View File

@@ -15,6 +15,7 @@ import {
import type { Memory } from '@mosaic/memory';
import { MEMORY } from './memory.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import { EmbeddingService } from './embedding.service.js';
import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './memory.dto.js';
@@ -23,33 +24,33 @@ import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './m
export class MemoryController {
constructor(
@Inject(MEMORY) private readonly memory: Memory,
private readonly embeddings: EmbeddingService,
@Inject(EmbeddingService) private readonly embeddings: EmbeddingService,
) {}
// ─── Preferences ────────────────────────────────────────────────────
@Get('preferences')
async listPreferences(@Query('userId') userId: string, @Query('category') category?: string) {
async listPreferences(@CurrentUser() user: { id: string }, @Query('category') category?: string) {
if (category) {
return this.memory.preferences.findByUserAndCategory(
userId,
user.id,
category as Parameters<typeof this.memory.preferences.findByUserAndCategory>[1],
);
}
return this.memory.preferences.findByUser(userId);
return this.memory.preferences.findByUser(user.id);
}
@Get('preferences/:key')
async getPreference(@Query('userId') userId: string, @Param('key') key: string) {
const pref = await this.memory.preferences.findByUserAndKey(userId, key);
async getPreference(@CurrentUser() user: { id: string }, @Param('key') key: string) {
const pref = await this.memory.preferences.findByUserAndKey(user.id, key);
if (!pref) throw new NotFoundException('Preference not found');
return pref;
}
@Post('preferences')
async upsertPreference(@Query('userId') userId: string, @Body() dto: UpsertPreferenceDto) {
async upsertPreference(@CurrentUser() user: { id: string }, @Body() dto: UpsertPreferenceDto) {
return this.memory.preferences.upsert({
userId,
userId: user.id,
key: dto.key,
value: dto.value,
category: dto.category,
@@ -59,16 +60,16 @@ export class MemoryController {
@Delete('preferences/:key')
@HttpCode(HttpStatus.NO_CONTENT)
async removePreference(@Query('userId') userId: string, @Param('key') key: string) {
const deleted = await this.memory.preferences.remove(userId, key);
async removePreference(@CurrentUser() user: { id: string }, @Param('key') key: string) {
const deleted = await this.memory.preferences.remove(user.id, key);
if (!deleted) throw new NotFoundException('Preference not found');
}
// ─── Insights ───────────────────────────────────────────────────────
@Get('insights')
async listInsights(@Query('userId') userId: string, @Query('limit') limit?: string) {
return this.memory.insights.findByUser(userId, limit ? Number(limit) : undefined);
async listInsights(@CurrentUser() user: { id: string }, @Query('limit') limit?: string) {
return this.memory.insights.findByUser(user.id, limit ? Number(limit) : undefined);
}
@Get('insights/:id')
@@ -79,13 +80,13 @@ export class MemoryController {
}
@Post('insights')
async createInsight(@Query('userId') userId: string, @Body() dto: CreateInsightDto) {
async createInsight(@CurrentUser() user: { id: string }, @Body() dto: CreateInsightDto) {
const embedding = this.embeddings.available
? await this.embeddings.embed(dto.content)
: undefined;
return this.memory.insights.create({
userId,
userId: user.id,
content: dto.content,
source: dto.source,
category: dto.category,
@@ -104,7 +105,7 @@ export class MemoryController {
// ─── Search ─────────────────────────────────────────────────────────
@Post('search')
async searchMemory(@Query('userId') userId: string, @Body() dto: SearchMemoryDto) {
async searchMemory(@CurrentUser() user: { id: string }, @Body() dto: SearchMemoryDto) {
if (!this.embeddings.available) {
return {
query: dto.query,
@@ -115,7 +116,7 @@ export class MemoryController {
const queryEmbedding = await this.embeddings.embed(dto.query);
const results = await this.memory.insights.searchByEmbedding(
userId,
user.id,
queryEmbedding,
dto.limit ?? 10,
dto.maxDistance ?? 0.8,

View File

@@ -9,6 +9,7 @@ services:
POSTGRES_DB: mosaic
volumes:
- pg_data:/var/lib/postgresql/data
- ./infra/pg-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U mosaic']
interval: 5s

View File

@@ -0,0 +1 @@
CREATE EXTENSION IF NOT EXISTS vector;