<!-- task=m-jenkins-credential/02+01_crumb_token_store plan=0 tag=AUTH_TOKEN -->

# Plan - AUTH_TOKEN

## 이 파일을 읽는 구현 에이전트에게

이 subtask는 `agent-task/m-jenkins-credential/01_web_login_entry/complete.log`가 생긴 뒤 구현한다. `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 사용자만 결정할 수 있는 Jenkins endpoint, credential, 조직 정책 blocker가 생기면 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 archive/log/complete.log를 직접 만들지 않는다.

## 배경

`web-login` Epic의 핵심은 WebView에 남은 Jenkins session으로 crumb를 받고 사용자별 API token을 발급한 뒤 Keychain에 저장하는 것이다. 이 작업은 Jenkins auth/session, token response parsing, Keychain persistence, 앱 session restore를 함께 만지므로 `login-webview` 이후 종속 plan으로 분리한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 후속 에이전트가 재실행으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니며, code-review가 최종 stop 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md`
- Task ids:
  - `crumb-token`: WebView same-origin 요청으로 crumb를 받고 사용자별 API token을 자동 발급한다.
  - `keychain-store`: 자동 발급한 username/API token을 `TokenStore`를 통해 macOS Keychain에 저장하고 삭제할 수 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/jenkins-credential.md`
- `agent-ops/rules/project/domain/artifact-flow/rules.md`
- `agent-ops/rules/project/domain/macos-platform/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-test/local/macos-platform-smoke.md`
- `pubspec.yaml`
- `/config/.pub-cache/hosted/pub.dev/webview_flutter-4.13.1/pubspec.yaml`
- `/config/.pub-cache/hosted/pub.dev/webview_flutter-4.13.1/README.md`
- `/config/.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.25.1/pubspec.yaml`
- `lib/src/app.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/features/settings/settings_page.dart`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/services/jenkins_artifact_session.dart`
- `lib/src/services/jenkins_client.dart`
- `lib/src/services/token_store.dart`
- `test/jenkins_artifact_session_test.dart`
- `test/jenkins_client_test.dart`
- `test/token_store_test.dart`
- `test/widget_test.dart`

### 테스트 환경 규칙

- 선택 환경: `local`.
- `agent-test/local/rules.md`를 읽었고 artifact-flow smoke와 macos-platform smoke profile을 읽었다.
- 필수 remote runner 명령: `zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'`, `zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'`.
- WebView macOS plugin과 Keychain wiring의 compile evidence를 위해 `zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter build macos'`를 포함한다.
- 실제 Jenkins sandbox endpoint가 없으면 token 발급은 fake bridge/mock HTTP로 검증하고, 실제 Jenkins 수동 smoke는 생략 사유를 review stub에 남긴다.

### 테스트 커버리지 공백

- `TokenStore.saveSession/readSession/clearSession`은 `test/token_store_test.dart`에 있다.
- WebView same-origin crumb/token JS bridge, token response parser, token 저장 후 session restore refresh는 기존 테스트가 없다. fake bridge와 mock storage 기반 unit/widget test가 필요하다.
- 실제 Jenkins CSRF crumb/token endpoint의 조직별 path 차이는 unit test만으로 보장할 수 없으므로, endpoint가 제공되면 수동 smoke가 필요하다.

### 심볼 참조

- renamed/removed symbol: none expected. 새 public typedef/class 추가 중심이다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `m-jenkins-credential`.
- `01_web_login_entry`: predecessor. WebView 진입과 login completion result를 제공해야 한다.
- `02+01_crumb_token_store`: 이 plan. directory name 기준 predecessor index `01` 필요.
- predecessor 상태: active `agent-task/m-jenkins-credential/01_web_login_entry/complete.log` 없음, archive candidate도 확인되지 않음. 구현 시작 전 해당 complete.log가 필요하다.

### 범위 결정 근거

