Files
DeskcommCRM/proxy.ts
T
PessoaandClaude Opus 5 73857d4881 docs: a Vercel deixa de aparecer como onde o CRM roda e valida
O projeto do CRM na Vercel foi desvinculado do GitHub por decisao do dono: a
plataforma fica so com a landing page, em outro repositorio. Medido com a mesma
sonda nos dois lados — `gh api repos/.../commits/<sha>/status` devolve `Vercel`
no commit f65d04667 (antes) e vazio em a8c9e1ad3, 7168961ad e no head do #1081
(depois).

Com isso, dezenas de afirmacoes do repositorio publico viraram falsas. As que
falavam no presente sairam:

- mensagem automatica ao contribuinte, molde de PR, CONTRIBUTING e os espelhos
  na skill de contribuir prometiam um check "Vercel" vermelho que nao aparece
  mais. No lugar, a mensagem passa a dizer onde olhar o que trava o merge — os
  checks marcados Required no proprio PR, com a ressalva de que `--required` so
  lista os que ja reportaram;
- runbooks de operacao mandavam abrir o painel da plataforma e redeployar a
  main; passam a descrever o `.env` da instalacao e a recriacao dos conteineres;
- `lib/env.ts` mandava, no boot, ajustar variavel "na Vercel";
- specs e PRDs descreviam topologia, crons e guarda de segredo na plataforma;
- comentarios de codigo ancoravam decisoes vivas em premissa morta.

Registros datados (pesquisa, pitch, epicos, handoffs) nao foram reescritos:
ganharam uma linha dizendo que sao registro da fase hospedada.

O `vercel.ts` fica: ele serve quem hospeda um fork, e o gate
`tests/unit/cron-routes-scheduled.test.ts` continua reprovando divergencia de
rotas entre ele e o crontab do scheduler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:00:03 -03:00

130 lines
4.6 KiB
TypeScript

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookieSecure } from "@/lib/supabase/cookie-secure";
import { NextResponse, type NextRequest } from "next/server";
import { env } from "@/lib/env";
import { isPublicPath } from "@/lib/auth/public-paths";
import {
verifyImpersonateCookieEdge,
IMPERSONATE_COOKIE_NAME_EDGE,
} from "@/lib/impersonate/cookie-edge";
const COOKIE_NAME = "sb-deskcomm-auth";
export async function proxy(request: NextRequest) {
const response = NextResponse.next({ request: { headers: request.headers } });
// Inject X-Request-Id for downstream correlation (audit log, error wrappers).
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
response.headers.set("x-request-id", requestId);
const { pathname, search } = request.nextUrl;
// Expose pathname to Server Components via header (used by onboarding layout).
response.headers.set("x-pathname", pathname);
request.headers.set("x-pathname", pathname);
// EPIC-11: the admin surface is reached by PATH (`/admin/*`) — the self-host kit
// points `NEXT_PUBLIC_ADMIN_URL` at the same host as the app and maps no `admin.`
// sub-domain. The host-based branch below stays a NOOP today and only exists as
// documentation of the intended deploy topology.
const host = request.headers.get("host") ?? "";
const isAdminSurface = host.startsWith("admin.") || pathname.startsWith("/admin");
if (isPublicPath(pathname)) {
return response;
}
const supabase = createServerClient(
env.NEXT_PUBLIC_SUPABASE_URL,
env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value);
response.cookies.set(name, value, options);
});
},
},
cookieOptions: {
name: COOKIE_NAME,
sameSite: "strict",
httpOnly: true,
secure: cookieSecure(),
path: "/",
},
},
);
// Validate JWT server-side (NEVER use getSession on backend per CLAUDE.md).
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
// API routes must respond with JSON envelope (contract: {error:{code,message}})
// — never redirect HTML to JSON consumers. UI routes redirect to /login as before.
if (pathname.startsWith("/api/")) {
return new NextResponse(
JSON.stringify({
error: {
code: "unauthenticated",
message: "Authentication required",
},
}),
{
status: 401,
headers: {
"content-type": "application/json",
"x-request-id": requestId,
},
},
);
}
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("next", pathname + search);
return NextResponse.redirect(loginUrl);
}
// EPIC-11 S-11.07: validate impersonate cookie on /app/* paths. Middleware
// runs in Edge — no DB access, only HMAC + expiry. On any failure we delete
// the presentation cookie. The database support session remains authoritative:
// expired/revoked support still blocks the app until explicit exit.
if (pathname.startsWith("/app")) {
const impCookie = request.cookies.get(IMPERSONATE_COOKIE_NAME_EDGE)?.value;
if (impCookie) {
const result = await verifyImpersonateCookieEdge(
impCookie,
env.IMPERSONATE_COOKIE_SECRET ?? "",
);
if (!result.valid) {
console.warn(
`[middleware] impersonate cookie invalid (${result.reason ?? "unknown"}) — clearing`,
);
response.cookies.delete(IMPERSONATE_COOKIE_NAME_EDGE);
}
}
}
// /admin/* additionally requires platform_admin (early gate — authoritative
// check is server-side in `requirePlatformAdmin`). Skip the RPC for
// `/admin/forbidden` (rendered to non-admins, would otherwise loop).
if (isAdminSurface && pathname.startsWith("/admin") && pathname !== "/admin/forbidden") {
const { data: isAdmin, error } = await supabase.rpc("fn_is_platform_admin");
if (error || !isAdmin) {
return NextResponse.redirect(new URL("/admin/forbidden", request.url));
}
}
return response;
}
export const config = {
matcher: [
// Run on all paths except static assets / Next internals.
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*)",
],
};