DesarrolladorGuía de Conexión WebSocket

Guía de Conexión WebSocket

Esta guía explica cómo establecer conexiones WebSocket autenticadas a la API GraphQL de Zeabur para suscripciones en tiempo real (registros, actividad del proyecto, etc.).

Descripción General

Zeabur utiliza el protocolo graphql-ws para suscripciones GraphQL a través de WebSocket. La conexión requiere autenticación mediante un token pasado en el payload connection_init.

Puntos de Conexión

EntornoURL WebSocket
Globalwss://api.zeabur.com/graphql
Chinawss://api.zeabur.cn/graphql

Autenticación

Las conexiones WebSocket se autentican pasando un authToken en los connectionParams durante la fase de inicialización de la conexión.

Fuente del Token

El token de autenticación se puede obtener de dos maneras:

  1. Desde Cookie - Al usar el Panel de Zeabur, el token se almacena en una cookie llamada token:
// Extraer token de la cookie
const token = document.cookie
  .split('; ')
  .find(row => row.startsWith('token='))
  ?.split('=')[1];
  1. Usando Clave API - Para acceso programático, puedes usar tu clave API como token de autenticación.

Flujo de Conexión

Implementación

Usando Apollo Client (Recomendado)

Este es el enfoque recomendado para aplicaciones React:

import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
 
const wsLink = new GraphQLWsLink(createClient({ 
  url: 'wss://api.zeabur.com/graphql',
  connectionParams: () => {
    // Obtener token de autenticación de la cookie
    const token = document.cookie
      .split('; ')
      .find(row => row.startsWith('token='))
      ?.split('=')[1];
    
    return {
      authToken: token,
    };
  },
}));

Usando el Cliente graphql-ws Directamente

Para implementaciones sin Apollo:

import { createClient } from 'graphql-ws';
 
const client = createClient({
  url: 'wss://api.zeabur.com/graphql',
  connectionParams: {
    authToken: 'YOUR_API_TOKEN',
  },
});
 
// Suscribirse a registros de tiempo de ejecución
const unsubscribe = client.subscribe(
  {
    query: `
      subscription SubscribeRuntimeLog(
        $projectID: ObjectID!
        $serviceID: ObjectID!
        $environmentID: ObjectID!
      ) {
        runtimeLogReceived(
          projectID: $projectID
          serviceID: $serviceID
          environmentID: $environmentID
        ) {
          timestamp
          message
        }
      }
    `,
    variables: {
      projectID: 'your-project-id',
      serviceID: 'your-service-id',
      environmentID: 'your-environment-id',
    },
  },
  {
    next: (data) => console.log('Recibido:', data),
    error: (err) => console.error('Error:', err),
    complete: () => console.log('Completado'),
  }
);

Usando la API WebSocket Nativa

Para pruebas o implementaciones de bajo nivel:

const API_URL = 'wss://api.zeabur.com/graphql';
 
// 1. Crear conexión WebSocket con subprotocolo graphql-ws
const ws = new WebSocket(API_URL, 'graphql-transport-ws');
 
ws.onopen = () => {
  // 2. Enviar connection_init con token de autenticación
  ws.send(JSON.stringify({
    type: 'connection_init',
    payload: {
      authToken: 'YOUR_AUTH_TOKEN'
    }
  }));
};
 
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  switch (message.type) {
    case 'connection_ack':
      // 3. Conexión autenticada, ahora suscribirse
      ws.send(JSON.stringify({
        id: '1',
        type: 'subscribe',
        payload: {
          query: `
            subscription SubscribeRuntimeLog(
              $projectID: ObjectID!
              $serviceID: ObjectID!
              $environmentID: ObjectID!
            ) {
              runtimeLogReceived(
                projectID: $projectID
                serviceID: $serviceID
                environmentID: $environmentID
              ) {
                timestamp
                message
              }
            }
          `,
          variables: {
            projectID: 'your-project-id',
            serviceID: 'your-service-id',
            environmentID: 'your-environment-id'
          }
        }
      }));
      break;
      
    case 'next':
      // 4. Datos de suscripción recibidos
      console.log('Datos:', message.payload.data);
      break;
      
    case 'error':
      console.error('Error de suscripción:', message.payload);
      break;
      
    case 'complete':
      console.log('Suscripción completada');
      break;
  }
};
 
ws.onerror = (error) => console.error('Error de WebSocket:', error);
ws.onclose = (event) => console.log('WebSocket cerrado:', event.code);

Tipos de Mensaje (protocolo graphql-ws)

Cliente → Servidor

TipoDescripción
connection_initInicializar conexión con payload de autenticación
subscribeIniciar una suscripción
completeDetener una suscripción
pingPing de keep-alive

Servidor → Cliente

TipoDescripción
connection_ackConexión aceptada
nextDatos de suscripción
errorError de suscripción
completeSuscripción finalizada
pongRespuesta de keep-alive

Suscripciones Disponibles

Registros de Tiempo de Ejecución

Suscribirse a registros de tiempo de ejecución en tiempo real de un servicio:

subscription SubscribeRuntimeLog(
  $projectID: ObjectID!
  $serviceID: ObjectID!
  $environmentID: ObjectID!
) {
  runtimeLogReceived(
    projectID: $projectID
    serviceID: $serviceID
    environmentID: $environmentID
  ) {
    timestamp
    message
  }
}

Registros de Tiempo de Ejecución (con filtro de Despliegue)

Filtrar registros de un despliegue específico:

subscription SubscribeRuntimeLogWithDeployment(
  $projectID: ObjectID!
  $serviceID: ObjectID!
  $environmentID: ObjectID!
  $deploymentID: ObjectID
) {
  runtimeLogReceived(
    projectID: $projectID
    serviceID: $serviceID
    environmentID: $environmentID
    deploymentID: $deploymentID
  ) {
    timestamp
    message
  }
}

