> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.adid.dev/llms.txt.
> For full documentation content, see https://docs.adid.dev/llms-full.txt.

# CORS, Rate Limiting, Audit

The platform stacks several controls on top of authentication and RBAC. Each is implemented in `packages/api/internal/middleware/*` and is composable — the production server enables all of them.

#### 13.7.1 CORS

CORS is configured in the router using Chi's `cors` middleware. The default production configuration:

```go
cors.Handler(cors.Options{
    AllowedOrigins:   []string{"https://adid.dev", "https://wallet.adid.dev"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Authorization", "X-API-Key", "Content-Type", "X-Request-Id"},
    ExposedHeaders:   []string{"X-RateLimit-Limit", "X-RateLimit-Remaining", "X-Request-Id"},
    AllowCredentials: true,
    MaxAge:           300,
})
```

> **VERIFY:** confirm the exact CORS config block in `router.go`.
> **Dev origins:** `http://localhost:5173` (Portal Vite) and `http://localhost:8081` (Expo dev). Add only what you need; never use `*` with `AllowCredentials: true` (browsers will reject it).

#### 13.7.2 Rate limiting

Verified in `packages/api/internal/middleware/ratelimit.go`. Implements a **token bucket** per client:

| Setting                                 | Default    | Configurable via                    |
| --------------------------------------- | ---------- | ----------------------------------- |
| Refill rate                             | 100 req/s  | `RateLimiterConfig.DefaultRate`     |
| Burst capacity                          | 200        | `RateLimiterConfig.DefaultBurst`    |
| Cleanup interval (idle bucket eviction) | 10 minutes | `RateLimiterConfig.CleanupInterval` |

**Client identity** (priority order, see `identifyClient` in `ratelimit.go:159-181`):

1. `X-API-Key` header (most specific — gives the partner their own bucket)
2. `Authorization` header (first 20 chars used as key)
3. `X-Forwarded-For` IP
4. `X-Real-IP`
5. `RemoteAddr`

**Response headers** (always set, even on success):

```
X-RateLimit-Limit:     100
X-RateLimit-Remaining: 87
X-RateLimit-Reset:     1745672461
```

**On limit exceeded** (`429 Too Many Requests`):

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{ "error": "rate limit exceeded", "status": 429 }
```

**Per-key limits** (programmatic at startup):

```go
rateLimiter.SetKeyLimit("trusted-partner", 1000, 2000)  // 1000 req/s, burst 2000
```

> **Recommended production settings:**
>
> * Public endpoints (`/credentials/verify`, `/dids/resolve`): 50 req/s burst 100 per IP.
> * Authenticated user endpoints: 100 req/s burst 200 per JWT.
> * Admin / API-key callers: 500 req/s burst 1000 per key.

#### 13.7.3 Audit logging

The platform ships with two layers of logging (verified in `middleware/logging.go`):

**Layer 1 — Request log (Zap, structured JSON):**

```jsonc
{
  "level": "info",
  "ts":    "2026-04-26T13:45:12.183Z",
  "msg":   "request completed",
  "method":"POST",
  "path":  "/api/v1/credentials/issue",
  "status":201,
  "latency_ms":  84,
  "remote_addr": "203.0.113.42",
  "request_id":  "req_01HXPKR7Y7M2ZP",
  "auth_scheme": "bearer",
  "auth_role":   "issuer",
  "auth_subject":"8b1f-...-c2e0"
}
```

Every line includes a **request\_id** (UUIDv7 / ULID) propagated via `X-Request-Id` so a client log can be tied to a server log without grepping.

**Layer 2 — Audit events (table `audit_events`, append-only):**

```sql
INSERT INTO audit_events (id, occurred_at, actor_subject, actor_role, action, resource_type, resource_id, request_id, ip, metadata)
VALUES (gen_random_uuid(), now(), '8b1f...', 'issuer', 'credentials.revoke',
        'credential', '67e5...c8', 'req_01HXPKR7Y7M2ZP', '203.0.113.42',
        '{"reason":"compromised"}');
```

Recommended audit-worthy actions (issuer/verifier-side):

| Action key                                           | Triggered by                |
| ---------------------------------------------------- | --------------------------- |
| `auth.login.success` / `.failure`                    | OTP verify / OAuth callback |
| `auth.token.refresh`                                 | Refresh endpoint            |
| `auth.logout`                                        | Logout endpoint             |
| `did.create` / `.update` / `.deactivate` / `.rotate` | DID CRUD                    |
| `credentials.issue` / `.revoke` / `.batch_revoke`    | Issuer flows                |
| `verifier.trusted_issuer.add` / `.remove`            | Trust framework             |
| `agent.register` / `.update` / `.decommission`       | Agent CRUD                  |
| `agent.delegate`                                     | IBCT issued                 |
| `zkp.proof.submit` / `.verify`                       | ZKP flows                   |

> **VERIFY:** confirm whether `audit_events` is the canonical table or whether logging is solely via Zap. If only Zap, recommend adding the table for compliance retention.
> **Retention:** 90 days hot in Postgres, then archive to S3 / object storage. Compliance environments (EU AI Act, ISO 27001) typically require 12–24 months.

#### 13.7.4 Security headers

The Portal nginx config (`packages/portal/nginx.conf`) and the API edge are responsible for:

| Header                      | Recommended value                                                                                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload`                                                                                                                      |
| `X-Content-Type-Options`    | `nosniff`                                                                                                                                                           |
| `X-Frame-Options`           | `DENY`                                                                                                                                                              |
| `Referrer-Policy`           | `strict-origin-when-cross-origin`                                                                                                                                   |
| `Content-Security-Policy`   | `default-src 'self'; connect-src 'self' https://adid.dev; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-ancestors 'none'` |
| `Permissions-Policy`        | `camera=(), microphone=(), geolocation=(self)`                                                                                                                      |

> **CSP note:** the Portal's React build inlines a small CSS critical block — `'unsafe-inline'` for styles is the minimum compromise; remove it once styled-components or `nonce` is wired up.

#### 13.7.5 TLS & certificate hygiene

* **TLS 1.2 minimum** at the edge (1.3 preferred). Disable RC4, 3DES, SHA-1.
* **Wildcard cert:** `*.adid.dev` covers Portal, API, Wallet OTA channel. Renew via cert-manager + Let's Encrypt (HTTP-01 or DNS-01).
* **HSTS preload:** submit `adid.dev` to the HSTS preload list once you are confident TLS is permanent.
* **mTLS for partner integrations:** if you accept high-volume partner traffic, terminate mTLS at the LB and forward `X-Client-Cert-Subject` to the API.

***