Skip to content

Commit ce3959e

Browse files
committed
config: make server.host and rateLimit.trustForwardedFor required
Two remaining hidden defaults flagged by the explicit-over-implicit principle: - `server.host` was defaulted to `'0.0.0.0'`. That's a security choice (every interface vs loopback-only), not a universal default. Operators should declare it. - `rateLimit.trustForwardedFor` was defaulted to `false`. Behind a reverse proxy this is the difference between per-client and per-proxy rate limiting — wrong answer either way is operationally surprising. Both are now required. Updated all four example configs, inline test YAMLs (schema/parser/validate test fixtures), the server.md reference, and the "applies defaults" test (which no longer asserts host defaulting since there's no default to assert).
1 parent 53c5af4 commit ce3959e

14 files changed

Lines changed: 112 additions & 39 deletions

File tree

CLAUDE.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ Key conventions:
5353
- In config YAML, the `settings` section goes immediately after `version`, before `apis`.
5454
- Runtime config is `config.yaml` + `.env` in the working directory (gitignored).
5555
- **Explicit over implicit**: config fields should be required with no defaults, unless a default is genuinely universal
56-
(e.g. `method: GET`). Only truly optional behavior (like `rateLimit`, `cors`) uses optional fields. When adding new
57-
schema fields, default to required.
56+
(e.g. `method: GET`). Only truly optional behavior (like `cors`, `cache`) uses optional fields. When adding new schema
57+
fields, default to required.
5858

5959
## Architecture
6060

@@ -68,15 +68,16 @@ HTTP service.
6868
Routes:
6969

7070
- `POST /endpoints/{endpointId}` — call an endpoint with parameters in the request body
71-
- `GET /health` — health check with version and airnode address
71+
- `GET /requests/{requestId}` — poll an async request's status (mode: `async` endpoints)
72+
- `GET /health` — status and airnode address (no version exposed)
7273

7374
### Request processing pipeline
7475

7576
The pipeline runs per-request in `src/pipeline.ts`:
7677

7778
1. **Resolve endpoint** → look up endpoint by ID in the endpoint map
7879
2. **Plugin: onHttpRequest** → plugins can reject requests early
79-
3. **Authenticate** → verify client credentials (free access or API key via `X-Api-Key` header)
80+
3. **Authenticate** → verify client credentials (`free`, `apiKey` via `X-Api-Key`, or `x402` payment proof)
8081
4. **Validate parameters** → check that all required parameters are present
8182
5. **Check cache** → return cached response if TTL has not expired
8283
6. **Plugin: onBeforeApiCall** → plugins can modify parameters
@@ -94,14 +95,19 @@ The pipeline runs per-request in `src/pipeline.ts`:
9495

9596
Version `'1.0'`. Top-level sections: `version`, `server`, `settings`, `apis`.
9697

