Skip to content

10.1 Gateway Security

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

Gateway Security

Relevant source files

The following files were used as context for generating this wiki page:

This document covers the security architecture of the HTTP gateway, including authentication mechanisms, rate limiting, network isolation, and webhook signature verification. The gateway implements defense-in-depth with multiple independent security layers to protect against unauthorized access, brute-force attacks, and various abuse vectors.

For general security architecture across the entire system (including tool execution, file access, and autonomy controls), see Security Model. For channel-specific security (allowlists, pairing codes), see Channel Security.


Overview

The gateway (src/gateway/mod.rs) provides HTTP endpoints for external integrations. Security is enforced through five independent layers:

Layer Mechanism Purpose
Network Localhost binding, tunnel requirement Prevent public exposure
Authentication Pairing + bearer tokens Identify legitimate clients
Rate Limiting Sliding window per IP Prevent brute-force and DoS
Webhook Auth SHA-256 secret validation Verify webhook sources
Request Security Body limits, timeouts, idempotency Prevent resource exhaustion

Each layer operates independently — even if one is bypassed, the others provide redundant protection.

Sources: src/gateway/mod.rs:1-505


Architecture Diagram

graph TB
    subgraph "External Clients"
        HTTP["HTTP Client"]
        WA["WhatsApp Platform"]
        WebhookSender["Webhook Sender"]
    end
    
    subgraph "Gateway Layers src/gateway/mod.rs"
        Bind["Bind Check<br/>is_public_bind()"]
        Tunnel["Tunnel Requirement<br/>create_tunnel()"]
        BodyLimit["RequestBodyLimitLayer<br/>MAX_BODY_SIZE=64KB"]
        Timeout["TimeoutLayer<br/>REQUEST_TIMEOUT_SECS=30"]
        RateLimit["GatewayRateLimiter<br/>SlidingWindowRateLimiter"]
        Pairing["PairingGuard<br/>src/security/pairing.rs"]
        WebhookSecret["hash_webhook_secret()<br/>SHA-256 + constant_time_eq()"]
        WhatsAppSig["verify_whatsapp_signature()<br/>HMAC-SHA256"]
        Idempotency["IdempotencyStore<br/>TTL + max_keys"]
    end
    
    subgraph "Handlers"
        HandlePair["handle_pair()<br/>/pair"]
        HandleWebhook["handle_webhook()<br/>/webhook"]
        HandleWhatsAppVerify["handle_whatsapp_verify()<br/>/whatsapp GET"]
        HandleWhatsAppMsg["handle_whatsapp_message()<br/>/whatsapp POST"]
        HandleHealth["handle_health()<br/>/health"]
    end
    
    subgraph "Backend"
        Provider["Provider trait"]
        Memory["Memory trait"]
        Observer["Observer trait"]
    end
    
    HTTP --> Bind
    WebhookSender --> Bind
    WA --> Bind
    
    Bind --> Tunnel
    Tunnel --> BodyLimit
    BodyLimit --> Timeout
    Timeout --> RateLimit
    
    RateLimit -->|/pair| HandlePair
    RateLimit -->|/webhook| HandleWebhook
    RateLimit -->|/whatsapp| HandleWhatsAppVerify
    RateLimit -->|/whatsapp| HandleWhatsAppMsg
    RateLimit -->|/health| HandleHealth
    
    HandlePair --> Pairing
    HandleWebhook --> Pairing
    HandleWebhook --> WebhookSecret
    HandleWebhook --> Idempotency
    HandleWhatsAppMsg --> WhatsAppSig
    
    Pairing --> Provider
    WebhookSecret --> Provider
    WhatsAppSig --> Provider
    
    Provider --> Memory
    Provider --> Observer
Loading

Sources: src/gateway/mod.rs:282-505, src/security/pairing.rs:1-485


Network Security

Localhost-Only Binding

The gateway refuses to bind to public addresses unless explicitly configured. This prevents accidental exposure to the internet.

