
# 환경 설정 업데이트

`PATCH /v1/environment-settings` · 권한 `environment-settings:write`

병합 패치(`application/merge-patch+json`, RFC 7396)로 환경 설정을 업데이트합니다. `If-Match`는 필수입니다. 누락되면 `428`, 오래된 버전이면 `412`가 반환됩니다. 설정이 한 번도 기록되지 않았다면 버전은 `"0"`이며, 첫 기록 시 행이 생성됩니다. 변경 사항이 없는 패치는 아무것도 기록하지 않으며 버전도 그대로 유지됩니다.

## 요청

| 필드 | 유형 | 필수 | 설명 |
|---|---|---|---|
| `If-Match` | header | 예 | 마지막으로 읽은 설정의 `ETag`입니다(기록된 적이 없으면 `"0"`). 누락 시 `428`, 오래된 값이면 `412`. |
| `locales.enabled` | string[] | 아니요 | 활성화된 언어 코드(등록된 언어), 1~64개 항목, 중복 없음. `null`일 수 없습니다. |
| `locales.default` | string | 아니요 | 기본 언어이며 `locales.enabled` 중 하나여야 합니다. `null`일 수 없습니다. |
| `default_zoneinfo` | string | 아니요 | IANA 시간대 이름(기본값 `UTC`). `null`일 수 없습니다. |
| `session.idle_timeout_s` | integer | 아니요 | 유휴 시간 제한(초), 60~8640000(기본값 259200). `session.absolute_timeout_s`보다 클 수 없습니다. `null`일 수 없습니다. |
| `session.absolute_timeout_s` | integer | 아니요 | 절대 수명(초), 60~31536000(기본값 2592000). `null`일 수 없습니다. |
| `session.max_concurrent` | integer | 아니요 | 사용자당 동시 세션 수, 0~500. `0`은 시스템 한도(기본값)를 의미합니다. `null`일 수 없습니다. |
| `session.concurrent_mode` | string | 아니요 | `evict_oldest`(기본값) 또는 `deny_new`. `null`일 수 없습니다. |
| `session.anomaly_action` | string | 아니요 | `enforce`(기본값) 또는 `log`. `null`일 수 없습니다. |
| `signup.mode` | string | 아니요 | `open`(기본값), `invite_only` 또는 `closed`. `null`일 수 없습니다. |
| `signup.email_domain_mode` | string | 아니요 | `off`(기본값), `allowlist` 또는 `denylist`. `null`일 수 없습니다. |
| `signup.email_domains` | string[] \| null | 아니요 | 최대 1000개 도메인. 목록 전체를 대체합니다(RFC 7396은 배열을 병합하지 않습니다). `null` 또는 `[]`이면 목록이 비워집니다. 항목은 정규화되며(소문자, IDNA A-label), 중복은 제거되고 정렬된 상태로 저장됩니다. 모드가 `off`인 동안에도 유지됩니다. |
| `security_notifications.enabled` | boolean | 아니요 | 마스터 스위치(기본값 `true`). `null`일 수 없습니다. |
| `security_notifications.events` | string[] \| null | 아니요 | 보안 이벤트 12개 중 일부(`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`). 목록 전체를 대체하며 표준 순서로 반환됩니다. `null` 또는 `[]`이면 목록이 비워집니다(이벤트 없음). |
| `account_recovery.mode` | string | 아니요 | `self_service_waiting`(기본값), `admin_only` 또는 `disabled`. `null`일 수 없습니다. |
| `account_recovery.waiting_hours` | integer | 아니요 | 24~168시간(기본값 72). 모든 모드에 저장됩니다. `null`일 수 없습니다. |
| `account_recovery.support_url` | string | 아니요 | 빈 값이거나, 사용자 정보와 프래그먼트가 없는 최대 2048바이트의 `https://` URL입니다. 복구가 `disabled`일 때 표시됩니다. `null`일 수 없습니다. |
| `mfa.email_second_factor_enabled` | boolean | 아니요 | 이메일 두 번째 요소 켜기 또는 끄기(기본값 `false`). `null`일 수 없습니다. |
| `mfa.email_second_factor_acr` | string | 아니요 | 이메일 요소가 인정되는 보증 수준: `1fa`(기본값) 또는 `mfa`. `null`일 수 없습니다. |
| `mfa.totp_issuer_label` | string | 아니요 | TOTP 발급자 레이블, 최대 64자. 빈 값이면 환경의 표시 이름을 사용합니다. 제어 문자, `:`, 앞뒤 공백은 사용할 수 없습니다. `null`일 수 없습니다. |
| `mfa.recovery_codes_enabled` | boolean | 아니요 | 복구 코드 켜기 또는 끄기(기본값 `true`). `null`일 수 없습니다. |
| `mfa.recovery_code_count` | integer | 아니요 | 복구 코드 개수, 8~16(기본값 10). `null`일 수 없습니다. |
| `mfa.required` | boolean | 아니요 | 환경 내 모든 로그인에 두 번째 요소를 요구합니다(기본값 `false`). 애플리케이션, 조직 및 요청 요구 사항과 OR로 결합됩니다. 이보다 낮은 기존 SSO 세션은 단계 상향 대상이 됩니다(`prompt=none`과 함께 `interaction_required`). `null`일 수 없습니다. |
| `mfa.min_acr` | string | 아니요 | 환경 내 모든 로그인이 도달해야 하는 최저 보증 수준: `1fa`(기본값, 효과 없음), `mfa` 또는 `phr`. 환경, 애플리케이션, 조직 및 요청 임계값 중 가장 높은 값이 적용되며, 새 로그인과 SSO 세션 재사용에 모두 적용됩니다. `null`일 수 없습니다. |
| `mfa.enrollment_grace_days` | integer | 아니요 | 두 번째 요소가 없는 사용자가 등록하지 않고 로그인할 수 있는 일수. 등록이 처음 요구된 시점부터 계산하며, 0~14(기본값 `0`, 유예 없음)입니다. 유예는 환경 및 조직 기준이 요구하는 항목만 연기하며, 애플리케이션의 `min_acr`과 요청된 `acr_values`는 연기되지 않습니다. 범위를 벗어나면 `out_of_range`와 함께 `422 validation_failed`가 반환됩니다. 기간을 줄이면 즉시 적용됩니다. `null`일 수 없습니다. |
| `frontchannel_logout_enabled` | boolean | 아니요 | 프론트채널 로그아웃 켜기 또는 끄기(기본값 `false`). 끄더라도 등록된 URI는 유지됩니다. `null`일 수 없습니다. |
| `allowed_first_factors` | string[] | 아니요 | 허용되는 첫 번째 요소: `password`, `passkey`, `email_otp`, `magic_link`, `sms_otp`, `social`, `enterprise` 중 하나 이상, 중복 없음. 목록 전체를 대체하며 이 순서대로 반환됩니다. 기본값은 `password`, `passkey`, `social`, `enterprise`이며, `email_otp`, `magic_link`, `sms_otp`는 명시적으로 추가할 때만 켜집니다. 빈 목록은 `422 required`를 반환합니다(아무도 로그인할 수 없는 환경이 되기 때문). `null`일 수 없습니다. |

