← GeoStream

15 — Guía de integración para sistemas terceros

ID: GS-INT-001 · Versión: 0.2.1 · Requisitos: FR-INT-001..004, NFR-DX-001

Esta guía es normativa en los flujos y en las reglas para integradores; los fragmentos de código son ILUSTRATIVOS y sus versiones ejecutables viven en examples/ (F8), probadas en CI.

1. Qué hace cada parte

Parte Responsabilidades
Backend del integrador Autenticar a sus usuarios; guardar la API key; emitir tokens de dispositivo (POST /v1/tokens); opcionalmente crear sesiones y miembros; recibir webhooks; opcionalmente consumir el stream con locations:read
App del integrador Integrar el SDK; implementar CredentialProvider contra su backend; obtener consentimiento del usuario antes de sharing.start; pedir permisos de ubicación con buena UX; mostrar el estado de compartición
GeoStream Autorizar, distribuir, revocar, notificar; no guardar historial por defecto

2. Integración paso a paso

  1. Credenciales: el operador ejecuta geostream admin tenant create, app create y key create (16 §3). Guarda la key en el gestor de secretos del backend.
  2. Endpoint de tokens en tu backend (§6.1): recibe el installation_id desde tu app autenticada, llama POST /v1/tokens con el external_subject_id de tu usuario y devuelve {access_token, expires_at}.
  3. CredentialProvider en la app: llama a ese endpoint con la sesión de tu usuario.
  4. Sesiones: créalas desde tu backend (recomendado para flujos de negocio: viajes, pedidos) o desde la app (flujos sociales: familia, eventos).
  5. Compartir y mostrar: sharing.start / sessions.watch en la app.
  6. Webhooks (opcional): registra un endpoint y verifica firmas (§6.2).
  7. Stream del lado servidor (opcional): para despacho o monitoreo, con una key que tenga stream:connect y locations:read (§5).

3. Patrones de integración

3.1 Ride-hailing (UC-01)

sequenceDiagram
  participant BE as Backend integrador
  participant GS as GeoStream
  participant D as App conductor
  participant R as App pasajero
  BE->>GS: POST /v1/sessions {ONE_TO_ONE, initial_members:[driver, rider], expires_in_seconds}
  BE-->>D: push/WS propio: "viaje asignado" + session_id
  BE-->>R: idem
  D->>GS: PUT …/members/me/sharing {true}
  R->>GS: PUT …/members/me/sharing {true} (opcional)
  D->>GS: stream: Subscribe + PublishLocation…
  R->>GS: stream: Subscribe
  GS-->>R: LocationUpdate (conductor)
  BE->>GS: POST …/close (viaje terminado)
  GS-->>D: SubscriptionEnded SESSION_CLOSED
  GS-->>R: SubscriptionEnded SESSION_CLOSED

Reglas: usar Idempotency-Key derivada del ID del viaje; cerrar siempre la sesión al terminar (no depender solo de la expiración); expires_in_seconds = duración máxima razonable del viaje.

3.2 Delivery con seguimiento para el cliente (UC-02)

3.3 Familia o equipo (UC-03)

3.4 Flota (UC-04)

3.5 Evento temporal (UC-05)

Sesión con expiración de horas, invitación multiuso, snapshot_ttl_seconds corto. La expiración automática limpia todo.

3.6 IoT / gateways (UC-08, F8)

4. Manejo de errores recomendado

5. Consumidor de stream del lado servidor

6. Ejemplos (ILUSTRATIVOS)

6.1 Endpoint de tokens (Node.js / Express)