- 포함: WebView same-origin JS bridge, crumb fetch, API token generate request, response parser, `TokenStore.saveSession/clearSession` wiring, AppSokApp session refresh, mock tests.
- 제외: LDAP 직접 연동, 공용 service account token, Jenkins plugin/Auth Broker, session-restore Epic의 `reauth`/`logout-clear` UI 전반.
- 실제 Jenkins endpoint URL은 secret 없이 표준 Jenkins API path로 구현하고, 조직별 차이가 발견되면 사용자 리뷰 요청이 아니라 test evidence와 follow-up plan으로 처리한다.

### 빌드 등급

- `cloud-G06`: auth/security, WebView JS bridge, Keychain persistence, app session refresh가 결합되어 실패 모드가 hard-to-review다.

## 구현 체크리스트

- [ ] `01_web_login_entry` predecessor complete.log를 확인하고 없으면 구현을 시작하지 않는다.
- [ ] WebView same-origin bridge로 `/whoAmI/api/json`, `/crumbIssuer/api/json`, API token generate POST를 실행하고 결과 parser를 추가한다.
- [ ] 자동 발급된 username/API token/base URL을 `TokenStore.saveSession`으로 저장하고 실패 시 partial credential을 남기지 않는다.
- [ ] AppSokApp/AppSokShell/SettingsPage wiring을 통해 저장 성공 후 Jenkins session을 restore하고 build 목록 loader가 활성화되게 한다.
- [ ] mock bridge/storage/client 기반 unit/widget test를 추가하고 `keychain-store` 검증을 포함한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask는 directory name `02+01_crumb_token_store` 기준으로 `01_web_login_entry` 완료에 의존한다.
- 구현 시작 전 `agent-task/m-jenkins-credential/01_web_login_entry/complete.log` 또는 matching archive complete.log가 있어야 한다.

### [AUTH_TOKEN-1] Same-Origin Crumb And Token Bridge

#### 문제

[jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:192)는 stored credential로 `/whoAmI`를 호출하지만 WebView session cookie를 쓰는 crumb/token 발급 API가 없다. [settings_page.dart](/config/workspace/appsok/lib/src/features/settings/settings_page.dart:89)의 Web Login UI도 token 발급 경로와 연결되어 있지 않다.

Before:

```dart
Future<JenkinsUser> fetchCurrentUser({
  required Uri baseUrl,
  required JenkinsCredentials credentials,
}) async {
  final uri = baseUrl.replace(
    path: _appendPath(baseUrl.path, 'whoAmI/api/json'),
  );
```

#### 해결 방법

- Plan 1의 WebView completion bridge를 확장하거나 새 `JenkinsWebAuthBridge` abstraction을 추가한다.
- JS fetch는 `credentials: 'same-origin'`만 사용하고 raw cookie를 Dart로 전달하지 않는다.
- 순서:
  1. `/whoAmI/api/json`에서 username/id 확인.
  2. `/crumbIssuer/api/json`에서 `crumbRequestField`와 `crumb` 확인.
  3. `/user/<encoded-user>/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken`에 `POST`하고 token JSON을 parse.
- token name은 `AppSok macOS <timestamp>`처럼 non-secret label만 쓴다.
- 모든 error string은 `JenkinsClientException.toString()` redaction 또는 equivalent sanitizer를 거치게 한다.

#### 수정 파일 및 체크리스트

- [ ] `lib/src/features/settings/jenkins_web_login_page.dart` 또는 bridge file: same-origin JS bridge.
- [ ] `lib/src/services/jenkins_client.dart`: 필요한 response model/parser만 추가하고 Basic-auth client와 경계를 유지한다.
- [ ] `test/jenkins_web_auth_bridge_test.dart`: crumb/token success, forbidden, malformed JSON, missing token tests.

#### 테스트 작성

- 작성: `test/jenkins_web_auth_bridge_test.dart`.
- assertion: username path encoding, crumb header name propagation, generated token value parsing, secret redaction.

