# Game tokens

A game token is the only credential a game ever gets. It is an Ed25519-signed JWT (`alg: EdDSA`), valid for 15 minutes, for one game (`aud`).

## Claims

| Claim | First-party | Third-party |
|---|---|---|
| `iss` | `https://id.sojo.gg` | same |
| `aud` | your game id | same |
| `sub` | sojo account id (`a_…`) | pairwise id (`p_…`): HMAC of game + account, different in every game |
| `h`, `n`, `p` | handle, name, picture | same, after the player agreed on the consent screen |
| `perks` | active perks, e.g. `["supporter"]` (Royal, code id `supporter`, in every game; see supporter.md) | same |
| `loc` | the player's language, one of your locales | same |
| `sid` | session id (`s_…`) | pairwise session id |
| `xid` | X user id | never sent |
| `did` | device id | never sent |
| `iat`, `exp` | seconds | same |

**How the player signed in doesn't matter.** Post-and-paste, Connect with X (sojo.gg), the popup and Tweetcraft all land on the same account for the same X user id, so the token, `sub`, perks and grants are identical.

**Key players on `sub`, never on `h`.** The handle is a display label: players rename on X, and an old handle can be taken by someone else. Sojo keys every account on the X user id, so a rename keeps the same `sub`, and a new person on an old handle gets a different `sub`.

## Verify

Keys: `https://id.sojo.gg/jwks.json` (`kid` in the header). Use the helper:

```js
import { verifySojoToken, SojoTokenError } from "@sojogg/sdk/server";
try {
  const c = await verifySojoToken(token, { aud: "my-game" });
} catch (e) {
  if (e instanceof SojoTokenError) e.code; // malformed | bad_alg | jwks_unavailable | unknown_kid | bad_signature | bad_issuer | bad_audience | expired | not_yet_valid
}
```

It caches the JWKS for an hour and refetches at most once a minute for an unknown `kid` (key rotation). 30 s leeway on `exp`.

Doing it yourself: check `alg === "EdDSA"`, the signature over `header.payload` with the JWK for `kid`, `iss`, `aud`, `exp`.

## What a token can't do

- It can't mint tokens or call any other Sojo endpoint.
- It is accepted only by your server and by `POST /v1/e` for its own `aud`.
- A suspended or archived game gets no new tokens; a Disconnected player gets no new tokens for that game. Both take effect within one token lifetime.