flowchart TD
    Start["run_gateway(host, port)"] --> CheckBind{"is_public_bind(host)?"}
    CheckBind -->|"No (127.0.0.1, localhost)"| BindOK["Proceed with bind"]
    CheckBind -->|"Yes (0.0.0.0, public IP)"| CheckTunnel{"config.tunnel.provider != 'none'?"}
    CheckTunnel -->|Yes| BindOK
    CheckTunnel -->|No| CheckOverride{"config.gateway.allow_public_bind?"}
    CheckOverride -->|Yes| WarnAndBind["⚠️ Warn + Proceed"]
    CheckOverride -->|No| Reject["🛑 Refuse to start<br/>Return error"]
    
    BindOK --> CreateListener["TcpListener::bind(addr)"]
    WarnAndBind --> CreateListener
Loading

Implementation: src/gateway/mod.rs:284-292

The is_public_bind() function checks if the host is 127.0.0.1, localhost, ::1, or [::1]. All other addresses are considered public.

Sources: src/security/pairing.rs:224-231

Tunnel Integration

When a tunnel is configured (Tunnel documentation), the gateway automatically starts it and prints the public URL. This allows external access while keeping the actual bind address on localhost.

Supported tunnel providers:

  • tailscale - Zero-config VPN tunnel
  • cloudflare - Cloudflare Tunnel (requires cloudflared)
  • ngrok - ngrok tunnel (requires auth token)
  • none - No tunnel (local-only)

Sources: src/gateway/mod.rs:415-431


Pairing Authentication

Overview

Pairing is a first-connect authentication mechanism that prevents unauthorized access. On startup, if no paired tokens exist, the gateway generates a 6-digit one-time code and prints it to the terminal. Clients must present this code via X-Pairing-Code header on POST /pair to receive a bearer token.

sequenceDiagram
    participant GW as Gateway Startup
    participant PG as PairingGuard
    participant Term as Terminal
    participant Client as HTTP Client
    participant Config as config.toml
    
    GW->>PG: new(require_pairing=true, existing_tokens=[])
    PG->>PG: generate_code()<br/>(6 digits, CSPRNG)
    PG-->>GW: pairing_code = "123456"
    GW->>Term: Print pairing code
    
    Note over Client: User sees code: 123456
    
    Client->>GW: POST /pair<br/>X-Pairing-Code: 123456
    GW->>PG: try_pair("123456")
    PG->>PG: constant_time_eq(code, expected)
    PG->>PG: generate_token()<br/>(32 bytes random → zc_...)
    PG->>PG: hash_token(plaintext)<br/>(SHA-256 → 64 hex)
    PG->>PG: Store hash in paired_tokens
    PG-->>GW: Ok(Some("zc_abc123..."))
    GW->>Client: 200 OK<br/>{"token": "zc_abc123..."}
    
    GW->>Config: persist paired_tokens<br/>(save hash only)
    
    Note over Client: Client saves token
    
    Client->>GW: POST /webhook<br/>Authorization: Bearer zc_abc123...
    GW->>PG: is_authenticated("zc_abc123...")
    PG->>PG: hash_token(input)<br/>Compare against stored hash
    PG-->>GW: true
    GW->>Client: 200 OK (request processed)
Loading

Sources: src/security/pairing.rs:38-151, src/gateway/mod.rs:392-612

Token Generation

Bearer tokens are generated using cryptographically secure randomness:

  1. Entropy Source: rand::rng() backed by OS CSPRNG (/dev/urandom on Linux, BCryptGenRandom on Windows)
  2. Format: zc_<64 hex chars> (32 random bytes → 256 bits of entropy)
  3. Storage: SHA-256 hash of the token (prevents plaintext exposure in config)

Implementation: src/security/pairing.rs:177-193

Pairing Code Generation

The 6-digit code uses rejection sampling to eliminate modulo bias:

const UPPER_BOUND: u32 = 1_000_000;
const REJECT_THRESHOLD: u32 = (u32::MAX / UPPER_BOUND) * UPPER_BOUND;

loop {
    let uuid = uuid::Uuid::new_v4();  // Uses getrandom (CSPRNG)
    let bytes = uuid.as_bytes();
    let raw = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
    
    if raw < REJECT_THRESHOLD {
        return format!("{:06}", raw % UPPER_BOUND);
    }
    // Rejection probability: ~0.02%
}

Sources: src/security/pairing.rs:153-175

Brute-Force Protection

The PairingGuard tracks failed pairing attempts and enforces a lockout after MAX_PAIR_ATTEMPTS (5) failures:

State Behavior
0-4 failed attempts Allow pairing attempts
5+ failed attempts Lock out for PAIR_LOCKOUT_SECS (300s = 5 min)
Successful pairing Reset failed attempt counter

