A Cloudflare Worker that authenticates GitHub Actions OIDC tokens and proxies requests to an upstream API with an injected API key. This lets CI workflows call APIs without storing long-lived secrets in GitHub.
## How it works
```
GitHub Actions (OIDC token) → Worker (validate JWT, inject API key) → Upstream API
```
1. A GitHub Actions workflow mints an OIDC token with a configured audience
2. The workflow sends requests to this proxy, passing the OIDC token as the API key
3. The worker validates the JWT against GitHub's JWKS, checks issuer/audience/age/repo
4. If valid, the request is forwarded to the upstream API with the real API key injected
| `MAX_TOKEN_AGE_SECONDS` | Operator-configured upper bound on `iat` age in seconds (default: `1200` = 20 min). Applied **in addition to** the IdP's `exp` claim, never as a replacement. |
Each OIDC token is tracked by its `jti` (JWT ID) claim using a Durable Object. This provides:
- **Budget**: Each token is limited to `MAX_REQUESTS_PER_TOKEN` total requests (default: 200). Once exhausted, the proxy returns `429` with `{"error": "Token budget exhausted"}`.
- **Rate limit**: Each token is limited to `RATE_LIMIT_PER_SECOND` requests per second (default: 2). When exceeded, the proxy returns `429` with `{"error": "Rate limit exceeded"}` and a `Retry-After: 1` header.
Both limits are enforced atomically — the Durable Object processes one request at a time per token, so there are no race conditions.
The proxy enforces **both** gates and a token must pass each:
1. The IdP's `exp` claim (always enforced).
2. The operator's `MAX_TOKEN_AGE_SECONDS` cap on `iat`, when configured (default `1200`s = 20 min).
`MAX_TOKEN_AGE_SECONDS` is a stricter upper bound *on top of*`exp` — it cannot extend a token past its `exp`. For workflows longer than the IdP's token lifetime (GitHub OIDC issues `exp = iat + 300` ≈ 5 min), refresh the OIDC token rather than relying on `MAX_TOKEN_AGE_SECONDS` to accept expired tokens.