# Service principals

A **service principal** is a Workload's own identity. It exists so that a hub's code can create duel servers **without** anyone pasting a personal key into an environment variable.

- At most **one principal per Workload**, created by a person with manage access.
- It holds **grants**: a target Workload of the **same project** (itself included) and one capability each:
  - `units.read` reads units and the event stream.
  - `units.control` also creates, starts, stops, destroys and allocates, and includes reading.
- It can never change the scaling policy, the webhooks, or its own grants.

## Create it and grant access

**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` is idempotent and a new principal holds **no grant**. `PUT .../principal/grants` replaces the whole list (at most 64; an empty list removes everything). You must be able to manage every Workload you name: otherwise `grant_target_unavailable` and nothing changes. A grant takes effect on the program's next request.

Enabling a principal changes the Workload's pod template, so it applies **as units restart**, by the Workload's rollout rule.

## Code inside a unit

Every unit of a Workload with a principal gets two things: `ZAROZ_API_URL` in its environment and a token file at `/zaroz/credentials/token`. The token lives 60 minutes and the platform replaces it before it expires, so **read the file on every request** (or when you get a `401`) and never cache it.

**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,
)
```


## A program outside Zaroz

A matchmaker running on your own server, or a CI job, mints an **external credential** for the principal. It acts with the principal's grants, has a name, an expiry (1 to 365 days, default 90), is revocable, and tracks `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
```


Send the token as `Authorization: Bearer zaroz_sp_...`. List credentials with `GET .../principal/credentials` and revoke one with `DELETE .../principal/credentials/{credential_id}`.

## Limits and audit

A principal is limited to 50 requests per second. Every call it makes is audited with the principal id and token kind. Disabling the principal (`DELETE .../principal`) revokes every token. Tearing down the Workload that owns it revokes it too, and tearing down a target Workload deletes the grants that name it.
