import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import type { Request } from "express"; import { loadEnv } from "../../config/env.js"; import { safeEqual } from "../../lib/crypto.js"; import { ApiError } from "../errors.js"; import { logger } from "../logger.js"; /** * Internal service-token guard (TASK-0092 slice 2 — the `/internal/*` * endpoints). * * The internal endpoints are the service-to-service surface the realtime * gateway uses to hydrate a room and to persist (flush) a CRDT doc against * Postgres. They are NOT user-facing: the room's permission model already * gated the edits, so the endpoints carry **no permission re-check** and * authenticate with a shared bearer token (`INTERNAL_SERVICE_TOKEN`) * instead of a user JWT / API key. * * Security model (recorded in the task file): * - The token is compared with `safeEqual` (constant-time) — never `===`. * - **Fail-closed**: when `INTERNAL_SERVICE_TOKEN` is empty (dev default), * every internal request is rejected (401) — the endpoints can never be * opened by accident. The gateway's HTTP store degrades to the in-memory * seam in that case (`REALTIME_DB=0` semantics). * - A request that carries a valid user JWT / API key is still rejected * (`token_not_service`): the internal surface accepts ONLY the service * token, so a user credential can never be replayed against it. * - Failures are logged (without the token) for the abuse trail. */ @Injectable() export class InternalServiceGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); const expected = loadEnv().INTERNAL_SERVICE_TOKEN; if (expected.length === 0) { // Fail closed: no configured token → the internal surface is closed. logger.warn(`internal endpoint rejected: INTERNAL_SERVICE_TOKEN not configured (fail-closed)`); throw ApiError.unauthorized("Internal service token is not configured.", [ { reason: "service_token_not_configured" } ]); } const header = req.headers.authorization; if (!header || !header.startsWith("Bearer ")) { throw ApiError.unauthorized("Missing service bearer token.", [{ reason: "service_token_missing" }]); } const token = header.slice(7).trim(); if (!token || !safeEqual(token, expected)) { // Do not echo the token; a reason code lets ops distinguish // missing vs mismatched without a timing oracle. throw ApiError.unauthorized("Invalid internal service token.", [ { reason: token ? "service_token_mismatch" : "service_token_missing" } ]); } // The internal surface authenticates ONLY as the shared service — set a // distinct principal so audit rows (and any downstream check) can tell a // system write apart from a user/API-key write. req.auth = { kind: "api", workspaceId: undefined, scopes: ["READ", "WRITE", "ADMIN"] }; return true; } }