1import { Body, Controller, HttpCode, Inject, Param, Post, UseGuards } from "@nestjs/common"; 2import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; 3import { InternalServiceGuard } from "../../common/guards/internal-service.guard.js"; 4import { TestSeamService } from "./test-seam.service.js"; 5import { InternalAddMemberDto, type InternalAddMemberInput } from "./test-seam.dto.js"; 6 7/** 8 * Test-only membership seam controller (TASK-0095 S9). 9 * 10 * `POST /internal/workspaces/:workspaceId/members` — add a registered user 11 * to a workspace directly (no invite mail), unblocking the two-user flows 12 * that the email-gated invite flow (D3) otherwise makes untestable. 13 * 14 * Gating (two independent switches, both fail-closed): 15 * - `InternalServiceGuard` — the shared `INTERNAL_SERVICE_TOKEN` 16 * (constant-time compare; user JWTs rejected; closed when the token is 17 * empty). Same guard as `InternalBlocksController`. 18 * - `INTERNAL_TEST_SEAM=1` — the controller AND the service both reject 19 * when the flag is not enabled, so a mis-wired controller cannot open the 20 * seam in production. 21 * 22 * EXCLUDED from the global `api/v1` prefix (like `/internal/blocks/*` — 23 * see main.ts): the canonical path is `/internal/workspaces/:id/members`. 24 */ 25@Controller("internal/workspaces") 26@UseGuards(InternalServiceGuard) 27export class InternalTestSeamController { 28 constructor(@Inject(TestSeamService) private readonly seam: TestSeamService) {} 29 30 @Post(":workspaceId/members") 31 @HttpCode(201) 32 addMember( 33 @Param("workspaceId") workspaceId: string, 34 @Body(ZodValidate(InternalAddMemberDto)) body: InternalAddMemberInput 35 ) { 36 return this.seam.addMember(workspaceId, body); 37 } 38} 39