# Principales de servicio

Un **principal de servicio** es la identidad propia de un Workload. Existe para que el código de un hub pueda crear servidores de duelo **sin** que nadie pegue una clave personal en una variable de entorno.

- Como máximo **un principal por Workload**, creado por una persona con acceso de gestión.
- Tiene **permisos**: un Workload destino del **mismo proyecto** (el propio incluido) y una capacidad por cada uno:
  - `units.read` lee unidades y el flujo de eventos.
  - `units.control` además crea, inicia, detiene, destruye y asigna, e incluye leer.
- Nunca puede cambiar la política de escalado, los webhooks ni sus propios permisos.

## Crearlo y darle acceso

**curl**

```bash
P="$ZAROZ_API/projects/$PROJECT/service-instances/$SERVICE/principal"
curl -sS -X PUT "$P" -H "$AUTH"                                  # 201 created / 200 already there
curl -sS -X PUT "$P/grants" -H "$AUTH" -H "Content-Type: application/json" -d '{
  "grants": [
    {"target_service_instance_id": "<duels service uuid>",  "capability": "units.control"},
    {"target_service_instance_id": "<lobbies service uuid>", "capability": "units.read"}
  ]
}'
```

**TypeScript**

```ts
const P = `/projects/${PROJECT}/service-instances/${SERVICE}/principal`;
await call("PUT", P);                                               // 201 created / 200 already there
await call("PUT", `${P}/grants`, {
  grants: [
    { target_service_instance_id: DUELS, capability: "units.control" },
    { target_service_instance_id: LOBBIES, capability: "units.read" },
  ],
});
```

**Python**

```python
P = f"/projects/{PROJECT}/service-instances/{SERVICE}/principal"
call("PUT", P)                                                      # 201 created / 200 already there
call("PUT", f"{P}/grants", {"grants": [
    {"target_service_instance_id": DUELS, "capability": "units.control"},
    {"target_service_instance_id": LOBBIES, "capability": "units.read"},
]})
```


`PUT .../principal` es idempotente y un principal nuevo **no tiene ningún permiso**. `PUT .../principal/grants` reemplaza la lista entera (máximo 64; una lista vacía los quita todos). Debes poder gestionar cada Workload que nombres: si no, `grant_target_unavailable` y no cambia nada. Un permiso surte efecto en la siguiente petición del programa.

Activar un principal cambia la plantilla de pods del Workload, así que se aplica **a medida que las unidades se reinician**, según la regla de despliegue del Workload.

## Código dentro de una unidad

Cada unidad de un Workload con principal recibe dos cosas: `ZAROZ_API_URL` en su entorno y un archivo de token en `/zaroz/credentials/token`. El token dura 60 minutos y la plataforma lo reemplaza antes de que caduque, así que **lee el archivo en cada petición** (o al recibir un `401`) y nunca lo guardes en caché.

**curl**

```bash
# inside a unit of a service that has a principal
curl -sS "$ZAROZ_API_URL/projects/$ZAROZ_PROJECT_ID/service-instances/$TARGET/units?label=mode:2v2" \
  -H "Authorization: Bearer $(cat /zaroz/credentials/token)"
```

**TypeScript**

```ts
import { readFileSync } from "node:fs";

// The mounted token is rotated by the platform: read it per request, never cache it.
const token = () => readFileSync("/zaroz/credentials/token", "utf8").trim();
const res = await fetch(
  `${process.env.ZAROZ_API_URL}/projects/${process.env.ZAROZ_PROJECT_ID}/service-instances/${TARGET}/units?label=mode:2v2`,
  { headers: { Authorization: `Bearer ${token()}` } },
);
```

**Python**

```python
import os

def token() -> str:
    # rotated by the platform: read it per request, never cache it
    with open("/zaroz/credentials/token") as f:
        return f.read().strip()

r = requests.get(
    f"{os.environ['ZAROZ_API_URL']}/projects/{os.environ['ZAROZ_PROJECT_ID']}/service-instances/{TARGET}/units",
    params={"label": "mode:2v2"},
    headers={"Authorization": "Bearer " + token()},
    timeout=10,
)
```


## Un programa fuera de Zaroz

Un matchmaker en tu propio servidor, o un trabajo de CI, crea una **credencial externa** para el principal. Actúa con los permisos del principal, tiene nombre y caducidad (de 1 a 365 días, 90 por defecto), es revocable y registra `last_used_at`.

**curl**

```bash
curl -sS -X POST "$P/credentials" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "matchmaker-eu", "expires_in_days": 90}'
# {"credential": {...}, "token": "zaroz_sp_..."}   the token is shown this once
```

**TypeScript**

```ts
const cred = await call("POST", `${P}/credentials`, { name: "matchmaker-eu", expires_in_days: 90 });
console.log(cred.body.token);   // zaroz_sp_...: store it in your secret manager now
```

**Python**

```python
status, cred = call("POST", f"{P}/credentials", {"name": "matchmaker-eu", "expires_in_days": 90})
print(cred["token"])   # zaroz_sp_...: store it in your secret manager now
```


Envía el token como `Authorization: Bearer zaroz_sp_...`. Lista las credenciales con `GET .../principal/credentials` y revoca una con `DELETE .../principal/credentials/{credential_id}`.

## Límites y auditoría

Un principal está limitado a 50 peticiones por segundo. Cada llamada suya se audita con el id del principal y el tipo de token. Desactivar el principal (`DELETE .../principal`) revoca todos sus tokens. Eliminar el Workload que lo posee también lo revoca, y eliminar un Workload destino borra los permisos que lo nombran.
