
# Actualizar la configuración del entorno

`PATCH /v1/environment-settings` · permiso `environment-settings:write`

Actualiza la configuración del entorno mediante un parche de fusión JSON (`application/merge-patch+json`, RFC 7396). `If-Match` es obligatorio: si falta, devuelve `428`; si la versión está obsoleta, devuelve `412`. Si la configuración nunca se escribió, la versión es `"0"` y la primera escritura crea el registro. Un parche que no cambia nada no escribe nada y conserva la versión.

## Solicitud

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `If-Match` | header | sí | El `ETag` de la configuración tal como se leyó por última vez (`"0"` si nunca se escribió). Si falta: `428`; si está obsoleto: `412`. |
| `locales.enabled` | string[] | no | Códigos de idioma habilitados (idiomas registrados), de 1 a 64 entradas, sin duplicados. No puede ser `null`. |
| `locales.default` | string | no | Idioma predeterminado; debe ser uno de `locales.enabled`. No puede ser `null`. |
| `default_zoneinfo` | string | no | Nombre de zona horaria IANA (predeterminado `UTC`). No puede ser `null`. |
| `session.idle_timeout_s` | integer | no | Tiempo de inactividad en segundos, de 60 a 8640000 (predeterminado 259200); no mayor que `session.absolute_timeout_s`. No puede ser `null`. |
| `session.absolute_timeout_s` | integer | no | Duración absoluta de la sesión en segundos, de 60 a 31536000 (predeterminado 2592000). No puede ser `null`. |
| `session.max_concurrent` | integer | no | Sesiones simultáneas por usuario, de 0 a 500; `0` significa el límite del sistema (predeterminado). No puede ser `null`. |
| `session.concurrent_mode` | string | no | `evict_oldest` (predeterminado) o `deny_new`. No puede ser `null`. |
| `session.anomaly_action` | string | no | `enforce` (predeterminado) o `log`. No puede ser `null`. |
| `signup.mode` | string | no | `open` (predeterminado), `invite_only` o `closed`. No puede ser `null`. |
| `signup.email_domain_mode` | string | no | `off` (predeterminado), `allowlist` o `denylist`. No puede ser `null`. |
| `signup.email_domains` | string[] \| null | no | Como máximo 1000 dominios; reemplaza la lista completa (RFC 7396 no fusiona arrays). `null` o `[]` la borra. Las entradas se normalizan (minúsculas, etiqueta A de IDNA), los duplicados se eliminan y la lista se guarda ordenada. Se conserva mientras el modo sea `off`. |
| `security_notifications.enabled` | boolean | no | Interruptor general (predeterminado `true`). No puede ser `null`. |
| `security_notifications.events` | string[] \| null | no | Subconjunto de los 12 eventos de seguridad (`password_changed`, `password_reset`, `email_changed`, `phone_changed`, `authenticator_added`, `authenticator_removed`, `recovery_code_used`, `recovery_codes_regenerated`, `account_locked`, `breached_password_detected`, `new_device_sign_in`, `account_recovery_started`); reemplaza la lista completa y se devuelve en orden canónico. `null` o `[]` la borra (ningún evento). |
| `account_recovery.mode` | string | no | `self_service_waiting` (predeterminado), `admin_only` o `disabled`. No puede ser `null`. |
| `account_recovery.waiting_hours` | integer | no | De 24 a 168 horas (predeterminado 72); se almacena en todos los modos. No puede ser `null`. |
| `account_recovery.support_url` | string | no | Vacía o una URL `https://` de como máximo 2048 bytes, sin información de usuario ni fragmento. Se muestra cuando la recuperación está `disabled`. No puede ser `null`. |
| `mfa.email_second_factor_enabled` | boolean | no | Segundo factor por correo activado o desactivado (predeterminado `false`). No puede ser `null`. |
| `mfa.email_second_factor_acr` | string | no | Nivel de garantía que cuenta el factor de correo: `1fa` (predeterminado) o `mfa`. No puede ser `null`. |
| `mfa.totp_issuer_label` | string | no | Etiqueta del emisor TOTP, de como máximo 64 caracteres; vacía significa el nombre de visualización del entorno. Sin caracteres de control, `:` ni espacios al principio o al final. No puede ser `null`. |
| `mfa.recovery_codes_enabled` | boolean | no | Códigos de recuperación activados o desactivados (predeterminado `true`). No puede ser `null`. |
| `mfa.recovery_code_count` | integer | no | Número de códigos de recuperación, de 8 a 16 (predeterminado 10). No puede ser `null`. |
| `mfa.required` | boolean | no | Exigir un segundo factor en cada inicio de sesión del entorno (predeterminado `false`). Se combina con OR con los requisitos de la aplicación, de la organización y de la solicitud; una sesión SSO existente por debajo de este nivel se obliga a subir de nivel (`interaction_required` con `prompt=none`). No puede ser `null`. |
| `mfa.min_acr` | string | no | Nivel de garantía mínimo que debe alcanzar cada inicio de sesión del entorno: `1fa` (predeterminado, sin efecto), `mfa` o `phr`. Se aplica el umbral más alto entre el del entorno, la aplicación, la organización y la solicitud, tanto a los nuevos inicios de sesión como a la reutilización de sesiones SSO. No puede ser `null`. |
| `mfa.enrollment_grace_days` | integer | no | Días durante los que un usuario sin segundo factor puede seguir iniciando sesión sin registrarlo, contados desde la primera vez que se le exige el registro, de 0 a 14 (predeterminado `0`, sin período de gracia). El período de gracia solo aplaza lo que exigen el entorno y la organización; el `min_acr` de una aplicación y los `acr_values` solicitados nunca se aplazan. Un valor fuera de rango devuelve `422 validation_failed` con `out_of_range`. Si se reduce, el cambio surte efecto de inmediato. No puede ser `null`. |
| `frontchannel_logout_enabled` | boolean | no | Cierre de sesión front-channel activado o desactivado (predeterminado `false`); desactivarlo conserva las URI registradas. No puede ser `null`. |
| `allowed_first_factors` | string[] | no | Primeros factores permitidos: al menos uno de `password`, `passkey`, `email_otp`, `magic_link`, `sms_otp`, `social`, `enterprise`, sin duplicados. Reemplaza la lista completa y se devuelve en este orden. Predeterminado `password`, `passkey`, `social`, `enterprise`; `email_otp`, `magic_link` y `sms_otp` solo se activan si se añaden explícitamente. Una lista vacía devuelve `422 required` (un entorno en el que nadie podría iniciar sesión). No puede ser `null`. |