#### 중간 검증

```bash
flutter test test/jenkins_web_auth_bridge_test.dart
```

예상: bridge/parser tests pass.

### [AUTH_TOKEN-2] Keychain Store And Session Refresh Wiring

#### 문제

[token_store.dart](/config/workspace/appsok/lib/src/services/token_store.dart:66)는 `saveSession`을 제공하고 [token_store.dart](/config/workspace/appsok/lib/src/services/token_store.dart:78)는 `clearSession`을 제공하지만, WebView에서 자동 발급한 token을 저장하는 호출 경로가 없다. [app.dart](/config/workspace/appsok/lib/src/app.dart:33)의 `_startup`은 `late final Future<bool>`라 로그인 성공 후 session restore를 다시 시작할 수 없다.

Before:

```dart
late final Future<bool> _startup = _session.restore();
```

```dart
Future<void> saveSession(JenkinsSessionConfig session) async {
  await _storage.write(key: _baseUrlKey, value: session.baseUrl.toString());
  await _storage.write(
    key: _usernameKey,
    value: session.credentials.username,
  );
```

#### 해결 방법

- AppSokApp의 startup future를 mutable state로 바꾸고, Web login completion handler가 session save 후 `_session.restore()`를 재실행하게 한다.
- SettingsPage에서 token 발급 성공 결과를 `JenkinsSessionConfig`로 저장한다.
- 저장 직후 `JenkinsArtifactSession.restore()` 또는 `JenkinsClient.fetchCurrentUser`로 검증하고, 실패 시 `TokenStore.clearSession()`을 호출해 partial credential을 제거한다.
- 삭제 기능은 기존 `clearSession` API 검증을 유지하고, 전체 logout UI는 `logout-clear` Task 범위로 남긴다.

#### 수정 파일 및 체크리스트

- [ ] `lib/src/app.dart`: session restore refresh callback.
- [ ] `lib/src/features/app_shell.dart`: SettingsPage에 auth completion handler 전달.
- [ ] `lib/src/features/settings/settings_page.dart`: save/progress/success/failure UI.
- [ ] `lib/src/services/token_store.dart`: 필요한 경우 generated session helper 추가. 기존 key names 유지.
- [ ] `test/token_store_test.dart`: generated session save/read/clear 검증 보강.
- [ ] `test/widget_test.dart` 또는 focused app wiring test: login success 후 jobs loader 활성화.

#### 테스트 작성

- 작성/보강:
  - `test/token_store_test.dart`: generated credential round trip and clear.
  - `test/widget_test.dart` 또는 `test/app_auth_test.dart`: fake login completion 후 stored session restore and jobs visible.

#### 중간 검증

```bash
flutter test test/token_store_test.dart test/jenkins_web_auth_bridge_test.dart
```

예상: storage and bridge tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/settings/jenkins_web_login_page.dart` | AUTH_TOKEN-1 |
| `lib/src/services/jenkins_client.dart` | AUTH_TOKEN-1 |
| `lib/src/services/token_store.dart` | AUTH_TOKEN-2 |
| `lib/src/features/settings/settings_page.dart` | AUTH_TOKEN-2 |
| `lib/src/features/app_shell.dart` | AUTH_TOKEN-2 |
| `lib/src/app.dart` | AUTH_TOKEN-2 |
| `test/jenkins_web_auth_bridge_test.dart` | AUTH_TOKEN-1 |
| `test/token_store_test.dart` | AUTH_TOKEN-2 |
| `test/widget_test.dart` 또는 `test/app_auth_test.dart` | AUTH_TOKEN-2 |

## 최종 검증

```bash
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'
zsh -lc 'cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter build macos'
```

예상: analyzer issue 없음, 모든 테스트 통과, macOS build 성공. 실제 Jenkins sandbox endpoint가 없으면 수동 smoke 생략 사유를 review stub에 기록한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