97-
- `server` contains `port`, `host` (default `'0.0.0.0'`), `cors` (optional), `rateLimit` (optional).
98-
- `settings` contains `timeout` (default 10s), `proof` (`'none'` or `{ type: 'reclaim', gatewayUrl }` for TLS proofs),
99-
`fhe` (`'none'` or `{ network, rpcUrl, verifier, apiKey? }` for FHE encryption), `plugins`.
98+
- `server` contains `port`, `host` (default `'0.0.0.0'`), `cors` (optional), `rateLimit` (**required**, with `window`,
99+
`max`, and an `x402` sub-block of `{window, max}`).
100+
- `settings` contains `timeout` (default 10s), `maxConcurrentApiCalls` (**required**), `proof` (`'none'` or
101+
`{ type: 'reclaim', gatewayUrl }` for TLS proofs), `fhe` (`'none'` or `{ network, rpcUrl, verifier, apiKey? }` for FHE
102+
encryption), `plugins`.
100103
- `apis[].url` is the upstream API base URL. Upstream credentials go in `apis[].headers`.
101104
- `apis[].auth` is client-facing: `{ type: 'free' }`, `{ type: 'apiKey', keys: [...] }`, or `{ type: 'x402', ... }`.
102-
- Endpoints use `encoding: { type, path, times? }` instead of `reservedParameters`. Encoding is optional — endpoints
103-
without it return raw JSON with a signature over the JSON hash. Endpoints can also set `encrypt: { type, contract }`
104-
(requires `settings.fhe` and an integer `encoding`) to FHE-encrypt the encoded value before signing.
105+
- Endpoints use `encoding: { type, path, times? }`. When `encoding` is set, `type` and `path` are required — each may be
106+
a concrete value or the literal `'*'` (which delegates that field to the matching reserved request param: `_type`,
107+
`_path`, `_times`). `times` is optional and only valid on numeric types. Without an `encoding` block the response is
108+
raw JSON with a signature over the JSON hash; reserved params can't synthesize encoding from nothing. Endpoints can
109+
also set `encrypt: { type, contract }` (requires `settings.fhe` and a concretely pinned integer `encoding`) to
110+
FHE-encrypt the encoded value before signing.
105111
- Auth and cache config inherit from API level; endpoint-level overrides take precedence.
106112

107113
### Plugin hooks

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ curl -X POST http://localhost:3000/endpoints/{endpointId} \
5656
- **Sign any API response** with your key — turn untrusted data into a verifiable attestation
5757
- **Serve data to smart contracts** — ABI-encoded responses ready for on-chain submission
5858
- **Monetize access** — API keys or pay-per-request via x402
59-
- **Control encoding at request time** — clients pass `_type`, `_path`, `_times` to choose what to extract
59+
- **Opt-in requester-controlled encoding** — endpoints that mark fields as `'*'` let clients supply
60+
`_type`/`_path`/`_times` per request
6061
- **Extend with plugins** — hooks at every pipeline stage for custom logic
6162

6263
## Routes
@@ -65,7 +66,7 @@ curl -X POST http://localhost:3000/endpoints/{endpointId} \
6566
| ------ | ------------------------- | -------------------------------- |
6667
| `POST` | `/endpoints/{endpointId}` | Call an endpoint with parameters |
6768
| `GET` | `/requests/{requestId}` | Poll an async request for status |
68-
| `GET` | `/health` | Version and airnode address |
69+
| `GET` | `/health` | Status and airnode address |
6970

7071
## Configuration
7172

@@ -76,8 +77,15 @@ version: '1.0'
7677

7778
server:
7879
port: 3000
80+
rateLimit:
81+
window: 60000
82+
max: 100
83+
x402:
84+
window: 60000
85+
max: 30
7986

8087
settings:
88+
maxConcurrentApiCalls: 50
8189
proof: none
8290

8391
apis:

book/docs/config/index.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Airnode is configured with a single YAML file. JSON is also accepted. The file h
1212
| ----------------------------------- | ------------------------------------------------------- |
1313
| `version` | Must be `'1.0'`. Used for schema validation. |
1414
| [`server`](/docs/config/server) | HTTP server port, host, CORS, and rate limiting. |
15-
| [`settings`](/docs/config/settings) | Global timeout, proof mode, and plugin configuration. |
15+
| [`settings`](/docs/config/settings) | Timeout, upstream concurrency, proof, FHE, and plugins. |
1616
| [`apis`](/docs/config/apis) | Upstream API definitions with endpoints and parameters. |
1717

1818
## Minimal config
@@ -25,8 +25,12 @@ server:
2525
rateLimit:
2626
window: 60000
2727
max: 100
28+
x402:
29+
window: 60000
30+
max: 30
2831

2932
settings:
33+
maxConcurrentApiCalls: 50
3034
proof: none
3135

3236
apis:
@@ -54,9 +58,13 @@ server:
5458
rateLimit:
5559
window: 60000
5660
max: 100
61+
x402:
62+
window: 60000
63+
max: 30
5764

