import { Body, Controller, Get, HttpException, HttpStatus, Put, UseGuards } from '@nestjs/common'; import { AuthGuard } from '../auth/auth.guard.js'; import { CurrentUser } from '../auth/current-user.decorator.js'; import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; import { HarnessOperationError } from './harness.registry.js'; import { HarnessSelectionService } from './harness-selection.service.js'; import { HarnessSelectionInputDto, type SelectionResponseDto } from './harness.dto.js'; /** * Chat-preferences selection surface. The scope is ALWAYS derived on the server * from the authenticated user (`scopeFromUser(CurrentUser)`); the request body and * query string can never name another user, tenant, or seat. A typed selection * failure (unknown tuple → `selection_invalid`, known-but-unavailable → * `model_unavailable`) is returned as 422 with the requested tuple echoed back * unchanged, and never mutates the stored selection. */ @Controller('api/chat/preferences/selection') @UseGuards(AuthGuard) export class HarnessSelectionController { constructor(private readonly selection: HarnessSelectionService) {} @Get() get(@CurrentUser() user: AuthenticatedUserLike): SelectionResponseDto { return { selection: this.selection.getSelection(scopeFromUser(user)) }; } @Put() async put( @CurrentUser() user: AuthenticatedUserLike, @Body() dto: HarnessSelectionInputDto, ): Promise { try { const stored = await this.selection.setSelection(scopeFromUser(user), { harnessId: dto.harnessId, providerId: dto.providerId, modelId: dto.modelId, }); return { selection: stored }; } catch (error) { if (error instanceof HarnessOperationError) { throw new HttpException(error.dto, HttpStatus.UNPROCESSABLE_ENTITY); } throw error; } } }