// ILUSTRATIVO — versión ejecutable en examples/token-backend-node (F8)
app.post("/geostream/token", requireUserSession, async (req, res) => {
  const { installation_id, platform } = req.body;          // del SDK
  const r = await fetch(`${GS_BASE_URL}/v1/tokens`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.GS_API_KEY}`,  // nunca en la app
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      external_subject_id: req.user.opaqueId,               // ID opaco, nunca email
      installation_id,
      platform,
      ttl_seconds: 900,
    }),
  });
  if (!r.ok) return res.status(502).json({ error: "token_unavailable" });
  const t = await r.json();
  res.json({ access_token: t.access_token, expires_at: t.expires_at });
});

6.3 Relevo de seguimiento para el cliente final (Node.js, ejecutable)

examples/tracking-relay-node/relay.mjs (sin dependencias, Node ≥ 20). Reglas que implementa y que debes mantener:

6.2 Verificación de webhooks (Node.js)

// ILUSTRATIVO — versión ejecutable en examples/webhook-receiver-node (F8)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: Buffer, headers: Record<string, string>, secret: string): boolean {
  const id = headers["gs-webhook-id"];
  const ts = headers["gs-webhook-timestamp"];
  const sigHeader = headers["gs-webhook-signature"] ?? "";
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
    .update(`${id}.${ts}.`).update(rawBody).digest("hex");
  return sigHeader.split(" ").some((part) => {
    const [v, sig] = part.split("=");
    return v === "v1" && sig?.length === expected.length &&
      timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

El receptor DEBE deduplicar por GS-Webhook-Id y responder 2xx rápido (procesar en segundo plano).

6.1 Integración de la pista E

Los cuatro patrones que cambian con la pista E, con su requisito previo.

6.1.1 Flota con ETA y asignación (E4 + E5)

  1. El backend crea un POOL con pools:manage.
  2. Cada dispositivo entra solo, activa sharing y marca availability = BUSY al aceptar un encargo.
  3. El backend llama a GET /v1/pools/{id}/nearby?rank=eta con origen y destino, y a POST /v1/tools/assignments para repartir las tareas.
  4. El cliente recibe las herramientas por el mismo stream (SessionView), no por una llamada extra.

6.1.2 Historial y analítica (E6)

  1. El tenant activa history_allowed y la DPIA (tarea T-E6-11) está cerrada.
  2. El backend crea sesiones con history_mode = TRACE_AND_ANALYTICS.
  3. El usuario concede el consentimiento por separado para HISTORY y para ANALYTICS, desde su dispositivo. El backend no puede concederlo (INV-SHR-5).
  4. Se consulta con history:read / analytics:read, que son scopes distintos y se conceden uno a uno.

6.1.3 BYOK de proveedor (E5)

  1. PUT /v1/providers/ROUTE_MATRIX con la credencial del tenant y terms.checked_on.
  2. Se revisa GET /v1/analytics/provider-usage para controlar coste y volumen.
  3. Si el contrato con el proveedor no es aceptable, se usa el grafo vial propio (GS_GRAPH_PATHS) y ninguna coordenada sale de la infraestructura del tenant.

6.1.4 Tags BLE (E8)

  1. El usuario escanea el tag (la EIK se queda en el tag).
  2. Se registra con tags:manage: modelo, nombre, nada más.
  3. Buscadores autorizados envían reportes cifrados; el servidor los almacena sin poder leerlos.
  4. El dueño consulta sus sightings con edge:lookup y los descifra en su cliente.

7. Checklist de producción del integrador

8. Antipatrones

9. Antipatrones de la pista E

Los cinco anteriores siguen siendo antipatrones. Estos se añaden:

10. Consumir el despliegue gestionado (geostream.gravitalcloud.com)

Esta sección es operativa: describe cómo un proyecto (propio o de terceros) consume la instancia de Railway con lo que ya está implementado. Lo que aún requiere decisión está enlazado al final.

10.1 Puntos de entrada

Uso URL
REST https://geostream.gravitalcloud.com/v1/… (contrato: contracts/openapi/geostream.v1.yaml)
Stream wss://geostream.gravitalcloud.com/v1/stream?ticket=<gst_…>, subprotocolo gs.v1.proto (§08 §2). gs.v1.json está apagado en prod (17, GS_STREAM_JSON_ENABLED)
Salud GET /health/live, GET /health/ready

10.2 Alta de un proyecto consumidor

Cada proyecto es una aplicación dentro de un tenant, con su propia API key y sus scopes mínimos (06 §3.4). Hoy el alta la hace el operador por CLI (16 §3); no hay registro de cuentas por HTTP (FR-ADM-004 es F15).

geostream admin tenant create --name <proyecto>
geostream admin app create --tenant ten_… --name <proyecto>-movil \
  --device-scopes sessions:create,invitations:redeem,stream:connect,sharing:write,ingest:write
geostream admin key create --app app_… --scopes tokens:mint,sessions:write,members:write,sessions:read

Proyectos distintos van en tenants distintos: toda consulta filtra por tenant_id y un tenant nunca ve datos de otro (06 §8).

10.3 Autenticación de usuarios con la identidad que ya tiene cada proyecto

GeoStream no gestiona cuentas de usuario final ni contraseñas: confía en el backend de cada proyecto, que ya autentica a sus usuarios (OTP, WebAuthn/passkeys, lo que use). El patrón es un intercambio de token:

sequenceDiagram
  participant App as App del proyecto
  participant BE as Backend del proyecto (ya autentica usuarios)
  participant GS as GeoStream
  App->>BE: POST /geo/token (sesión propia del proyecto, installation_id)
  BE->>BE: verifica la sesión con SU identidad
  BE->>GS: POST /v1/tokens (API key, external_subject_id opaco)
  GS-->>BE: access_token (≤ GS_TOKEN_MAX_TTL_S)
  BE-->>App: access_token, expires_at
  App->>GS: REST / stream con el token de dispositivo

10.4 Dispositivos: móvil, microcontrolador o gateway

Toda muestra lleva coordenada y su radio de confianza en metros; sin horizontal_accuracy_m válido la muestra se rechaza (09 §3).

Dispositivo Transporte recomendado Notas
App móvil SDK (F5) o apps/flutter_app como referencia Ingesta REST o stream
Microcontrolador con IP (ESP32 + GNSS, módulo LTE-M) Stream gs.v1.proto: una conexión persistente y PublishLocation binario; Protobuf con nanopb Una muestra con posición, precisión, secuencia, instante y calidad ocupa del orden de 60 bytes. El dispositivo pide su token a tu backend (nunca guarda una API key) y lo renueva antes de expires_at con AuthRefresh (08 §6.6)
Microcontrolador sin stream POST /v1/ingest/locations (JSON, hasta 50 muestras por lote) Más simple; útil para lotes tras pérdida de cobertura
Rastreador LPWAN/celular A través del servidor de red del operador (25 §4) Pendiente de DR-20261018-04

Campos que un dispositivo debe rellenar con honestidad (contracts/proto/geostream/v1/location.proto): latitude, longitude, horizontal_accuracy_m (el radio de precisión que da el receptor GNSS en cada medición, nunca un valor fijo), captured_at (instante de la medición, no de envío), sequence (monotónica y persistida, 12 §9), quality.source (GNSS, NETWORK, FUSED…) y quality.is_mock si la plataforma lo sabe. Opcionales: velocidad, rumbo, altitud y sus precisiones.

10.5 Precisión y ubicaciones falsas: qué hace hoy el servidor

Mecanismo Estado Dónde
Rechazar muestras con precisión peor que un umbral por tenant Implementado. max_horizontal_accuracy_m, rango 5..=100000 m geostream admin policy set --tenant ten_… max_horizontal_accuracy_m=25
Rechazar 0,0 (Null Island), coordenadas no finitas, secuencias repetidas, muestras del futuro Implementado 09 §3
Señales de plausibilidad por muestra (MOCK_FLAGGED, velocidad o aceleración imposibles, fix congelado, solo red, baja precisión) y puntuación plausibility Implementado: viajan en estimate de cada LocationUpdate del stream 22 §2.2, 22 §2.7
Rechazar ubicaciones simuladas (is_mock = true) Implementado, opcional por tenant geostream admin policy set --tenant ten_… reject_mock_samples=true (09 §3.1 regla 17)
Rechazar muestras solo de red (sin GNSS) Implementado, opcional por tenant reject_network_only=true (regla 18)
Atestación del dispositivo (Play Integrity, App Attest, elemento seguro) Pendiente de ADR docs/decisions/accepted/DR-20261031-02.md fase 2

Ninguna técnica del lado servidor impide del todo que un dispositivo rooteado o un firmware modificado invente coordenadas: lo que se puede hacer es encarecer el engaño (atestación de la app, coherencia física de la trayectoria) y dejar la sanción al negocio del integrador. Una muestra rechazada vuelve con status = REJECTED, error_code = INVALID_LOCATION y field (quality.is_mock, quality.source o position.horizontal_accuracy_m).