
# Planifier une modification de configuration

`POST /v1/config-plans` · permission `config:read` et la permission de lecture de chaque type du document

Compare un document de configuration à la configuration actuelle de l'environnement et enregistre le résultat sous forme de plan. Rien n'est modifié : un plan montre seulement ce que ferait l'application du document. Utilisez-le pour examiner une promotion de `development` vers `staging` ou `production` avant de l'appliquer.

## Requête

| Champ | Description |
|---|---|
| `document` | Obligatoire. Un document de configuration au format de [`GET /v1/config-export`](../config-export/get.md). Il doit respecter le JSON Schema `https://docs.keenauth.com/config/schema/v1.json`. |
| `prune` | Facultatif, `false` par défaut. Si `true`, les ressources des types du document absentes du document sont planifiées pour suppression. |

- Les types du document sont `kinds` s'il est fourni (chaque ressource doit alors être d'un type listé), sinon les types de ses ressources. Seuls ces types sont comparés ; les autres restent inchangés.
- Une ressource absente du document n'est **pas** supprimée sauf si `prune` vaut `true`. Les types singletons (`environment_settings`, `identity_settings`, `branding`) ne sont jamais supprimés.
- Dans les types singletons, un champ absent du document reste inchangé : la valeur actuelle de la cible est conservée. Un document exporté avant l'ajout d'un champ ne réinitialise donc pas ce champ. Une valeur donnée explicitement, y compris `null`, est toujours écrite. Dans les autres types, un champ absent prend sa valeur par défaut.
- Le document ne doit contenir aucune valeur secrète. Un champ secret ne contient qu'un espace réservé `secret_ref` ou `null` ; une valeur est rejetée et n'est jamais renvoyée.
- Les `warnings` du document sont ignorés ; 15 000 au plus sont acceptés (un export n'en contient jamais davantage).
- Les permissions sont celles d'un export des mêmes types. Avec `prune`, les types pouvant référencer une ressource supprimée sont aussi lus, leurs permissions sont donc également requises.

## Réponse

`201 Created` avec `Location: /v1/config-plans/{plan_id}` et `Cache-Control: no-store`.

| Champ | Description |
|---|---|
| `id` | Identifiant du plan, `cfgp_…`. |
| `status` | `planned` ; `expired` après `expires_at`. |
| `expires_at` | Le plan est valable 1 heure. Il est supprimé 7 jours après son expiration. |
| `summary` | Nombre d'actions `create`, `update`, `delete` et `no_op`. |
| `changes` | Une entrée par ressource, dans l'ordre des types puis par `key`. |
| `warnings` | `secret_required` : la connexion a besoin d'un secret client que la cible n'a pas encore. |

Chaque modification contient `kind`, `key`, `action` (`create`, `update`, `delete` ou `no_op`), `object_version` (la version de la ressource cible au moment du plan ; `null` pour `create`), `requires_secret`, `fields` et, pour `create` et `update`, le `spec` souhaité. Chaque entrée de `fields` est une différence de champ : `pointer` (RFC 6901, par exemple `/spec/name`), `op` (`add`, `remove` ou `replace`, comme dans RFC 6902), `before` et `after`. Les listes sont comparées comme des valeurs entières.

Un document exporté fourni sans modification planifie `no_op` pour chaque ressource, quel que soit l'ordre des ressources et des listes de type ensemble.

```json
{
  "id": "cfgp_01m4qpqtywfs7930tb6kqn5rc5",
  "status": "planned",
  "prune": false,
  "kinds": ["connection"],
  "expires_at": "2026-10-12T11:00:00Z",
  "summary": {"create": 1, "update": 1, "delete": 0, "no_op": 1},
  "changes": [
    {"kind": "connection", "key": "db", "action": "no_op", "object_version": 5, "requires_secret": false, "fields": []},
    {"kind": "connection", "key": "github", "action": "create", "object_version": null, "requires_secret": true, "fields": [], "spec": {"type": "social", "strategy": "github", "name": "GitHub", "display_name": null, "status": "active", "settings": {}, "client_secret": {"secret_ref": {"source": "env", "name": "KEENAUTH_CONNECTION_GITHUB_CLIENT_SECRET"}}}},
    {"kind": "connection", "key": "google", "action": "update", "object_version": 4, "requires_secret": false, "fields": [{"pointer": "/spec/name", "op": "replace", "before": "Google", "after": "Google Workspace"}], "spec": {"type": "social", "strategy": "google", "name": "Google Workspace", "display_name": null, "status": "active", "settings": {}, "client_secret": {"secret_ref": {"source": "env", "name": "KEENAUTH_CONNECTION_GOOGLE_CLIENT_SECRET"}}}}
  ],
  "warnings": [{"code": "secret_required", "kind": "connection", "key": "github", "pointer": "/spec/client_secret"}]
}
```

Chaque plan écrit une entrée `config_plan.created` dans le journal de gestion de l'environnement.

## Limites

- **Débit :** les plans ont leur propre limite par minute et par environnement : en `production`, 20 en Free, 60 en Startup et Business, 120 en Enterprise ; en `development` et `staging`, un dixième (au moins 2).
- **Quota par environnement :** au plus 100 plans ni appliqués ni expirés, et au plus 2 000 plans et 1 Gio de plans stockés jusqu'à leur suppression (7 jours après l'expiration). Les plans des autres environnements ne comptent jamais.
- **Capacité :** le serveur calcule un nombre limité de grands plans à la fois ; un environnement peut utiliser au plus un quart de cette capacité.
- **Limites de champs :** ce sont celles que le service de configuration applique à l'écriture : le schéma borne les listes et les textes (par exemple `redirect_uris` 50, `allowed_web_origins` 20, `permissions` 1 000), les `settings` d'une connexion font au plus 32 Kio et 8 niveaux de profondeur, les `jwks` d'une application et le `consent_text_i18n` de chaque permission au plus 16 Kio.