5865
settings:
5966
timeout: 15000
67+
maxConcurrentApiCalls: 50
6068
proof: none # or: { type: reclaim, gatewayUrl: 'http://localhost:5177/v1/prove' }
6169
plugins:
6270
- source: ../../plugins/heartbeat.ts

book/docs/config/server.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,46 @@ The `server` section configures the HTTP server that receives client requests.
1010
```yaml
1111
server:
1212
port: 3000
13-
host: '0.0.0.0' # default
13+
host: '0.0.0.0'
1414
cors:
1515
origins: ['*'] # default
1616
rateLimit:
1717
window: 60000 # ms
1818
max: 100 # requests per window per IP
19+
trustForwardedFor: false
1920
x402:
2021
window: 60000 # ms
2122
max: 30 # x402 verification attempts per window per IP
2223
```
2324
2425
## Fields
2526
26-
| Field | Type | Required | Default | Description |
27-
| ----------------------------- | ---------- | -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
28-
| `port` | `number` | Yes | -- | TCP port the server listens on. |
29-
| `host` | `string` | No | `'0.0.0.0'` | Bind address. Use `127.0.0.1` to restrict to localhost. |
30-
| `cors` | `object` | No | -- | CORS configuration. When omitted, `Access-Control-Allow-Origin: *` is used. |
31-
| `cors.origins` | `string[]` | No | `['*']` | Allow-list of origins. The request's `Origin` is reflected back only if it matches an entry. |
32-
| `rateLimit` | `object` | **Yes** | -- | Per-IP rate limiting (required). Set `max` very high to effectively disable it. |
33-
| `rateLimit.window` | `number` | Yes | -- | Time window in milliseconds. |
34-
| `rateLimit.max` | `number` | Yes | -- | Maximum requests per IP within the window. |
35-
| `rateLimit.trustForwardedFor` | `boolean` | No | `false` | Use the first `X-Forwarded-For` entry as the client IP. Only enable behind a trusted reverse proxy. |
36-
| `rateLimit.x402` | `object` | **Yes** | -- | Stricter per-IP bucket on x402 verification attempts (each one fires several chain-RPC reads). Shares `trustForwardedFor`. |
37-
| `rateLimit.x402.window` | `number` | Yes | -- | Time window in milliseconds. |
38-
| `rateLimit.x402.max` | `number` | Yes | -- | Maximum x402 verification attempts per IP within the window. |
27+
| Field | Type | Required | Default | Description |
28+
| ----------------------------- | ---------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
29+
| `port` | `number` | **Yes** | -- | TCP port the server listens on. |
30+
| `host` | `string` | **Yes** | -- | Bind address. `'0.0.0.0'` binds every interface; `'127.0.0.1'` restricts to localhost. |
31+
| `cors` | `object` | No | -- | CORS configuration. When omitted, `Access-Control-Allow-Origin: *` is used. |
32+
| `cors.origins` | `string[]` | No | `['*']` | Allow-list of origins. The request's `Origin` is reflected back only if it matches an entry. |
33+
| `rateLimit` | `object` | **Yes** | -- | Per-IP rate limiting. Set `max` very high to effectively disable it. |
34+
| `rateLimit.window` | `number` | **Yes** | -- | Time window in milliseconds. |
35+
| `rateLimit.max` | `number` | **Yes** | -- | Maximum requests per IP within the window. |
36+
| `rateLimit.trustForwardedFor` | `boolean` | **Yes** | -- | Use the first `X-Forwarded-For` entry as the client IP. Set `true` only behind a trusted reverse proxy; otherwise `false`. |
37+
| `rateLimit.x402` | `object` | **Yes** | -- | Stricter per-IP bucket on x402 verification attempts (each one fires several chain-RPC reads). Shares `trustForwardedFor`. |
38+
| `rateLimit.x402.window` | `number` | **Yes** | -- | Time window in milliseconds. |
39+
| `rateLimit.x402.max` | `number` | **Yes** | -- | Maximum x402 verification attempts per IP within the window. |
3940

