/** * TASK-0133 — the MEMBER-facing effective-policy endpoint (spec: "a * lightweight `GET /api/v1/workspaces/:id/policy/effective` for members — * used by the [file] picker, never authoritative"). * * It returns the org policy + the EFFECTIVE caps (org policy ∩ plan limits — * the same computation the upload path enforces, `effectiveMaxFileBytes`). * The file picker reads this to warn BEFORE the user picks a file; the * presign endpoint remains the authoritative enforcement (413 — a client * that ignores the warning still cannot exceed the cap). Requires VIEW on * the workspace (any member) — it reveals policy VALUES (not secrets), which * is fine for members of the org. */ import { Controller, Get, Inject, Param, Req } from "@nestjs/common"; import type { Request } from "express"; import { PrismaService } from "../../common/prisma.service.js"; import { requireUser } from "../../common/types.js"; import { assertCan } from "../../lib/authorize.js"; import { effectiveMaxFileBytes, planMaxFileBytes, workspaceOrgPolicy } from "./org-policy.service.js"; @Controller("workspaces/:workspaceId/policy") export class PolicyEffectiveController { // Explicit @Inject (see OrgAdminController — bare params break under // vitest/esbuild's missing design:paramtypes). constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} @Get("effective") async effective(@Param("workspaceId") workspaceId: string, @Req() req: Request) { const userId = requireUser(req); await assertCan(this.prisma, { workspaceId, userId }, "VIEW"); const ws = await this.prisma.workspace.findUnique({ where: { id: workspaceId }, select: { organizationId: true, billingPlan: true } }); if (!ws) throw new (await import("../../common/errors.js")).ApiError("NOT_FOUND", 404, "Workspace not found."); const { orgId, policy } = await workspaceOrgPolicy(this.prisma, workspaceId); const maxBytes = effectiveMaxFileBytes(policy, ws.billingPlan); return { orgId, policy, effective: { // The org's effective upload cap (org ∩ plan) — the picker warns at // this size; the presign endpoint enforces it authoritatively (413). maxFileSizeBytes: maxBytes, maxFileSizeMB: Math.floor(maxBytes / 1024 / 1024), planMaxFileSizeBytes: planMaxFileBytes(ws.billingPlan), sessionLifetimeHours: policy?.sessionLifetimeHours ?? null, allowExternalEmbeds: policy?.allowExternalEmbeds ?? true } }; } }