Registros de Compilación

Suscribirse a registros de compilación de un despliegue:

subscription SubscribeBuildLog(
  $projectID: ObjectID!
  $deploymentID: ObjectID!
) {
  buildLogReceived(
    projectID: $projectID
    deploymentID: $deploymentID
  ) {
    timestamp
    message
  }
}

Actividad del Proyecto

Suscribirse a eventos a nivel de proyecto (compilaciones, despliegues, cambios de estado del servicio):

subscription SubscribeProjectActivity($projectID: ObjectID!) {
  projectActivityReceived(projectID: $projectID) {
    type
    payload
  }
}

Manejo de Errores

Errores de Autenticación

Si el token falta o es inválido, el servidor rechazará la conexión durante la fase connection_init:

{
  "type": "error",
  "payload": {
    "message": "Please establish websocket connection with a valid token"
  }
}

Errores de Permisos

Si está autenticado pero carece de acceso a un recurso:

{
  "type": "next",
  "id": "1",
  "payload": {
    "errors": [{
      "message": "Permission denied",
      "path": ["runtimeLogReceived"],
      "extensions": {
        "code": "FORBIDDEN"
      }
    }],
    "data": null
  }
}

Causas comunes:

  • IDs de proyecto/servicio/entorno incorrectos
  • El usuario no tiene acceso al proyecto
  • Los IDs no coinciden (por ejemplo, el servicio no pertenece al proyecto)

Prueba de Conexiones

Herramientas de Desarrollador del Navegador

  1. Abre la pestaña Red
  2. Filtra por “WS”
  3. Navega a una página con suscripciones (por ejemplo, registros de despliegue)
  4. Haz clic en la conexión WebSocket para inspeccionar los frames

Página de Prueba Independiente

Guarda este archivo HTML y ábrelo en un navegador donde hayas iniciado sesión en Zeabur:

<!DOCTYPE html>
<html>
<head>
  <title>Prueba WebSocket de Zeabur</title>
  <style>
    body { font-family: system-ui; padding: 20px; }
    input { margin: 5px; padding: 8px; }
    button { padding: 8px 16px; cursor: pointer; }
    pre { background: #f5f5f5; padding: 15px; overflow: auto; max-height: 400px; }
  </style>
</head>
<body>
  <h1>Prueba de Conexión WebSocket de Zeabur</h1>
  <div>
    <input id="projectId" placeholder="ID del Proyecto" />
    <input id="serviceId" placeholder="ID del Servicio" />
    <input id="envId" placeholder="ID del Entorno" />
    <button onclick="connect()">Conectar</button>
  </div>
  <pre id="logs"></pre>
  
  <script>
    function log(msg) {
      const el = document.getElementById('logs');
      el.textContent += new Date().toISOString() + ' | ' + msg + '\n';
      el.scrollTop = el.scrollHeight;
    }
    
    function connect() {
      const token = document.cookie.split('; ')
        .find(r => r.startsWith('token='))?.split('=')[1];
      
      if (!token) {
        log('ERROR: No se encontró token en las cookies. Asegúrate de haber iniciado sesión en Zeabur.');
        return;
      }
      
      log('Token encontrado, conectando...');
      const ws = new WebSocket('wss://api.zeabur.com/graphql', 'graphql-transport-ws');
      
      ws.onopen = () => {
        log('Conectado, enviando autenticación...');
        ws.send(JSON.stringify({
          type: 'connection_init',
          payload: { authToken: token }
        }));
      };
      
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        log('Recibido: ' + JSON.stringify(msg, null, 2));
        
        if (msg.type === 'connection_ack') {
          log('¡Autenticado exitosamente! Iniciando suscripción...');
          ws.send(JSON.stringify({
            id: '1',
            type: 'subscribe',
            payload: {
              query: `subscription($p:ObjectID!,$s:ObjectID!,$e:ObjectID!){
                runtimeLogReceived(projectID:$p,serviceID:$s,environmentID:$e){
                  timestamp message
                }
              }`,
              variables: {
                p: document.getElementById('projectId').value,
                s: document.getElementById('serviceId').value,
                e: document.getElementById('envId').value
              }
            }
          }));
        }
      };
      
      ws.onerror = (e) => log('Error de WebSocket: ' + JSON.stringify(e));
      ws.onclose = (e) => log('Conexión Cerrada: code=' + e.code + ', reason=' + e.reason);
    }
  </script>
</body>
</html>

Consideraciones de Seguridad

⚠️

El token de autenticación accedido a través de document.cookie se almacena en una cookie no HttpOnly. Esta es una compensación necesaria porque WebSockets no puede enviar automáticamente cookies HttpOnly.

Mitigaciones

  1. Política de Seguridad de Contenido (CSP) - Prevenir ataques XSS que podrían robar el token
  2. Expiración corta del token - Limitar el daño si el token se ve comprometido
  3. Atributo SameSite de cookie - Prevenir ataques CSRF
  4. Usar Claves API del lado del servidor - Para aplicaciones backend, usa claves API en lugar de tokens de sesión

Solución de Problemas

ProblemaSolución
No se recibe connection_ackVerifica que el token sea válido y no haya expirado
Permission denied en la suscripciónVerifica que los IDs sean correctos y el usuario tenga acceso
La conexión se cierra inmediatamenteVerifica la URL de WebSocket y asegúrate de que el subprotocolo sea graphql-transport-ws
No se reciben datosVerifica que el servicio esté funcionando y generando registros
WebSocket connection failedVerifica la conectividad de red y la configuración del firewall

Recursos Relacionados