Implementation: src/security/pairing.rs:83-128

stateDiagram-v2
    [*] --> Ready: require_pairing=true<br/>no tokens
    Ready --> AttemptingPair: POST /pair
    AttemptingPair --> Success: Code matches
    AttemptingPair --> Failed: Code mismatch
    Success --> Paired: Return bearer token
    Failed --> Ready: attempts < 5
    Failed --> LockedOut: attempts >= 5
    LockedOut --> Ready: After 5 minutes
    Paired --> [*]: Token persisted
Loading

Sources: src/security/pairing.rs:15-36

Constant-Time Comparison

To prevent timing attacks that could leak code/token information, all string comparisons use constant-time equality:

pub fn constant_time_eq(a: &str, b: &str) -> bool {
    let a = a.as_bytes();
    let b = b.as_bytes();
    
    let len_diff = a.len() ^ b.len();
    let max_len = a.len().max(b.len());
    let mut byte_diff = 0u8;
    
    // Always iterate over max length, padding shorter with zeros
    for i in 0..max_len {
        let x = *a.get(i).unwrap_or(&0);
        let y = *b.get(i).unwrap_or(&0);
        byte_diff |= x ^ y;
    }
    
    (len_diff == 0) & (byte_diff == 0)
}

This ensures comparison time is independent of where strings differ, preventing attackers from inferring correct prefixes via timing measurements.

Sources: src/security/pairing.rs:201-222


Rate Limiting

Architecture

The gateway implements sliding-window rate limiting using GatewayRateLimiter, which wraps two independent SlidingWindowRateLimiter instances:

  • Pair endpoint: pair_rate_limit_per_minute (default: unlimited)
  • Webhook endpoint: webhook_rate_limit_per_minute (default: unlimited)
classDiagram
    class GatewayRateLimiter {
        +pair: SlidingWindowRateLimiter
        +webhook: SlidingWindowRateLimiter
        +new(pair_per_min, webhook_per_min, max_keys)
        +allow_pair(key) bool
        +allow_webhook(key) bool
    }
    
    class SlidingWindowRateLimiter {
        -limit_per_window: u32
        -window: Duration
        -max_keys: usize
        -requests: Mutex~HashMap~
        +new(limit, window, max_keys)
        +allow(key) bool
        -prune_stale(requests, cutoff)
    }
    
    class ClientKeyExtraction {
        +client_key_from_request(peer_addr, headers, trust_forwarded)
        +forwarded_client_ip(headers) Option~IpAddr~
        +parse_client_ip(value) Option~IpAddr~
    }
    
    GatewayRateLimiter --> SlidingWindowRateLimiter : uses
    GatewayRateLimiter --> ClientKeyExtraction : uses
Loading

Sources: src/gateway/mod.rs:136-158

Client Key Extraction

Rate limits are enforced per-client, identified by IP address:

Priority (if trust_forwarded_headers = true):

  1. First IP in X-Forwarded-For header
  2. X-Real-IP header
  3. Socket peer address

Priority (if trust_forwarded_headers = false):

  1. Socket peer address only

Implementation: src/gateway/mod.rs:235-249

⚠️ Security Note: Only enable trust_forwarded_headers when the gateway is behind a trusted reverse proxy. Otherwise, clients can spoof headers to bypass rate limits.

Sliding Window Algorithm

For each client key, the limiter maintains a list of request timestamps:

flowchart LR
    Request["New Request<br/>at time T"] --> GetKey["client_key_from_request()"]
    GetKey --> Cutoff["cutoff = T - window"]
    Cutoff --> Prune["Prune timestamps < cutoff"]
    Prune --> CheckCount{"timestamps.len() >= limit?"}
    CheckCount -->|Yes| Reject["Return 429 Too Many Requests"]
    CheckCount -->|No| Allow["Add T to timestamps<br/>Return 200 OK"]
Loading

Periodic Cleanup: Every RATE_LIMITER_SWEEP_INTERVAL_SECS (300s), the limiter removes client keys with no recent requests to prevent unbounded memory growth.

Cardinality Protection: If the map reaches max_keys, the limiter evicts the least-recently-used client before adding a new one.

Sources: src/gateway/mod.rs:66-134

Configuration