## 동작

- 그룹은 재귀적으로 병합됩니다. `locales`, `session`, `signup`, `security_notifications`, `account_recovery`, `mfa` 안에서는 변경하는 필드만 보내십시오. 그룹 자체는 `null`일 수 없습니다.
- 세션 수명 단축, 동시 세션 한도 낮추기, 가입 또는 계정 복구 범위 축소, 이메일 두 번째 요소를 `mfa`에서 `1fa`로 낮추기, `allowed_first_factors`에서 요소 제거는 새 세션과 로그인에 즉시 적용됩니다. 기존 세션은 각자의 수명을 유지합니다.
- 검증 오류에는 필드의 JSON 포인터가 포함됩니다. 예를 들어 `/session/idle_timeout_s`와 코드 `out_of_range`입니다. 목록 항목의 오류는 해당 항목을 가리킵니다. `/allowed_first_factors/1`에 `unknown_value`(알 수 없는 값) 또는 `duplicate`(반복된 값).
- `allowed_first_factors`는 애플리케이션 및 조직 정책과 교차 적용됩니다. 이들은 이 집합을 좁힐 수만 있습니다.
- 새 집합이 자체 `allowed_first_factors`를 가진 애플리케이션과 공통 방법을 하나도 남기지 않으면 요청은 `422 policy_conflict`를 반환합니다. 먼저 애플리케이션의 집합을 변경하십시오.
- 감사 기록과 변경 이벤트에는 변경된 필드의 이름만 포함되며 값은 포함되지 않습니다. 공격 방어는 별도의 리소스이며 이 리소스에 포함되지 않습니다.

## 응답

`200 OK`와 함께 업데이트된 설정 및 새 `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"
}
```

## 오류

| HTTP | code | 발생 조건 |
|---|---|---|
| 400 | `unknown_field` | 패치에 알 수 없거나 읽기 전용인 필드가 있거나, JSON 객체가 아닙니다(`malformed_request`). |
| 412 | `precondition_failed` | `If-Match`가 현재 버전과 일치하지 않습니다. |
| 415 | `unsupported_media_type` | 콘텐츠 타입이 `application/merge-patch+json`이 아닙니다. |
| 422 | `validation_failed` | 값이 잘못되었습니다(포인터가 필드를 지정). 또는 `null`일 수 없는 필드를 `null`로 설정했습니다(`code: invalid`). |
| 422 | `policy_conflict` | 새 `allowed_first_factors`가 자체 집합을 가진 애플리케이션과 공통 방법을 남기지 않습니다. |
| 428 | `precondition_required` | `If-Match` 헤더가 누락되었습니다. |
| 401 | `unauthenticated` | 자격 증명이 없거나, 잘못되었거나, 취소되었습니다. |
| 403 | `insufficient_permissions` | 자격 증명에 필요한 권한이 없습니다. |
| 429 | `rate_limited` | 요청 한도를 초과했습니다. `Retry-After` 기간이 지난 후 다시 시도하십시오. |
| 503 | `service_unavailable` | 종속 서비스를 일시적으로 사용할 수 없습니다. `Retry-After` 기간이 지난 후 다시 시도하십시오. |

## 예시

이메일 매직 링크를 켜고 비밀번호를 끄는 예시:

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

세션 및 가입 설정 예시:

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