## Erreurs

| HTTP | code | Quand |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | Le corps n'est pas du JSON ou contient un champ autre que `document` et `prune`. |
| 401 | `unauthenticated` | Les identifiants sont absents, invalides ou révoqués. |
| 403 | `insufficient_permissions` | Il manque une permission requise ; `required_permissions` les liste toutes. |
| 413 | `config_document_too_large`, `payload_too_large` | Le document dépasse 5 000 ressources ou 10 Mio ; planifiez moins de types. |
| 422 | `validation_failed` | Voir ci-dessous ; `errors[].pointer` pointe dans la requête (`/document/...`). |
| 429 | `rate_limited` | La limite de débit des plans ou le quota de plans de l'environnement est atteint, ou l'environnement calcule trop de plans ; réessayez après le délai `Retry-After` (pour le quota, lorsque le plus ancien plan expire ou est supprimé). |
| 503 | `service_unavailable` | Le calcul des plans est saturé ou une dépendance est temporairement indisponible ; réessayez après le délai `Retry-After`. |

| `errors[].code` | Signification |
|---|---|
| `required` | `document` est absent. |
| `invalid` | Le document ne respecte pas le schéma à ce pointeur. |
| `duplicate` | Deux ressources ont le même `kind` et la même `key`. |
| `kind_not_listed` | Le type de la ressource n'est pas dans `kinds`. |
| `secret_value` | Une valeur secrète a été trouvée ; utilisez un espace réservé `secret_ref`. |
| `unresolved_reference` | La `key` référencée n'existerait plus après la modification. |
| `key_in_use` | La `key` appartient à une application non gérée par la configuration (appartenant à une organisation ou auto-enregistrée), ou à l'API système. |
| `still_referenced` | `/prune` : une ressource conservée référence une ressource que le plan supprimerait. |
| `too_large` | Une ressource, un avertissement ou un champ de premier niveau contient plus de 200 000 valeurs JSON. |
| `too_long` | Les `settings` d'une connexion dépassent 32 Kio, ou les `jwks` d'une application ou le `consent_text_i18n` d'une permission dépassent 16 Kio. |
| `too_deep` | Les `settings` d'une connexion sont imbriqués sur plus de 8 niveaux. |
| `reserved` | L'`identifier` de l'API est dans l'espace de noms `urn:idp:` de la plateforme. |

## Exemple

```bash
curl -X POST "https://api.eu.keenauth.com/v1/config-plans" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"document\": $(cat keenauth.json), \"prune\": false}"

keenauth config plan -f keenauth.yaml
```
