road-runner-common 0.10.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
# Gateway Identity Propagation Contract (v1)

`road-runner-common` is the single source of truth for authentication/authorization across all
Rust services. Authentication happens **once, at the edge gateway**; downstream services trust
the gateway and never re-validate tokens. The gateway forwards the resolved identity as signed
`x-auth-*` headers, and each service builds a [`UserContext`] from them.

## Trust model

- The gateway is the only ingress. It **must strip** any client-supplied `x-auth-*` headers and
  re-emit its own.
- Defense-in-depth: the gateway **signs** the identity headers with a shared HMAC secret. Services
  verify the signature (`gateway::gateway_resolver`), so a spoofed header that bypasses network
  isolation is still rejected.
- Network isolation (mTLS / NetworkPolicy) remains the first line of defense; the signature is the
  second.

## Headers (gateway → service)

| Header                   | Required | Meaning                                              |
|--------------------------|----------|------------------------------------------------------|
| `x-auth-principal-type`  | no       | `user` \| `apikey` \| `service` (default `user`)     |
| `x-auth-user-sub`        | yes*     | canonical subject; user UUID (owner UUID for apikey) |
| `x-auth-user-email`      | no       | email                                                |
| `x-auth-user-username`   | no       | username                                             |
| `x-auth-roles`           | no       | comma-separated roles                                |
| `x-auth-scopes`          | no       | comma-separated API-key permissions (Core catalog)   |
| `x-auth-capabilities`    | no       | comma-separated account capabilities (entitlements)  |
| `x-auth-apikey-id`       | apikey   | API key id                                           |
| `x-auth-issued-at`       | yes**    | unix seconds (replay protection)                     |
| `x-auth-signature`       | yes**    | lowercase hex HMAC-SHA256 of the canonical string    |

\* Without `x-auth-user-sub` the request is treated as anonymous (the per-service `required` flag
and the per-endpoint `UserContext` extractor decide whether that is allowed).
\** Required whenever signature verification is enabled (the default).

## Canonical signing string (v1)

Fields joined with `\n`, missing optionals are the empty string. `roles`/`scopes`/`issued_at`
are the **exact** raw header values, so the gateway must build this identically:

```text
v1
<sub>
<principal>          # "user" | "apikey" | "service"
<email>
<username>
<apikey_id>
<roles>              # raw csv
<scopes>             # raw csv
<capabilities>       # raw csv
<issued_at>
```

`signature = lowercase_hex( HMAC_SHA256( secret, canonical ) )`

## Service configuration

| Env var                         | Default | Meaning                                        |
|---------------------------------|---------|------------------------------------------------|
| `AUTH_GATEWAY_HMAC_SECRET`      || shared secret (required when verifying)        |
| `AUTH_GATEWAY_VERIFY_SIGNATURE` | `true`  | fail closed: reject missing/invalid signatures |
| `AUTH_GATEWAY_MAX_AGE_SECS`     | `300`   | replay window for `x-auth-issued-at`           |

Verification on with no secret is a hard startup error (fail closed).

## Usage in a service

```rust
use road_runner_common::gateway::{gateway_resolver, GatewayAuthConfig};
use road_runner_common::middleware::UserContextMiddleware;

let cfg = GatewayAuthConfig::from_env()?;                 // in main()
let auth = UserContextMiddleware::new(gateway_resolver(cfg)).required(false);
// `.wrap(auth)` on the App. Public routes (health, swagger) stay open;
// user handlers take `user_ctx: UserContext` and 401 automatically when absent.
```

Services with their own async identity resolution (e.g. `identity-account`, which reads Redis
sessions) keep their own middleware but **build the same `UserContext` type** — the synchronous
`gateway_resolver` is for the common stateless case.

## API-key permissions (Core catalog)

API keys are member-scoped and self-service. A member picks permissions from a fixed catalog
(`road_runner_common::permissions`): `read`, `spot`, `derivatives`, `wallet_transfer`,
`withdraw`. A key with only `read` is a BEARER (hash-verified) key; any write permission makes it
an HMAC (request-signed) key. `withdraw` requires an IP allowlist on the key.

## Authorization helpers

```rust
user_ctx.require_role("trader")?;
user_ctx.require_any_role(&["admin", "ops"])?;
user_ctx.require_scope("spot")?;     // API-key permission
user_ctx.require_admin()?;            // ADMIN or *_ADMIN
```