[gateway]
# Rate limits per minute (0 = unlimited)
pair_rate_limit_per_minute = 10
webhook_rate_limit_per_minute = 60

# Maximum distinct client IPs tracked
rate_limit_max_keys = 10000

# Trust X-Forwarded-For headers (only enable behind trusted proxy)
trust_forwarded_headers = false

Sources: src/gateway/mod.rs:397-405


Webhook Secret Authentication

In addition to pairing authentication, the /webhook endpoint supports an optional second authentication layer via X-Webhook-Secret header. This is useful for webhook sources that cannot send Authorization: Bearer headers.

Flow

sequenceDiagram
    participant WS as Webhook Source
    participant GW as Gateway
    participant Hash as hash_webhook_secret()
    participant Compare as constant_time_eq()
    
    Note over GW: Startup: config.channels_config.webhook.secret
    GW->>Hash: hash_webhook_secret(plaintext)
    Hash->>Hash: SHA-256 digest
    Hash-->>GW: Store hex hash in webhook_secret_hash
    
    WS->>GW: POST /webhook<br/>Authorization: Bearer ...<br/>X-Webhook-Secret: raw_secret
    
    GW->>GW: Check pairing (bearer token)
    
    alt webhook_secret_hash is Some
        GW->>Hash: hash_webhook_secret(header_value)
        Hash-->>GW: header_hash
        GW->>Compare: constant_time_eq(header_hash, stored_hash)
        Compare-->>GW: true/false
        alt Mismatch
            GW->>WS: 401 Unauthorized<br/>"invalid or missing X-Webhook-Secret"
        end
    end
    
    GW->>WS: 200 OK (process webhook)
Loading

Sources: src/gateway/mod.rs:352-360, src/gateway/mod.rs:655-670

Storage

The plaintext secret is never stored. Only the SHA-256 hash is retained:

let webhook_secret_hash: Option<Arc<str>> = 
    config.channels_config.webhook.as_ref()
        .and_then(|webhook| {
            webhook.secret.as_ref()
                .and_then(|raw_secret| {
                    let trimmed = raw_secret.trim();
                    (!trimmed.is_empty()).then(|| 
                        Arc::<str>::from(hash_webhook_secret(trimmed))
                    )
                })
        });

Sources: src/gateway/mod.rs:56-61


WhatsApp Signature Verification

The /whatsapp webhook endpoint verifies incoming requests using HMAC-SHA256 signature validation, as required by Meta's Webhooks API.

Signature Format

Meta sends the signature in the X-Hub-Signature-256 header:

X-Hub-Signature-256: sha256=<hex_hmac_signature>

Verification Process

flowchart TD
    Request["POST /whatsapp<br/>X-Hub-Signature-256: sha256=..."] --> Extract["Extract hex signature<br/>from header"]
    Extract --> Decode["hex::decode(signature)"]
    Decode --> ComputeHMAC["HMAC-SHA256(body, app_secret)"]
    ComputeHMAC --> Compare["mac.verify_slice(expected)<br/>(constant-time)"]
    Compare -->|"Ok()"| Accept["Process webhook"]
    Compare -->|"Err()"| Reject["Return empty 200<br/>(Meta retries on non-200)"]
Loading

Implementation:

pub fn verify_whatsapp_signature(app_secret: &str, body: &[u8], signature_header: &str) -> bool {
    use hmac::{Hmac, Mac};
    use sha2::Sha256;
    
    // Extract hex signature from "sha256=<hex>"
    let Some(hex_sig) = signature_header.strip_prefix("sha256=") else {
        return false;
    };
    
    let Ok(expected) = hex::decode(hex_sig) else {
        return false;
    };
    
    // Compute HMAC-SHA256
    let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(app_secret.as_bytes()) else {
        return false;
    };
    mac.update(body);
    
    // Constant-time comparison (prevents timing attacks)
    mac.verify_slice(&expected).is_ok()
}

Sources: src/gateway/mod.rs:843-868

App Secret Configuration

The WhatsApp app secret can be provided via:

Priority:

  1. Environment variable: ZEROCLAW_WHATSAPP_APP_SECRET
  2. Config file: [channels_config.whatsapp] app_secret = "..."

Sources: src/gateway/mod.rs:373-390

Security Note

The signature is computed over the raw request body. The handler receives body: Bytes directly (not parsed JSON) to ensure the HMAC input matches exactly what Meta signed.

