Plataforma técnica · Sevastopol
BFF Proxy a Orchestrator
Sevastopol Bff Orchestrator
Propósito y Alcance
Section titled “Propósito y Alcance”Sevastopol nunca habla directo con la base de datos. Toda lectura y escritura del navegador pasa por src/pages/api/, un conjunto de ~50 rutas Astro que reenvían cada request al Orchestrator. Este patrón es un Backend-for-Frontend: el frontend tiene su propia capa de servidor que adapta el contrato del backend al navegador.
Tres beneficios:
- CORS resuelto — el navegador siempre habla con el mismo origen que sirvió el HTML.
- Sesión opaca — la cookie
sidviaja entre navegador y proxy; el navegador nunca recibe tokens reutilizables. - Adaptación uniforme — un único helper (
createProxy()) cubre el 80% de las rutas; las que necesitan lógica custom la encapsulan localmente.
| Pieza | Archivo | Rol |
|---|---|---|
createProxy() | src/lib/proxyUtils.ts | Fábrica que devuelve handlers GET/POST/PUT/DELETE/PATCH |
ORCHESTRATOR_BASE_URL | src/lib/orchestratorUrl.ts | URL base del backend (env var + fallback localhost:8000) |
authenticatedFetch() | src/lib/authFetch.ts | Wrapper del lado del navegador (cookies automáticas, redirect 401) |
Rutas src/pages/api/** | src/pages/api/*.ts | Endpoints proxy (49 usan createProxy, ~15 son custom) |
Flujo de una llamada de datos
Section titled “Flujo de una llamada de datos”sequenceDiagram autonumber participant ISL as Island (browser) participant FETCH as authenticatedFetch participant MW as Middleware participant PRX as createProxy handler participant ORC as Orchestrator ISL->>FETCH: GET /api/employees?tenant_id=X FETCH->>MW: fetch con credentials: include MW->>MW: ¿/api/* y sin sid? → 401 MW->>PRX: next() (cookie sid presente) PRX->>PRX: getProxyHeaders(request)<br/>(cookie, content-type, authorization) PRX->>ORC: fetch ORCHESTRATOR_BASE_URL/api/employees?tenant_id=X ORC-->>PRX: 200 JSON + Set-Cookie? PRX->>PRX: arrayBuffer() · copia content-type<br/>content-disposition · set-cookie PRX-->>MW: Response MW->>MW: withSecurityHeaders() (CSP) MW-->>ISL: Response final
createProxy(): la fábrica
Section titled “createProxy(): la fábrica”El helper genera un objeto con los cinco verbos HTTP apuntando al mismo handler interno:
export function createProxy(apiPath: string): { GET?: APIRoute; POST?: APIRoute; PUT?: APIRoute; DELETE?: APIRoute; PATCH?: APIRoute;};Pasos del handler
Section titled “Pasos del handler”- Construir URL destino —
ORCHESTRATOR_BASE_URL + apiPathmás query string original. - Resolver
[path]dinámico — si la ruta es catch-all ([...path]), concatena el segmento dinámico. - Leer body — sólo para verbos != GET/HEAD, como texto crudo (preserva forma original).
- Reenviar headers seleccionados — vía
getProxyHeaders(): cookie, content-type, authorization. No reenvía otros headers del cliente. - Responder con status y body del backend, traspasando
content-type,content-dispositionyset-cookie. - Manejar 204 — body explícitamente
null(requisito del estándar HTTP). - Manejar fallo de red —
try/catchglobal devuelve503 Service unavailable.
Headers reenviados
Section titled “Headers reenviados”function getProxyHeaders(request: Request) { const headers: Record<string, string> = {}; const cookie = request.headers.get('cookie'); if (cookie) headers.cookie = cookie; const contentType = request.headers.get('content-type'); if (contentType) headers['content-type'] = contentType; const authorization = request.headers.get('authorization'); if (authorization) headers.authorization = authorization; return headers;}Esta lista es deliberadamente corta. Pasar headers del navegador sin filtrar es una vía de smuggling — el proxy se queda con lo mínimo que Orchestrator necesita.
Soporte de binarios
Section titled “Soporte de binarios”El body de la respuesta se lee como ArrayBuffer, no como JSON:
const buffer = await orchestratorRes.arrayBuffer();// ...return new Response(buffer, { status, headers });Esto permite que el mismo proxy sirva JSON, PDFs generados, Excel exportado y cualquier blob sin lógica adicional.
Set-Cookie passthrough
Section titled “Set-Cookie passthrough”Cuando Orchestrator emite cookies (login, refresh de sesión), el proxy las propaga al navegador:
const setCookie = orchestratorRes.headers.get('set-cookie');if (setCookie) headers['Set-Cookie'] = setCookie;Sin esto, /api/auth/login no podría establecer sid en el navegador.
Tres patrones de ruta
Section titled “Tres patrones de ruta”Patrón 1: Proxy simple (preferido)
Section titled “Patrón 1: Proxy simple (preferido)”49 de las 64 rutas siguen este patrón. La ruta de Astro es una línea:
import { createProxy } from '@/lib/proxyUtils';export const { GET, POST, PUT, DELETE } = createProxy('/api/employees');Cuándo usar: cuando el contrato del frontend con Orchestrator es 1-a-1 y no necesita transformación.
Patrón 2: Proxy + override de un verbo
Section titled “Patrón 2: Proxy + override de un verbo”Reutiliza el handler genérico para verbos comunes y reemplaza el problemático:
export const GET: APIRoute = async () => { return new Response(JSON.stringify({ success: false, error: 'Use POST method for login', }), { status: 405, headers: { 'Content-Type': 'application/json' } });};
export const { POST } = createProxy('/api/auth/login');Cuándo usar: cuando un verbo debe rechazarse explícitamente con mensaje custom (mejor UX que el 404 por defecto de Astro).
Patrón 3: Handler totalmente custom
Section titled “Patrón 3: Handler totalmente custom”Rutas con transformación de payload, lógica condicional o multi-step. Replican getProxyHeaders() y la mecánica de fetch a mano:
export const POST: APIRoute = async ({ request }) => { const bodyText = await request.text(); const payload = JSON.parse(bodyText);
// Filtrar el payload: Orchestrator sólo espera { html } const orchestratorPayload = { html: payload.html };
const targetUrl = `${ORCHESTRATOR_BASE_URL}/api/generate-pdf${url.search}`; const orchestratorRes = await fetch(targetUrl, { method: 'POST', headers: getProxyHeaders(request), body: JSON.stringify(orchestratorPayload), credentials: 'include', }); // ...};Cuándo usar: cuando el contrato browser→proxy difiere del proxy→Orchestrator (filtrar campos, fusionar fuentes, validar antes de reenviar).
authenticatedFetch(): el lado del navegador
Section titled “authenticatedFetch(): el lado del navegador”Las islands y hooks nunca llaman fetch() directamente. Usan el wrapper:
export const authenticatedFetch = async (url: string, options: RequestInit = {}) => { const headers = { 'Content-Type': 'application/json', ...options.headers, }; return fetch(url, { ...options, headers, credentials: 'include', // envía cookies automáticamente });};Tres responsabilidades implícitas:
credentials: 'include'— la cookiesidviaja con cada request.Content-Type: application/jsonpor defecto — sobreescribible víaoptions.headers.- Punto único de modificación — si mañana se necesita un header global (correlation ID, version), se agrega aquí.
Manejo centralizado de 401
Section titled “Manejo centralizado de 401”handleAuthResponse se aplica en endpoints donde la sesión expirada debe redirigir:
export const handleAuthResponse = async (response: Response) => { if (response.status === 401) { window.dispatchEvent(new CustomEvent('toast:push', { detail: { msg: '🔐 Sesión expirada - Redirigiendo al login', kind: 'error' }, })); setTimeout(() => { window.location.href = '/'; }, 2000); throw new Error('Session expired'); } return response;};El delay de 2s permite que el toast sea visible antes de la navegación.
ORCHESTRATOR_BASE_URL: configuración
Section titled “ORCHESTRATOR_BASE_URL: configuración”Una sola fuente de verdad para apuntar al backend:
const DEFAULT_ORCHESTRATOR_BASE_URL = "http://localhost:8000";
const configuredUrl = import.meta.env.ORCHESTRATOR_BASE_URL || process.env.ORCHESTRATOR_BASE_URL || DEFAULT_ORCHESTRATOR_BASE_URL;
export const ORCHESTRATOR_BASE_URL = configuredUrl.replace(/\/+$/, "");Orden de resolución:
import.meta.env.ORCHESTRATOR_BASE_URL— inyectada en build por Astro.process.env.ORCHESTRATOR_BASE_URL— variable del runtime Node SSR.http://localhost:8000— fallback para desarrollo.
El replace(/\/+$/, "") normaliza removiendo barras finales — todas las rutas se concatenan asumiendo que la base no termina en /.
Mapa de rutas del proxy
Section titled “Mapa de rutas del proxy”Las 64 rutas se agrupan por dominio:
| Dominio | Carpeta o archivos |
|---|---|
| Autenticación | auth/login.ts, auth/logout.ts, auth/validate.ts, auth/2fa/* |
| Multi-tenant | tenant.ts, tenant-db/*, sessions.ts, permissions.ts |
| Remuneraciones | payroll/*, employees.ts, contracts/*, cargos.ts, attendance.ts, vacations.ts |
| Previsionales | afp.ts, afc.ts, isapre.ts, apv_contracts.ts, isapre_contracts.ts, previsiones.ts |
| Contabilidad | accounting/*, gastos/*, inventario/*, activo-fijo/*, ciclo-contable/* |
| Financieros | financieros/* (bancos, cartolas, conciliación, préstamos) |
| Declaraciones SII | declaraciones/*, declaraciones-juradas/*, honorarios/*, impuesto_2cat.ts |
| Reportes | reportes/*, generate-pdf.ts |
| Admin | admin/* (capital, chart-of-accounts, company, system-config, representatives) |
| ETL & Files | etl/*, files/*, manual-cuentas/* |
| Comandos | command/*, agent/*, parameters/* |
| Operaciones | operaciones.ts, monitoring.ts, working_day.ts |
| Catálogos | menu.ts, departments.ts |
Para el catálogo exhaustivo, leer directamente sevastopol/src/pages/api/. Cada archivo es de una línea cuando usa createProxy.
Cómo agregar una ruta nueva
Section titled “Cómo agregar una ruta nueva”Caso simple (proxy 1-a-1)
Section titled “Caso simple (proxy 1-a-1)”-
Confirmar que Orchestrator ya expone el endpoint (por ejemplo
/api/nuevo-recurso). -
Crear
src/pages/api/nuevo-recurso.ts:import { createProxy } from '@/lib/proxyUtils';export const { GET, POST, PUT, DELETE } = createProxy('/api/nuevo-recurso'); -
Llamar desde una island con
authenticatedFetch('/api/nuevo-recurso').
No hay paso 4. El middleware ya protege /api/* con verificación de cookie.
Caso con parámetro dinámico
Section titled “Caso con parámetro dinámico”Para /api/recurso/:id crear src/pages/api/recurso/[id].ts. El proxy interpreta los params y los reescribe en la URL destino — ver payroll/[id].ts como referencia.
Caso con transformación
Section titled “Caso con transformación”Si el payload del navegador no coincide con el que espera Orchestrator (filtrar campos, fusionar fuentes), copiar la estructura de generate-pdf.ts y adaptar el body antes del fetch.
Errores y degradación
Section titled “Errores y degradación”| Situación | Respuesta del proxy |
|---|---|
Cookie sid ausente | Middleware responde 401 antes de llegar al handler |
| Orchestrator responde error HTTP (4xx/5xx) | El proxy reenvía el status y body tal cual |
Orchestrator inalcanzable (fetch lanza) | 503 Service unavailable con {success: false, error} JSON |
| Body de respuesta 204 No Content | Response(null, { status: 204 }) — body debe ser null |
Una falla 503 del proxy debe interpretarse en el navegador como backend caído, no como sesión expirada. Las islands diferencian con el código HTTP para decidir si mostrar toast de reconexión o redirección al login.