## Comportamiento

- Los grupos se fusionan de forma recursiva: envíe solo los campos que cambie dentro de `locales`, `session`, `signup`, `security_notifications`, `account_recovery` y `mfa`. Un grupo en sí no puede ser `null`.
- Reducir la duración de una sesión, bajar el límite de sesiones simultáneas, restringir el registro o la recuperación de cuenta, pasar el segundo factor por correo de `mfa` a `1fa` y quitar un factor de `allowed_first_factors` se aplican de inmediato a las nuevas sesiones e inicios de sesión; las sesiones existentes conservan su propia duración.
- Los errores de validación incluyen el puntero JSON del campo, por ejemplo `/session/idle_timeout_s` con el código `out_of_range`. Un error en una entrada de una lista apunta a la entrada: `/allowed_first_factors/1` con `unknown_value` (valor desconocido) o `duplicate` (valor repetido).
- `allowed_first_factors` se intersecta con las políticas de la aplicación y de la organización: estas solo pueden restringir este conjunto.
- Si el nuevo conjunto no deja ningún método en común con una aplicación que tenga su propio `allowed_first_factors`, la solicitud devuelve `422 policy_conflict`; cambie primero el conjunto de la aplicación.
- El registro de auditoría y el evento de cambio incluyen los nombres de los campos modificados, nunca sus valores. La protección frente a ataques es un recurso independiente y no forma parte de este.

## Respuesta

`200 OK` con la configuración actualizada y el nuevo `ETag`.

```json
{
  "locales": {"enabled": ["en", "tr"], "default": "tr"},
  "default_zoneinfo": "Europe/Istanbul",
  "session": {"idle_timeout_s": 3600, "absolute_timeout_s": 2592000, "max_concurrent": 5, "concurrent_mode": "evict_oldest", "anomaly_action": "enforce"},
  "signup": {"mode": "invite_only", "email_domain_mode": "allowlist", "email_domains": ["example.com"]},
  "security_notifications": {"enabled": true, "events": ["password_changed", "password_reset"]},
  "account_recovery": {"mode": "self_service_waiting", "waiting_hours": 72, "support_url": ""},
  "mfa": {"email_second_factor_enabled": false, "email_second_factor_acr": "1fa", "totp_issuer_label": "Acme", "recovery_codes_enabled": true, "recovery_code_count": 10, "required": false, "min_acr": "1fa", "enrollment_grace_days": 0},
  "frontchannel_logout_enabled": false,
  "allowed_first_factors": ["password", "passkey", "magic_link", "enterprise"],
  "updated_at": "2026-10-05T09:30:00.123456Z"
}
```

## Errores

| HTTP | código | Cuándo |
|---|---|---|
| 400 | `unknown_field` | El parche contiene un campo desconocido o de solo lectura, o no es un objeto JSON (`malformed_request`). |
| 412 | `precondition_failed` | `If-Match` no coincide con la versión actual. |
| 415 | `unsupported_media_type` | El tipo de contenido no es `application/merge-patch+json`. |
| 422 | `validation_failed` | Un valor no es válido (el puntero indica el campo), o se asignó `null` a un campo que no puede ser `null` (`code: invalid`). |
| 422 | `policy_conflict` | El nuevo `allowed_first_factors` no deja ningún método en común con una aplicación que tiene su propio conjunto. |
| 428 | `precondition_required` | Falta la cabecera `If-Match`. |
| 401 | `unauthenticated` | La credencial falta, no es válida o fue revocada. |
| 403 | `insufficient_permissions` | La credencial no tiene el permiso necesario. |
| 429 | `rate_limited` | Se superó el límite de solicitudes; reintente después del período indicado en `Retry-After`. |
| 503 | `service_unavailable` | Una dependencia no está disponible temporalmente; reintente después del período indicado en `Retry-After`. |

## Ejemplo

Activar los enlaces mágicos por correo y desactivar las contraseñas:

```bash
curl -X PATCH https://api.eu.keenauth.com/v1/environment-settings \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/merge-patch+json" \
  -H 'If-Match: "0"' \
  -d '{"allowed_first_factors":["passkey","magic_link","social","enterprise"]}'
```

Configuración de sesión y de registro:

```bash
curl -X PATCH https://api.eu.keenauth.com/v1/environment-settings \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/merge-patch+json" \
  -H 'If-Match: "0"' \
  -d '{"session":{"idle_timeout_s":3600},"signup":{"mode":"invite_only","email_domain_mode":"allowlist","email_domains":["example.com"]}}'
```