Sources: src/gateway/mod.rs:870-918


Idempotency Protection

The gateway provides optional idempotency for /webhook requests via the X-Idempotency-Key header. This prevents duplicate processing if a webhook sender retries the same request.

Store Design

classDiagram
    class IdempotencyStore {
        -ttl: Duration
        -max_keys: usize
        -keys: Mutex~HashMap~String, Instant~~
        +new(ttl, max_keys)
        +record_if_new(key) bool
    }
    
    class CleanupStrategy {
        +Retain: keys.retain(expired)
        +Eviction: Remove LRU when full
    }
    
    IdempotencyStore --> CleanupStrategy
Loading

Behavior:

  • First request: record_if_new(key) returns true → process normally
  • Duplicate request: record_if_new(key) returns false → return 200 with {"status": "duplicate"}

Expiration: Keys older than ttl are removed automatically on each call.

Cardinality: If max_keys is reached, the least-recently-seen key is evicted before adding a new one.

Sources: src/gateway/mod.rs:160-200

Configuration

[gateway]
# Idempotency key TTL in seconds (default: 3600 = 1 hour)
idempotency_ttl_secs = 3600

# Maximum distinct idempotency keys retained
idempotency_max_keys = 10000

Sources: src/gateway/mod.rs:406-413

Usage Example

# First request
curl -X POST http://localhost:3000/webhook \
  -H "Authorization: Bearer zc_abc..." \
  -H "X-Idempotency-Key: req-12345" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'
# Response: 200 OK, {"response": "...", "model": "..."}

# Duplicate request (within TTL)
curl -X POST http://localhost:3000/webhook \
  -H "Authorization: Bearer zc_abc..." \
  -H "X-Idempotency-Key: req-12345" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'
# Response: 200 OK, {"status": "duplicate", "idempotent": true, ...}

Sources: src/gateway/mod.rs:684-700


Request Security

Body Size Limit

The gateway enforces a 64KB maximum request body size to prevent memory exhaustion attacks:

pub const MAX_BODY_SIZE: usize = 65_536;

let app = Router::new()
    // ...
    .layer(RequestBodyLimitLayer::new(MAX_BODY_SIZE));

Requests exceeding this limit receive a 413 Payload Too Large response.

Sources: src/gateway/mod.rs:37-38, src/gateway/mod.rs:491

Request Timeout

All requests must complete within 30 seconds to prevent slow-loris attacks:

pub const REQUEST_TIMEOUT_SECS: u64 = 30;

let app = Router::new()
    // ...
    .layer(TimeoutLayer::with_status_code(
        StatusCode::REQUEST_TIMEOUT,
        Duration::from_secs(REQUEST_TIMEOUT_SECS),
    ));

Timed-out requests receive a 408 Request Timeout response.

Sources: src/gateway/mod.rs:39-40, src/gateway/mod.rs:492-495

HTTP/1.1 Compliance

