
# 구성 변경 계획

`POST /v1/config-plans` · 권한 `config:read`와 문서에 있는 각 종류의 읽기 권한

구성 문서를 환경의 현재 구성과 비교하고 결과를 계획으로 저장합니다. 아무것도 변경되지 않습니다. 계획은 문서를 적용하면 무엇이 바뀌는지만 보여 줍니다. `development`에서 `staging` 또는 `production`으로 승격하기 전에 검토할 때 사용하세요.

## 요청

| 필드 | 설명 |
|---|---|
| `document` | 필수. [`GET /v1/config-export`](../config-export/get.md) 형식의 구성 문서입니다. JSON Schema `https://docs.keenauth.com/config/schema/v1.json`과 일치해야 합니다. |
| `prune` | 선택, 기본값 `false`. `true`이면 문서에 없는 문서 종류의 리소스 삭제가 계획됩니다. |

- 문서의 종류는 `kinds`가 있으면 그것이고(이때 모든 리소스는 나열된 종류여야 함), 없으면 리소스의 종류입니다. 이 종류만 비교하며 다른 종류는 그대로 둡니다.
- 문서에 없는 리소스는 `prune`이 `true`가 아니면 삭제되지 **않습니다**. 단일 종류(`environment_settings`, `identity_settings`, `branding`)는 절대 삭제되지 않습니다.
- 단일 종류에서 문서에 없는 필드는 변경되지 않으며 대상의 현재 값이 유지됩니다. 따라서 필드가 추가되기 전에 내보낸 문서는 그 필드를 초기화하지 않습니다. `null`을 포함해 명시적으로 지정한 값은 그대로 기록됩니다. 다른 종류에서는 없는 필드가 기본값을 받습니다.
- 문서에는 비밀 값이 있으면 안 됩니다. 비밀 필드에는 `secret_ref` 자리 표시자나 `null`만 넣을 수 있으며, 값은 거부되고 응답에 다시 표시되지 않습니다.
- 문서의 `warnings`는 무시됩니다. 최대 15,000개까지 허용됩니다(내보내기에는 그보다 많이 포함되지 않습니다).
- 권한은 같은 종류를 내보낼 때와 같습니다. `prune`을 사용하면 삭제되는 리소스를 참조할 수 있는 종류도 읽으므로 그 권한도 필요합니다.

## 응답

`201 Created`, `Location: /v1/config-plans/{plan_id}`와 `Cache-Control: no-store` 포함.

| 필드 | 설명 |
|---|---|
| `id` | 계획 ID, `cfgp_…`. |
| `status` | `planned`, `expires_at` 이후에는 `expired`. |
| `expires_at` | 계획은 1시간 동안 유효하며 만료 7일 후 삭제됩니다. |
| `summary` | `create`, `update`, `delete`, `no_op` 작업 수. |
| `changes` | 리소스마다 하나의 항목, 종류 순서 다음 `key` 순서. |
| `warnings` | `secret_required`: 대상에 아직 없는 클라이언트 시크릿이 연결에 필요합니다. |

각 변경에는 `kind`, `key`, `action`(`create`, `update`, `delete` 또는 `no_op`), `object_version`(계획 시점 대상 리소스의 버전, `create`에서는 `null`), `requires_secret`, `fields`, 그리고 `create`와 `update`에서는 원하는 `spec`이 있습니다. `fields`의 각 항목은 필드 차이입니다. `pointer`(RFC 6901, 예: `/spec/name`), `op`(RFC 6902와 같은 `add`, `remove` 또는 `replace`), `before`, `after`. 목록은 전체 값으로 비교합니다.

내보낸 문서를 변경하지 않고 보내면 리소스와 집합형 목록의 순서와 관계없이 모든 리소스가 `no_op`으로 계획됩니다.

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

모든 계획은 환경의 관리 로그에 `config_plan.created` 항목을 기록합니다.

## 제한

- **속도:** 플랜에는 환경별 분당 자체 한도가 있습니다. `production`에서 Free 20, Startup과 Business 60, Enterprise 120이며 `development`와 `staging`에서는 그 10분의 1(최소 2)입니다.
- **환경별 할당량:** 적용되지도 만료되지도 않은 플랜은 최대 100개, 삭제될 때까지(만료 7일 후) 저장되는 플랜은 최대 2,000개 및 1 GiB입니다. 다른 환경의 플랜은 계산되지 않습니다.
- **용량:** 서버는 큰 플랜을 동시에 제한된 수만 계산하며, 한 환경은 그 용량의 최대 4분의 1만 사용할 수 있습니다.
- **필드 제한**은 구성 서비스가 쓰기 시 적용하는 제한입니다. 스키마가 목록과 텍스트를 제한하고(예: `redirect_uris` 50, `allowed_web_origins` 20, `permissions` 1,000), 연결의 `settings`는 최대 32 KiB, 깊이 8단계, 애플리케이션의 `jwks`와 각 권한의 `consent_text_i18n`은 최대 16 KiB입니다.

## 오류

| HTTP | code | 발생 조건 |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | 본문이 JSON이 아니거나 `document`, `prune` 이외의 필드가 있습니다. |
| 401 | `unauthenticated` | 자격 증명이 없거나 유효하지 않거나 취소되었습니다. |
| 403 | `insufficient_permissions` | 필요한 권한이 없습니다. `required_permissions`에 모두 나열됩니다. |
| 413 | `config_document_too_large`, `payload_too_large` | 문서가 리소스 5,000개 또는 10 MiB를 넘습니다. 더 적은 종류를 계획하세요. |
| 422 | `validation_failed` | 아래를 참조하세요. `errors[].pointer`는 요청 안(`/document/...`)을 가리킵니다. |
| 429 | `rate_limited` | 플랜 속도 제한 또는 환경의 플랜 할당량에 도달했거나 환경이 너무 많은 플랜을 계산하고 있습니다. `Retry-After` 시간 후 다시 시도하세요(할당량의 경우 가장 오래된 플랜이 만료되거나 삭제될 때). |
| 503 | `service_unavailable` | 플랜 계산 용량이 가득 찼거나 종속 서비스를 일시적으로 사용할 수 없습니다. `Retry-After` 시간 후 다시 시도하세요. |

| `errors[].code` | 의미 |
|---|---|
| `required` | `document`가 없습니다. |
| `invalid` | 이 포인터에서 문서가 스키마와 일치하지 않습니다. |
| `duplicate` | 두 리소스의 `kind`와 `key`가 같습니다. |
| `kind_not_listed` | 리소스의 종류가 `kinds`에 없습니다. |
| `secret_value` | 비밀 값이 발견되었습니다. `secret_ref` 자리 표시자를 사용하세요. |
| `unresolved_reference` | 참조하는 `key`가 변경 후에는 존재하지 않습니다. |
| `key_in_use` | `key`가 구성으로 관리되지 않는 애플리케이션(조직 소유 또는 자체 등록) 또는 시스템 API에 속합니다. |
| `still_referenced` | `/prune`: 남는 리소스가 계획이 삭제할 리소스를 참조합니다. |
| `too_large` | 리소스, 경고 또는 최상위 필드에 JSON 값이 200,000개를 초과합니다. |
| `too_long` | 연결의 `settings`가 32 KiB를 초과하거나, 애플리케이션의 `jwks` 또는 권한의 `consent_text_i18n`이 16 KiB를 초과합니다. |
| `too_deep` | 연결의 `settings`가 8단계보다 깊게 중첩되어 있습니다. |
| `reserved` | API `identifier`가 플랫폼의 `urn:idp:` 네임스페이스에 있습니다. |

## 예제

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