
# Planificar un cambio de configuración

`POST /v1/config-plans` · permiso `config:read` y el permiso de lectura de cada tipo del documento

Compara un documento de configuración con la configuración actual del entorno y guarda el resultado como un plan. No se cambia nada: un plan solo muestra lo que haría aplicar el documento. Úsalo para revisar una promoción de `development` a `staging` o `production` antes de aplicarla.

## Solicitud

| Campo | Descripción |
|---|---|
| `document` | Obligatorio. Un documento de configuración con el formato de [`GET /v1/config-export`](../config-export/get.md). Debe cumplir el JSON Schema `https://docs.keenauth.com/config/schema/v1.json`. |
| `prune` | Opcional, `false` por defecto. Si es `true`, se planifica eliminar los recursos de los tipos del documento que faltan en el documento. |

- Los tipos del documento son `kinds` si se indica (entonces cada recurso debe ser de un tipo listado); si no, los tipos de sus recursos. Solo se comparan estos tipos; los demás quedan como están.
- Un recurso que falta en el documento **no** se elimina salvo que `prune` sea `true`. Los tipos únicos (`environment_settings`, `identity_settings`, `branding`) nunca se eliminan.
- En los tipos singleton, un campo que falta en el documento no cambia: se conserva el valor actual del destino. Así, un documento exportado antes de que existiera un campo no restablece ese campo. Un valor indicado explícitamente, incluido `null`, se sigue escribiendo. En los demás tipos, un campo que falta toma su valor predeterminado.
- El documento no debe contener valores secretos. Un campo secreto solo contiene un marcador `secret_ref` o `null`; un valor se rechaza y nunca se devuelve.
- Los `warnings` del documento se ignoran; se aceptan como máximo 15.000 (una exportación nunca contiene más).
- Los permisos son los de una exportación de los mismos tipos. Con `prune` también se leen los tipos que pueden hacer referencia a un recurso eliminado, así que sus permisos también son necesarios.

## Respuesta

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

| Campo | Descripción |
|---|---|
| `id` | ID del plan, `cfgp_…`. |
| `status` | `planned`; `expired` después de `expires_at`. |
| `expires_at` | El plan es válido durante 1 hora. Se elimina 7 días después de caducar. |
| `summary` | Número de acciones `create`, `update`, `delete` y `no_op`. |
| `changes` | Una entrada por recurso, en orden de tipo y luego por `key`. |
| `warnings` | `secret_required`: la conexión necesita un client secret que el destino aún no tiene. |

Cada cambio tiene `kind`, `key`, `action` (`create`, `update`, `delete` o `no_op`), `object_version` (la versión del recurso de destino al planificar; `null` en `create`), `requires_secret`, `fields` y, en `create` y `update`, el `spec` deseado. Cada entrada de `fields` es una diferencia de campo: `pointer` (RFC 6901, por ejemplo `/spec/name`), `op` (`add`, `remove` o `replace`, como en RFC 6902), `before` y `after`. Las listas se comparan como valores completos.

Un documento exportado enviado sin cambios planifica `no_op` para cada recurso, sea cual sea el orden de los recursos y de las listas de tipo conjunto.

```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"}]
}
```

Cada plan escribe una entrada `config_plan.created` en el registro de gestión del entorno.

## Límites

- **Frecuencia:** los planes tienen su propio límite por minuto y entorno: en `production`, 20 en Free, 60 en Startup y Business, 120 en Enterprise; en `development` y `staging`, una décima parte (al menos 2).
- **Cuota por entorno:** como máximo 100 planes que no se hayan aplicado ni caducado, y como máximo 2.000 planes y 1 GiB de planes almacenados hasta que se eliminan (7 días después de caducar). Los planes de otros entornos nunca cuentan.
- **Capacidad:** el servidor calcula un número limitado de planes grandes a la vez; un entorno puede usar como máximo una cuarta parte de esa capacidad.
- **Límites de campos:** son los que el servicio de configuración aplica al escribir: el esquema limita listas y textos (por ejemplo `redirect_uris` 50, `allowed_web_origins` 20, `permissions` 1.000), los `settings` de una conexión tienen como máximo 32 KiB y 8 niveles de profundidad, y los `jwks` de una aplicación y el `consent_text_i18n` de cada permiso como máximo 16 KiB.

## Errores

| HTTP | code | Cuándo |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | El cuerpo no es JSON o tiene un campo distinto de `document` y `prune`. |
| 401 | `unauthenticated` | Las credenciales faltan, no son válidas o se revocaron. |
| 403 | `insufficient_permissions` | A las credenciales les falta un permiso necesario; `required_permissions` los enumera todos. |
| 413 | `config_document_too_large`, `payload_too_large` | El documento supera los 5000 recursos o 10 MiB; planifica menos tipos. |
| 422 | `validation_failed` | Consulta abajo; `errors[].pointer` apunta dentro de la solicitud (`/document/...`). |
| 429 | `rate_limited` | Se alcanzó el límite de frecuencia de planes o la cuota de planes del entorno, o el entorno está calculando demasiados planes; vuelve a intentarlo tras el periodo `Retry-After` (en la cuota, cuando el plan más antiguo caduque o se elimine). |
| 503 | `service_unavailable` | El cálculo de planes está al límite de capacidad o una dependencia no está disponible temporalmente; vuelve a intentarlo tras el periodo `Retry-After`. |

| `errors[].code` | Significado |
|---|---|
| `required` | Falta `document`. |
| `invalid` | El documento no cumple el esquema en este puntero. |
| `duplicate` | Dos recursos tienen el mismo `kind` y `key`. |
| `kind_not_listed` | El tipo del recurso no está en `kinds`. |
| `secret_value` | Se encontró un valor secreto; usa un marcador `secret_ref`. |
| `unresolved_reference` | La `key` referenciada no existiría después del cambio. |
| `key_in_use` | La `key` pertenece a una aplicación que no se gestiona con configuración (propiedad de una organización o autorregistrada) o a la API del sistema. |
| `still_referenced` | `/prune`: un recurso que se mantiene hace referencia a un recurso que el plan eliminaría. |
| `too_large` | Un recurso, una advertencia o un campo de nivel superior tiene más de 200.000 valores JSON. |
| `too_long` | Los `settings` de una conexión superan 32 KiB, o los `jwks` de una aplicación o el `consent_text_i18n` de un permiso superan 16 KiB. |
| `too_deep` | Los `settings` de una conexión tienen más de 8 niveles de anidamiento. |
| `reserved` | El `identifier` de la API está en el espacio de nombres `urn:idp:` de la plataforma. |

## Ejemplo

```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
```
