import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { timingSafeEqual } from 'crypto'; /** * Guard for the AI-dev stream + writeback endpoints. * * Authenticates with a single static bearer token (AI_STREAM_TOKEN), accepted * either as `Authorization: Bearer ` (POST / header-capable clients) or * as a `?token=` query param (browser EventSource cannot set headers). * * This is a separate auth realm from the admin/member JWT — the AI dev never * gets a login, just one long-lived token you rotate by changing the env var. */ @Injectable() export class AiStreamGuard implements CanActivate { constructor(private readonly config: ConfigService) {} canActivate(context: ExecutionContext): boolean { const expected = this.config.get('AI_STREAM_TOKEN'); if (!expected) { throw new UnauthorizedException('AI stream is not configured'); } const req = context.switchToHttp().getRequest(); const header: string | undefined = req.headers?.authorization; const fromHeader = header?.startsWith('Bearer ') ? header.slice(7) : undefined; const fromQuery: string | undefined = req.query?.token; const provided = fromHeader ?? fromQuery; if (!provided || !safeEqual(provided, expected)) { throw new UnauthorizedException('Invalid AI stream token'); } return true; } } function safeEqual(a: string, b: string): boolean { const ab = Buffer.from(a); const bb = Buffer.from(b); if (ab.length !== bb.length) return false; return timingSafeEqual(ab, bb); }