# Configurations cibles des prochaines versions

> Cette page montre, pour chaque cas d'usage prévu, la forme cible d'un flexway.yaml complet et l'extrait de contrat qui va avec, et la version qui doit le…

Guide - Depuis la version 1.6.0 - Flexway v1.6.3 - https://flexway.io/docs/operations-planned-configurations/

> Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : [guide produit](https://flexway.io/docs/product-overview/) ; l'appeler : [guide développeur](https://flexway.io/docs/developer-overview/).

Cette page montre, pour chaque cas d'usage **prévu**, la forme cible d'un `flexway.yaml` complet et
l'extrait de contrat qui va avec, et la version qui doit le livrer : `1.7.0` (appareils et industrie) et
`1.8.0` (besoins des entreprises). **Rien de ce qui suit n'est construit** : un détail peut encore bouger
d'ici la version, et cette page suivra. Là où la forme d'une clé n'est pas encore fixée, l'exemple le dit
au lieu de l'inventer. Chaque exemple deviendra un fichier de `examples/`, validé comme les autres, avec
sa version.

> **Déjà livré en `1.6.0`**, et donc décrit ailleurs : le proxy Kafka natif
> ([Clients Kafka natifs : clusters virtuels par tenant](https://flexway.io/docs/operations-kafka-clients/)), le chiffrement par
> champ ([Chiffrement par champ](https://flexway.io/docs/operations-kafka-encryption/)), les charges Protobuf, Avro et CBOR sous
> schéma et le registre de schémas
> ([Charges Avro sous schéma, registre de schémas](https://flexway.io/docs/operations-examples-brokers/#charges-avro-sous-sch%C3%A9ma-registre-de-sch%C3%A9mas)),
> NATS et AMQP 1.0 ([Exemples : Kafka, RabbitMQ, MQTT, Avro, NATS, AMQP 1.0](https://flexway.io/docs/operations-examples-brokers/#nats-jetstream--publier-depuis-http-ou-un-outil-mcp)).

**Ce que fait la passerelle d'aujourd'hui** de ces formes (vérifié avec `flexway validate`) :

| L'exemple déclare | Aujourd'hui |
| --- | --- |
| Une clé nouvelle de `flexway.yaml` : `listeners.mqtt`, `plans`, `webhooks`, `server.ip_deny`, `server.proxy_protocol`, `ai.budget.per_subject`, `ai.use_cases`, `ai.aliases`, `plan_claim`, `use_case_claim`, `trusted_clients`, `auth.mtls.clients[].plan` | **Refusée au chargement** (`field ... not found`) : toute la configuration est refusée |
| Une liaison `x-flexway-bind: "cert:<champ>"`, un schéma de webhook lu dans un autre en-tête qu'`Authorization` | **Refusé au chargement** |
| Un type de politique à venir : `compress`, `ip-filter` | **Accepté, jamais appliqué** : `flexway validate` et le démarrage l'avertissent (`a later version - NOT applied`) |
| Une extension de contrat à venir : `x-flexway-device`, `x-flexway-sparkplug`, `x-flexway-use-case`, `x-flexway-webhook`, `x-flexway-coalesce` | **Refusée au chargement** (`not served yet - planned for <version>`) : tout le contrat est refusé, comme toute extension `x-flexway-*` inconnue (`unknown extension`, avec la plus proche si elle est évidente) ou écrite là où elle n'est pas lue (`not read here`) - [Pas encore construit](https://flexway.io/docs/operations-examples-gateway/#pas-encore-construit) |

## Appareils MQTT : X.509 ou jeton, topic lié au tenant et au certificat

> [!NOTE]
> Prévu en 1.7

> **Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`listeners.mqtt`, `x-flexway-bind: "cert:cn"`, `x-flexway-device`).

La passerelle est un proxy MQTT devant le courtier du client : une connexion amont par appareil,
chaque paquet jugé par la même décision qu'une requête HTTP. Sessions, retenus, testaments et QoS 2
restent au courtier.

```yaml
# flexway.yaml - l'écouteur ; aucun topic ici, ils viennent du contrat
version: 1
contracts:
  asyncapi: api/asyncapi.yaml
listeners:
  mqtt:
    address: ":8883"
    tls: { cert_file: /tls/tls.crt, key_file: /tls/tls.key, client_ca_file: /tls/ca.crt }   # X.509 des appareils
    websocket: { path: /mqtt }
    max_packet_bytes: 262144
    connect_timeout: 5s                # un CONNECT lent est fermé
upstreams:
  plant-broker:
    protocol: mqtt
    brokers: ["mqtts://broker.plant.internal:8883"]
    protocol_version: 5
auth:
  issuers:                             # pour un appareil sans certificat : un jeton en mot de passe
    - name: devices
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: plant-devices
      tenant_claim: site
```

```yaml
# api/asyncapi.yaml - la convention des écouteurs, celle du proxy Kafka natif (1.6.0) :
# l'extension sur l'opération, send = l'appareil publie, receive = il s'abonne
channels:
  telemetry:
    address: "plant/{site}/line/{line}/{device}/telemetry"
    parameters:
      site:   { x-flexway-bind: tenant }      # le tenant vérifié, jamais celui du topic
      device: { x-flexway-bind: "cert:cn" }   # le nom commun du certificat de l'appareil
    messages:
      reading:
        contentType: application/x-protobuf
        payload: { schemaFormat: "application/vnd.google.protobuf;version=3", schema: { $ref: "./reading.binpb#Reading" } }
operations:
  publishTelemetry:
    action: send                       # l'appareil publie : écrit dans le canal
    channel: { $ref: "#/channels/telemetry" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]
```

| Paquet | Décision |
| --- | --- |
| `CONNECT` | Certificat vérifié, sinon mot de passe (jeton vérifié ou clé), sinon refus `0x87` ; client id **réécrit, lié à l'identité** - un appareil n'en évince jamais un autre ; testament jugé comme un `PUBLISH` |
| `PUBLISH` | Topic -> canal déclaré, paramètres liés **égaux** aux valeurs vérifiées (`site` = tenant, `device` = CN), portée, quota, schéma si `validate: full` ; refus `PUBACK` `0x87`, `0x97` (quota), `0x99` (format) en MQTT 5, fermeture en 3.1.1 |
| `SUBSCRIBE` | Chaque filtre, jokers compris, doit tenir dans un canal déclaré une fois les paramètres liés posés ; sinon `SUBACK 0x87` |
| `exp` du jeton | `DISCONNECT 0xA0` (MQTT 5) ; l'appareil revient avec un jeton neuf |

## Sparkplug B

> [!NOTE]
> Prévu en 1.7

> **Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`listeners.mqtt`, `x-flexway-bind: "cert:cn"`, `x-flexway-sparkplug`).

`flexway.yaml` est celui de 12.29.6. Le canal Sparkplug B 3.0 se déclare d'un mot : l'espace de noms
`spBv1.0/...` et le descripteur `sparkplug_b.proto` sont embarqués.

```yaml
# api/asyncapi.yaml - extrait
channels:
  sparkplug:
    address: "spBv1.0/{group}/{type}/{edge_node}"
    x-flexway-sparkplug: true
    parameters:
      group:     { x-flexway-bind: tenant }     # le groupe Sparkplug est le tenant
      edge_node: { x-flexway-bind: "cert:cn" }  # un nœud ne publie que sous son nom
```

| Élément | Traitement |
| --- | --- |
| Naissance et mort | `NBIRTH`/`DBIRTH` -> en ligne, `NDEATH` (testament)/`DDEATH` -> hors ligne, par tenant ; `bdSeq` vérifié ; vus dans la console |
| Séquences | `seq` 0-255 vérifié par nœud, écart compté (`flexway.sparkplug.sequence.gaps`), jamais bloquant |
| Commandes | `NCMD`/`DCMD` publiés par une application (porte HTTP `x-flexway-publish` ou MQTT) sous une portée d'écriture déclarée |

## OPC UA PubSub JSON sur MQTT

> [!NOTE]
> Prévu en 1.7

> **Cible de la version 1.7.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`listeners.mqtt`, `x-flexway-device`).

Aucune extension propre : un éditeur OPC UA PubSub qui publie en JSON sur MQTT est un appareil de
[Appareils MQTT](#appareils-mqtt--x509-ou-jeton-topic-lié-au-tenant-et-au-certificat), sous un **contrat ordinaire** - un canal, son schéma JSON, ses liaisons. Le préfixe
`plant/{site}/` (configuré dans l'éditeur) lie le tenant ; la suite reprend la structure de topic
recommandée par OPC UA (partie 14).

```yaml
# api/asyncapi.yaml - extrait
channels:
  opcua-data:
    address: "plant/{site}/opcua/json/data/{publisher}/{writer_group}"
    parameters:
      site:      { x-flexway-bind: tenant }
      publisher: { x-flexway-bind: "cert:cn" }   # l'éditeur publie sous son propre nom
    messages:
      network-message:
        contentType: application/json
        payload: { $ref: "./opcua-network-message.json" }   # le NetworkMessage JSON, validé
operations:
  publishOpcuaData:
    action: send
    channel: { $ref: "#/channels/opcua-data" }
    x-flexway-upstream: plant-broker
    x-flexway-device: { qos_max: 1, validate: full }
    security: [{ deviceCert: [] }]
```

## Plans lus d'une claim, plan par défaut

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`plans`, `plan_claim`).

Vendre son API par plans : le plan d'un appelant vient d'une **claim vérifiée** de son jeton, ou de
l'attribut `plan` de sa clé d'API ou de son certificat ; ses limites sont déclarées ici. Un appelant
sans plan, ou dont le plan n'est pas déclaré, reçoit le plan **par défaut** - jamais « illimité ».

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # exigé : refus au chargement sans magasin partagé
auth:
  issuers:                             # la clé existante est auth.issuers
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: orders-api
      tenant_claim: org
      plan_claim: subscription.plan    # chemin pointé d'une claim du jeton vérifié
plans:
  default: free                        # obligatoire dès que plans: existe
  count_per: tenant                    # tenant (défaut) | subject
  definitions:
    free:
      requests: { day: 1000, month: 20000 }
      tokens:   { day: 50000, month: 1000000 }
      eur:      { month: 5 }
    pro:
      requests: { day: 100000, month: 2000000 }
      tokens:   { month: 50000000 }
      eur:      { month: 500 }
```

```yaml
# api/openapi.yaml - extrait : rien de propre aux plans, le contrat dit seulement qui entre
paths:
  /orders:
    get:
      operationId: listOrders
      security: [ { corporate: [orders:read] } ]
```

| Règle | Comportement |
| --- | --- |
| Plan inconnu | Le plan `default`, une ligne de journal `plan_unknown` et le compteur `flexway.plans.unknown` - l'appelant est authentifié, l'écart vient de son fournisseur |
| Avec les quotas du tenant ([Poser des quotas](https://flexway.io/docs/operations-quotas/)) | Les deux s'appliquent, le premier atteint refuse : `429` `quota_exceeded`, membre `limit: plan` |
| Clé d'API, certificat | Attribut `plan` de l'entrée du magasin (écrit par `flexway api-key`) ou de `auth.mtls.clients[]` |

## Budgets d'inférence par utilisateur et par cas d'usage

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`ai.budget.per_subject`, `ai.use_cases`, `ai.aliases`, `use_case_claim`, `x-flexway-use-case`).
>

En plus du budget du tenant ([Servir l'inférence](https://flexway.io/docs/operations-inference/)), un budget par **utilisateur** (le sujet vérifié) et par **cas
d'usage**. Le cas d'usage vient du contrat (`x-flexway-use-case`), d'un **alias de modèle** de
`/v1/*`, ou de la claim d'un client de confiance - **jamais** d'un en-tête de l'appelant. Le budget le
plus restrictif gagne.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://tickets.internal:8080
auth:
  issuers:
    - name: corporate
      discovery: https://id.example.com/.well-known/openid-configuration
      audience: ai
      tenant_claim: org
      use_case_claim: ai_use_case      # crue seulement pour les clients de trusted_clients
      trusted_clients: [batch-reviewer]
ai:
  security: [ { oidc: { scopes: [inference] } } ]
  default_backend: hosted
  backends:
    hosted:
      url: https://api.provider.example/v1
      api_key: "file:/etc/flexway/secrets/provider-key"
      default_model: gpt-4.1-mini
      external: true
      price: { input_per_mtok: 0.40, output_per_mtok: 1.60 }
  budget:
    store: "${COUNTERS_URL}"
    monthly_eur: 2000                  # tenant, inchangé
    per_subject: { monthly_eur: 20, daily_tokens: 200000 }   # chaque sujet vérifié
  use_cases:
    support-bot: { monthly_eur: 300 }
    code-review: { monthly_eur: 800, daily_tokens: 5000000 }
  aliases:                             # un nom de modèle de /v1/* qui porte un cas d'usage
    support-assistant: { backend: hosted, model: gpt-4.1-mini, use_case: support-bot }
```

```yaml
# api/openapi.yaml - extrait : une opération (ou une méthode gRPC mesurée) porte son cas d'usage
paths:
  /tickets/{id}/summary:
    post:
      operationId: summarizeTicket
      x-flexway-use-case: support-bot  # un cas d'usage absent de ai.use_cases : refus au chargement
```

| Règle | Comportement |
| --- | --- |
| Appel sans sujet | Pas de budget utilisateur ; celui du tenant s'applique |
| Contrat et claim | Le contrat l'emporte : un client de confiance ne déplace pas une opération marquée vers un autre budget |
| Refus | `429` `budget_exceeded`, membre `limit` : `tenant`, `plan`, `subject` ou `use_case` (enveloppe OpenAI sur `/v1/*`) |
| Plafond souple | Celui du tenant seul ; un budget d'utilisateur ou de cas d'usage est un plafond dur |
| Consommation par utilisateur | `GET /usage/subjects` (administration), sujet haché ; jamais une dimension de métrique |

## Webhook entrant Stripe et rotation du secret

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`webhooks`, `x-flexway-webhook`, schéma lu dans `Stripe-Signature`).

Recevoir les webhooks d'un fournisseur **signés** : la signature est l'identité de l'appelant, une
alternative de `security` au contrat. Un préréglage suffit pour Stripe, GitHub, Slack, Shopify et
SendGrid.

```yaml
# flexway.yaml - les secrets par référence, jamais en clair
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://billing.internal:8080
webhooks:
  secrets:
    stripe-endpoint:
      current: file:/secrets/stripe/current
      previous: file:/secrets/stripe/previous   # rotation : les deux vérifient
      tenant: acme                     # lié comme une clé d'API
  replay: { max_ids: 100000 }          # identifiants vus, par réplique, bornés
```

```yaml
# api/openapi.yaml - extrait
components:
  securitySchemes:
    stripe:
      type: apiKey
      in: header
      name: Stripe-Signature
      x-flexway-webhook: { preset: stripe, secret: stripe-endpoint }
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      security: [{ stripe: [] }]
```

**Rotation du secret**, sans perdre un évènement :

| Étape | Action |
| --- | --- |
| 1 | L'ancien secret passe dans `previous`, le nouveau dans `current` (deux clés du `Secret` monté) ; rechargement à chaud ([Changer la configuration sans redémarrer](https://flexway.io/docs/operations-live-config/)) - les deux vérifient |
| 2 | Le fournisseur bascule sur le nouveau secret ; une signature contre l'un **ou** l'autre suffit |
| 3 | Une fois la bascule faite, `previous` est retiré |

| Refus | Réponse |
| --- | --- |
| En-tête absent ou illisible, signature fausse, horodatage hors tolérance, identifiant déjà vu | `401` `webhook_signature_invalid`, sans dire lequel ; la cause va au journal et à `flexway.webhook.verifications{outcome}` |
| Corps au-delà de la borne | `413`, avant toute vérification |

Le corps est lu en entier et borné **avant** la vérification, puis relayé **octet pour octet** ; les
en-têtes de signature restent (l'amont peut revérifier), le secret ne sort jamais.

## Webhook générique HMAC

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`webhooks`, `x-flexway-webhook`, schéma lu dans `X-Signature`).

Un partenaire sans préréglage : le schéma HMAC se déclare en entier, à **gabarit fermé**.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://partners.internal:8080
webhooks:
  secrets:
    partner-hooks:
      current: file:/secrets/partner/current
      tenant: partner-a
  replay: { max_ids: 100000 }
```

```yaml
# api/openapi.yaml - extrait
components:
  securitySchemes:
    partner:
      type: apiKey
      in: header
      name: X-Signature
      x-flexway-webhook:
        secret: partner-hooks
        algorithm: sha256              # sha256 | sha512 ; sha1 refusé
        signature: { header: X-Signature, prefix: "sha256=", encoding: hex }
        timestamp: { header: X-Timestamp, format: unix }
        id: { header: X-Delivery-Id }
        signed: "{timestamp}.{body}"   # gabarit fermé : {id}, {timestamp}, {body}
        tolerance: 5m
paths:
  /hooks/partner:
    post:
      operationId: partnerEvents
      security: [{ partner: [] }]
```

## Standard Webhooks, mémoire de rejeu partagée

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`webhooks`, `x-flexway-webhook`, schéma lu dans `webhook-signature`).

Un émetteur conforme à [Standard Webhooks](https://www.standardwebhooks.com/), le format des
webhooks **sortants** de Flexway ([Publier et consommer sur un courtier](https://flexway.io/docs/operations-async/)) : HMAC-SHA256 (`v1`) ou Ed25519 (`v1a`), identifiant et horodatage
signés. Avec plusieurs répliques, la mémoire des identifiants vus peut être **partagée** (option
déclarée) : sans elle, un rejeu sur une autre réplique dans la fenêtre de tolérance passe.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://events.internal:8080
server:
  counters: { store: "${COUNTERS_URL}" }   # la mémoire partagée vit dans le magasin des compteurs
webhooks:
  secrets:
    platform-events:
      current: file:/secrets/platform/current   # whsec_<base64>
      previous: file:/secrets/platform/previous
      tenant: acme
  replay: { max_ids: 100000, shared: true }    # délai, et refus si le magasin ne répond pas
```

```yaml
# api/openapi.yaml - extrait
components:
  securitySchemes:
    platform:
      type: apiKey
      in: header
      name: webhook-signature
      x-flexway-webhook: { preset: standard-webhooks, secret: platform-events }
paths:
  /hooks/platform:
    post:
      operationId: platformEvents
      security: [{ platform: [] }]
```

| Préréglage | Rejeu |
| --- | --- |
| `standard-webhooks`, `stripe`, `slack`, `sendgrid` | Horodatage signé : tolérance et identifiants vus |
| `github`, `shopify` | Identifiant **non signé** : protection partielle, dite par `/config` |

## Listes d'adresses : liste noire globale et `ip-filter`

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`server.ip_deny`, `server.proxy_protocol`). Seule, une politique `ip-filter` est acceptée avec un
> avertissement et **n'est pas appliquée**.

Deux portes : `server.ip_deny`, liste **noire globale** jugée avant le routage (un flux de menaces ne
coûte ni routage ni cryptographie) ; la politique `ip-filter`, attachée à un hôte, une route ou une
opération, jugée après le routage et avant CORS et l'identité.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://shop.internal:8080
server:
  trusted_proxies: [10.0.0.0/8]        # X-Forwarded-For cru de ces pairs seulement (existe déjà)
  proxy_protocol: { from: [10.0.5.0/24] }   # v1 et v2, seulement de ces pairs ; lu au démarrage
  ip_deny:
    inline: [203.0.113.0/24]
    files: [/lists/threat-feed.txt]    # une plage par ligne, # commentaire ; relu au rechargement
policies:
  definitions:
    office-only:   { type: ip-filter, config: { allow: [198.51.100.0/24, "2001:db8::/32"] } }
    stripe-ranges: { type: ip-filter, config: { allow_files: [/lists/stripe-ips.txt] } }
  chains:
    hosts: { admin.example.com: [office-only] }
```

```yaml
# api/openapi.yaml - extrait : le niveau opération de la chaîne, au contrat comme toute politique
paths:
  /hooks/stripe:
    post:
      operationId: stripeEvents
      x-flexway-policies: [stripe-ranges]   # les plages publiées par le fournisseur
```

| Règle | Comportement |
| --- | --- |
| Adresse cliente | Le pair TCP ; derrière un pair de `trusted_proxies`, `X-Forwarded-For` lu de droite à gauche jusqu'au premier saut non fiable ; PROXY protocol d'un autre pair : connexion fermée |
| `ip-filter` | `allow` : l'adresse doit y être ; `deny` : refusée si elle y est ; `deny` gagne ; la plus précise des chaînes gagne, `!office-only` la retire |
| Route inconnue | La chaîne de l'hôte est jugée avant `404` ou `405` : un refusé reçoit `403` et ne cartographie pas les routes |
| Refus | `403` `address_denied`, avant toute cryptographie ; `flexway.ip.refused{level}` |
| Fichier illisible au rechargement | La configuration précédente reste - jamais une liste vide substituée |

## Compression négociée

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : aujourd'hui, la politique `compress` est
> acceptée avec un avertissement (`a later version - NOT applied`) et n'est pas appliquée.**

gzip, brotli et zstd, négociés par `Accept-Encoding` ; opt-in par une politique - sans elle, rien ne
change.

```yaml
# flexway.yaml
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
policies:
  definitions:
    compress-json:
      type: compress
      config:
        encodings: [zstd, br, gzip]    # préférence du serveur à qualité égale
        min_bytes: 1024                # défaut fixé au banc M1
        types: [application/json, "application/*+json", "text/*"]   # liste blanche
  chains:
    global: [compress-json]
```

```yaml
# api/openapi.yaml - extrait : rien à déclarer ; un flux n'est jamais compressé
paths:
  /products:
    get:
      operationId: listProducts
```

| Règle | Comportement |
| --- | --- |
| Éligible | Longueur connue au moins `min_bytes`, type de la liste blanche, pas de `Content-Encoding` de l'amont, méthode autre que `HEAD` ; un flux (SSE, longueur inconnue, `101`) jamais |
| En-têtes | `Content-Encoding`, `Content-Length` recalculé, `Vary: Accept-Encoding` sur toute réponse éligible, `ETag` fort rendu faible |
| Niveaux | Fixés au banc, une constante par algorithme : aucune clé |
| Cache ([Mettre une lecture en cache](https://flexway.io/docs/operations-cache/)) | Une entrée par encodage choisi, jamais par valeur brute d'`Accept-Encoding` |
| BREACH | Une réponse qui mêle un secret et une entrée reflétée : l'exclure par une condition `when` |

## Coalescence des lectures en vol

> [!NOTE]
> Prévu en 1.8

> **Cible de la version 1.8.0 - pas encore construit : refusé au chargement aujourd'hui**
> (`x-flexway-coalesce`).

Une rafale de lectures identiques pendant qu'une première est en vol : un seul appel amont, la
réponse copiée à chaque attendant. Opt-in **par opération**, au contrat ; la clé est celle du cache -
tenant **et** sujet pour une opération qui exige une identité. Chaque attendant a été admis et
**compté** : la coalescence épargne l'amont, jamais le quota. `flexway.yaml` n'a rien de nouveau.

```yaml
# flexway.yaml - rien de propre à la coalescence
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://catalog.internal:8080
```

```yaml
# api/openapi.yaml - extrait
paths:
  /catalog/products:
    get:
      operationId: listProducts
      x-flexway-cache: { ttl: 30s }    # facultatif : le cache d'abord, la coalescence sur ses absences
      x-flexway-coalesce: { max_waiters: 256, max_bytes: 1MiB }
```

| Règle | Comportement |
| --- | --- |
| Refus au chargement | Méthode autre que `GET` ou `HEAD`, flux, WebSocket, méthode gRPC, inférence |
| Non partageable | `Set-Cookie`, longueur inconnue, au-delà de `max_bytes` : chaque attendant appelle l'amont |
| Au-delà de `max_waiters` | L'appel suit son chemin seul (`flexway.coalesce.requests{role="overflow"}`) |
| Erreur de l'amont | Partagée comme une réponse ; un délai dépassé du meneur ne l'est pas |