The gateway uses axum (built on hyper) for proper HTTP/1.1 parsing, replacing a previous raw TCP implementation. This provides:

  • Automatic Content-Length validation
  • Header sanitization (via hyper's strict parsing)
  • Protection against HTTP smuggling attacks
  • Standards-compliant request/response handling

Sources: src/gateway/mod.rs:1-9


Endpoint Security Matrix

Endpoint Pairing Rate Limit Webhook Secret WhatsApp Sig Idempotency
GET /health ❌ Public ❌ None ❌ N/A ❌ N/A ❌ N/A
GET /metrics ❌ Public ❌ None ❌ N/A ❌ N/A ❌ N/A
POST /pair ⚠️ Code-based ✅ Yes ❌ N/A ❌ N/A ❌ N/A
POST /webhook ✅ Required ✅ Yes ✅ Optional ❌ N/A ✅ Optional
GET /whatsapp ❌ Token-based ❌ None ❌ N/A ❌ N/A ❌ N/A
POST /whatsapp ❌ N/A ❌ None ❌ N/A ✅ Required ❌ N/A

Notes:

  • GET /health and GET /metrics are intentionally public for monitoring
  • GET /whatsapp uses Meta's verify_token (not pairing)
  • POST /whatsapp requires signature verification instead of pairing
  • POST /webhook supports both pairing and webhook secret (layered defense)

Sources: src/gateway/mod.rs:483-490


Configuration Reference

Complete Security Settings

[gateway]
# ── Network Security ──
host = "127.0.0.1"
port = 3000
allow_public_bind = false  # Refuse public bind without tunnel

# ── Pairing Authentication ──
require_pairing = true
paired_tokens = [
    "a1b2c3d4e5f6...",  # SHA-256 hashes (64 hex chars)
]

# ── Rate Limiting ──
pair_rate_limit_per_minute = 10
webhook_rate_limit_per_minute = 60
rate_limit_max_keys = 10000
trust_forwarded_headers = false  # Only enable behind trusted proxy

# ── Idempotency ──
idempotency_ttl_secs = 3600
idempotency_max_keys = 10000

[tunnel]
provider = "tailscale"  # Options: tailscale, cloudflare, ngrok, none

[channels_config.webhook]
secret = "your-webhook-secret"  # Optional second auth layer

[channels_config.whatsapp]
app_secret = "meta-app-secret"  # Or use ZEROCLAW_WHATSAPP_APP_SECRET env var
verify_token = "meta-verify-token"
access_token = "whatsapp-access-token"
phone_number_id = "123456789"
allowed_numbers = ["+1234567890"]

Sources: src/gateway/mod.rs:282-480


Attack Mitigation Summary

Attack Vector Mitigation Implementation
Unauthorized Access Pairing + bearer tokens PairingGuard, SHA-256 token hashing
Brute-Force Pairing Rate limiting + lockout 5 attempts → 5 min lockout
Credential Theft Token hashing Only SHA-256 hashes persisted
Timing Attacks Constant-time comparison constant_time_eq() for all secrets
DoS via Rate Sliding-window rate limiter Per-IP limits with cardinality caps
Memory Exhaustion Body size limits 64KB max per request
Slow-Loris Request timeouts 30s timeout per request
Replay Attacks Idempotency keys TTL-based duplicate detection
Webhook Forgery HMAC signatures verify_whatsapp_signature()
Public Exposure Network isolation Localhost-only + tunnel requirement
IP Spoofing Header trust control trust_forwarded_headers flag

Sources: src/gateway/mod.rs:1-918, src/security/pairing.rs:1-485


Code Entity Map

Key Components

graph LR
    subgraph "src/gateway/mod.rs"
        run_gateway["run_gateway(host, port, config)"]
        AppState["AppState struct"]
        handle_pair["handle_pair()"]
        handle_webhook["handle_webhook()"]
        handle_whatsapp_message["handle_whatsapp_message()"]
        GatewayRateLimiter["GatewayRateLimiter struct"]
        IdempotencyStore["IdempotencyStore struct"]
        verify_whatsapp_signature["verify_whatsapp_signature()"]
        hash_webhook_secret["hash_webhook_secret()"]
        client_key_from_request["client_key_from_request()"]
    end
    
    subgraph "src/security/pairing.rs"
        PairingGuard["PairingGuard struct"]
        try_pair["try_pair()"]
        is_authenticated["is_authenticated()"]
        generate_code["generate_code()"]
        generate_token["generate_token()"]
        constant_time_eq["constant_time_eq()"]
        is_public_bind["is_public_bind()"]
    end
    
    subgraph "Axum Middleware"
        RequestBodyLimitLayer["RequestBodyLimitLayer"]
        TimeoutLayer["TimeoutLayer"]
    end
    
    run_gateway --> AppState
    run_gateway --> PairingGuard
    run_gateway --> GatewayRateLimiter
    run_gateway --> IdempotencyStore
    
    handle_pair --> PairingGuard
    handle_pair --> try_pair
    handle_pair --> GatewayRateLimiter
    
    handle_webhook --> PairingGuard
    handle_webhook --> is_authenticated
    handle_webhook --> hash_webhook_secret
    handle_webhook --> IdempotencyStore
    handle_webhook --> GatewayRateLimiter
    
    handle_whatsapp_message --> verify_whatsapp_signature
    
    PairingGuard --> generate_code
    PairingGuard --> generate_token
    PairingGuard --> constant_time_eq
    
    GatewayRateLimiter --> client_key_from_request
    
    run_gateway --> RequestBodyLimitLayer
    run_gateway --> TimeoutLayer
Loading

Sources: src/gateway/mod.rs:282-918, src/security/pairing.rs:38-231


Clone this wiki locally