CreaRack-SL

Endpoints /api/correo/peticiones · Cola de Encargos de Correo (F2)

Descripción

Conjunto de endpoints que gestionan la cola de peticiones (encargos) que un miembro encola para que la secretaria de correo atienda. Son simples, CRUD sin lógica multiturno.

  • Navegador crea peticiones (POST, CF Access).
  • Navegador lista su cola (GET, CF Access).
  • Pasada desatendida de /correo las atiende (PUT, Bearer token).
  • Miembro descarta una suya (DELETE, CF Access o Bearer).

Autenticación

Dos canales de identidad escopo estricto:

CF Access (Navegador)

resolveTeamMember(request) — extrae la identidad CF Access. Usa X-Forwarded-For o headers CF típicos.

Bearer Token (Pasada Desatendida)

memberFromBearer(request, env) — extrae el token del header Authorization: Bearer <token> y lo mapea a un miembro:

const auth = request.headers.get('Authorization');
if (!auth?.startsWith('Bearer ')) return null;
const token = auth.slice(7);
for (const pair of (env.MCP_TOKENS || '').split(',').filter(Boolean)) {
  const [name, t] = pair.split(':');
  if (t && t === token) {
    const cap = name.trim().charAt(0).toUpperCase() + name.trim().slice(1).toLowerCase();
    return isTeamMember(cap) ? cap : null;
  }
}

Env var MCP_TOKENS = edu:token123,dani:token456,... (pares de nombre:token).

Resolución Unificada

function resolveMember(request: Request, env: Env): TeamMember | null {
  return resolveTeamMember(request) ?? memberFromBearer(request, env);
}

Intenta CF Access primero, luego Bearer. Si ninguno, 403 Forbidden.

GET /api/correo/peticiones

Lista las peticiones del miembro autenticado.

Query params (opcionales):

  • estado: filtro ('pendiente'|'atendida'|'descartada'). Sin este parámetro, devuelve todas.
  • limit: máx filas (default 20, capped a 50).

Ejemplo:

GET /api/correo/peticiones?estado=pendiente&limit=10

Respuesta (200):

[
  {
    "id": 1,
    "owner": "Edu",
    "texto": "Respóndele a Hetzner que confirmen la entrega",
    "estado": "pendiente",
    "respuesta": null,
    "compose_url": null,
    "created_at": "2026-08-12 14:30:00",
    "atendida_at": null
  },
  {
    "id": 2,
    "owner": "Edu",
    "texto": "Aviso a los clientes sobre la caída",
    "estado": "atendida",
    "respuesta": "Estimados, la infraestructura fue afectada...",
    "compose_url": "https://mail.zoho.com/...#compose&to=...",
    "created_at": "2026-08-12 13:15:00",
    "atendida_at": "2026-08-12 14:28:00"
  }
]

POST /api/correo/peticiones

Crea una petición nueva. Solo desde navegador (CF Access).

Body:

{
  "texto": "Respóndele a X que Z"  // ≤1000 chars, obligatorio
}

Validaciones:

  • texto requerido y no vacío.
  • texto.length ≤ 1000.

Respuesta (201 Created):

{
  "id": 3,
  "owner": "Edu",
  "texto": "Respóndele a X que Z",
  "estado": "pendiente",
  "respuesta": null,
  "compose_url": null,
  "created_at": "2026-08-12 14:35:00",
  "atendida_at": null
}

Errores:

  • 403 Forbidden: no autenticado.
  • 400 Bad Request: body JSON inválido o texto vacío.
  • 413 Payload Too Large: texto > 1000 chars.

PUT /api/correo/peticiones/:id

Atiende una petición: marca como atendida, añade respuesta y deep-link a Zoho Compose.

Body (todos opcionales, parcial):

{
  "estado": "pendiente|atendida|descartada",  // estado nuevo
  "respuesta": "el borrador que la secretaria generó",
  "compose_url": "https://mail.zoho.com/..."
}

Validaciones:

  • estado, si presente, debe ser uno de ['pendiente', 'atendida', 'descartada'].
  • respuesta, si presente, ≤4000 chars.
  • compose_url, si presente, debe comenzar con https://mail.zoho..

Lógica:

  • Solo el propietario de la petición (verificar existing.owner == member) puede actualizar.
  • Si estado == 'atendida', establece atendida_at = now().
  • Usa COALESCE para parciales: solo los campos presentes se actualizan, el resto quedan intactos.

Respuesta (200):

{
  "id": 2,
  "owner": "Edu",
  "texto": "Aviso a los clientes sobre la caída",
  "estado": "atendida",
  "respuesta": "Estimados, se produjo un corte...",
  "compose_url": "https://mail.zoho.com/...",
  "created_at": "2026-08-12 13:15:00",
  "atendida_at": "2026-08-12 14:28:00"
}

Errores:

  • 404 Not Found: petición no existe.
  • 403 Forbidden: petición es de otro miembro.
  • 400 Bad Request: campos inválidos (estado fuera de enum, respuesta muy larga, etc.).

DELETE /api/correo/peticiones/:id

Descarta una petición (elimina permanentemente).

Validación:

  • Solo el propietario puede eliminar.

Respuesta (204 No Content).

Errores:

  • 404 Not Found: petición no existe.
  • 403 Forbidden: petición es de otro miembro.

Flujo de Integración (F2)

Paso 1: Navegador Crea

const response = await fetch('/api/correo/peticiones', {
  method: 'POST',
  body: JSON.stringify({ texto: "Respóndele a Hetzner que confirmen..." })
});

→ Crea fila con estado='pendiente' en correo_peticiones.

Paso 2: Pasada Desatendida Atiende

La pasada de /correo (parte de Paso 7 de claude-method) corre periódicamente y:

  1. Autentica con Bearer {miembro_token} (del MCP_TOKENS).
  2. Llama GET /api/correo/peticiones?estado=pendiente&limit=5 → obtiene hasta 5 peticiones pendientes.
  3. Para cada petición, genera un borrador de correo (en claude-method, Paso 7).
  4. Produce un compose_url de Zoho Compose con el borrador pre-rellenado.
  5. Llama PUT /api/correo/peticiones/:id con {"estado":"atendida", "respuesta":"...", "compose_url":"..."}.

Paso 3: Navegador ve el Resultado

El miembro abre /correo, ve la petición ahora con estado='atendida' + botón compose_url para ir a Zoho y enviar manualmente.

Tabla Underlying (correo_peticiones)

Ver [[entity—correo—table—peticiones]] para schema e índices.

Diseño Aplazado

  • Envío directo: toggle F3 que permite que la pasada enviara el correo directamente en lugar de dejar solo el borrador.
  • Hoy: 100% manual — miembro recibe el borrador y hace clic en el deep-link a Zoho para revisar y enviar.

Véase también

  • [[feature—workspace—correo-menu-f0-f1-f2]]
  • [[entity—correo—table—peticiones]]
  • [[entity—correo—endpoint—ask]]
  • [[concept—saas—multi-tenancy]]
  • [[decision—20260403—multi-tenancy-rls]]