4041
## Minimal
4142

42-
`port` and `rateLimit` (including the `x402` sub-block) are required:
43+
`port`, `host`, and `rateLimit` (including `trustForwardedFor` and the `x402` sub-block) are required:
4344

4445
```yaml
4546
server:
4647
port: 3000
48+
host: '0.0.0.0'
4749
rateLimit:
4850
window: 60000
4951
max: 100
52+
trustForwardedFor: false
5053
x402:
5154
window: 60000
5255
max: 30
@@ -63,9 +66,11 @@ resetting at fixed intervals.
6366
```yaml
6467
server:
6568
port: 3000
69+
host: '0.0.0.0'
6670
rateLimit:
6771
window: 60000 # 60 seconds
6872
max: 100 # 100 requests per 60s per IP
73+
trustForwardedFor: false
6974
x402:
7075
window: 60000
7176
max: 30 # 30 x402 verification attempts per 60s per IP
@@ -75,10 +80,11 @@ When a client exceeds the limit, the server returns `429 Too Many Requests`.
7580

7681
The rate limiter tracks up to 10,000 unique IPs. When this limit is reached, the oldest entries are evicted.
7782

78-
By default the client IP is the socket peer. If Airnode runs behind a reverse proxy that is the proxy's address, so
79-
every client would share one bucket — set `rateLimit.trustForwardedFor: true` to use the first `X-Forwarded-For` entry
80-
instead. Only enable this when a trusted proxy controls that header; a client-supplied `X-Forwarded-For` is otherwise
81-
trivially spoofable.
83+
The client IP defaults to the socket peer when `trustForwardedFor` is `false`. If Airnode runs behind a reverse proxy
84+
the peer is the proxy itself, so every client would share one bucket — set `trustForwardedFor: true` to use the first
85+
`X-Forwarded-For` entry instead. Only enable this when a trusted proxy controls that header; a client-supplied
86+
`X-Forwarded-For` is otherwise trivially spoofable, which would let any client bypass the rate limit by picking a fresh
87+
IP per request.
8288

8389
### x402 verification bucket
8490

book/docs/introduction.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,12 @@ server:
122122
rateLimit:
123123
window: 60000
124124
max: 100
125+
x402:
126+
window: 60000
127+
max: 30
125128

126129
settings:
130+
maxConcurrentApiCalls: 50
127131
proof: none
128132

129133
apis:

book/docs/operators/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ server:
3737
rateLimit:
3838
window: 60000
3939
max: 100
40+
x402:
41+
window: 60000
42+
max: 30
4043

4144
settings:
45+
maxConcurrentApiCalls: 50
4246
proof: none
4347

4448
apis:

examples/configs/complete/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ server:
99
rateLimit:
1010
window: 60000
1111
max: 100
12+
trustForwardedFor: false
1213
x402:
1314
window: 60000
1415
max: 30

examples/configs/fhe-encrypt/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ version: '1.0'
22

33
server:
44
port: 3000
5+
host: '0.0.0.0'
56
rateLimit:
67
window: 60000
78
max: 100
9+
trustForwardedFor: false
810
x402:
911
window: 60000
1012
max: 30

examples/configs/minimal/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ version: '1.0'
22

33
server:
44
port: 3000
5+
host: '0.0.0.0'
56
rateLimit:
67
window: 60000
78
max: 100
9+
trustForwardedFor: false
810
x402:
911
window: 60000
1012
max: 30

examples/configs/reclaim-proof/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ version: '1.0'
22

33
server:
44
port: 3000
5+
host: '0.0.0.0'
56
rateLimit:
67
window: 60000
78
max: 100
9+
trustForwardedFor: false
810
x402:
911
window: 60000
1012
max: 30

0 commit comments

Comments
 (0)