ADR 0004 — Регистрация git-учётных данных пользователя
Контекст
Пункт 3 Фазы 2 (см. CLAUDE.md): git-учётные данные пользователя
(токены GitHub/GitLab, используемые GithubProvider/GitlabProvider
для работы от его имени) должны жить в той же таблице
user_credentials, что и API-ключи LLM (Фаза 1), с тем же envelope
encryption и с обязательной проверкой подключения при регистрации.
Два вопроса формы:
user_credentials.providerуже был enumllm_provider(ollama/anthropic/...). Токен git — не LLM-provider: расширениеllm_providerсмешало бы два домена в одном enum, используемом такжеmodels/token_usage(действительно только LLM). Повторное использованиеgit_provider(используемогоproject_git_connections) тоже не годится: он включает'local', что не имеет смысла как provider учётных данных (токена для локального git не существует).- Невалидный/отозванный токен обнаруживается лишь при попытке использовать его — без проверки подключения регистрация "успешна", а сбой появляется лишь при первом bootstrap Gitflow, гораздо позже и с гораздо более дорогой диагностикой.
Решение
Выделенный enum credential_provider (миграция 0007), с
CredentialProviderName = LLMProviderName | GitCredentialProviderName
в packages/shared — объединение двух доменов лишь в типе, который
использует UserCredentialRepository, без смешения enum базы.
GitCredentialProviderName — это Extract<GitProviderName, 'github' | 'gitlab'> — выводится из enum git-provider, не дублирует список,
но структурно исключает 'local'.
СИНХРОННАЯ и ОБЯЗАТЕЛЬНАЯ проверка подключения перед шифрованием/сохранением.
RegisterGitCredentialUseCase.execute (новый) сначала вызывает
GitCredentialConnectionTester.test(provider, token); только если это
разрешается, токен шифруется
(EncryptionService.encrypt) и записывается
(UserCredentialRepository.upsert). При сбое ничего не пишется — см.
GitCredentialConnectionTestFailedError в
domain/git/git-errors.ts. Реальная реализация
(GitCredentialConnectionTesterImpl) делает самый дешёвый возможный вызов
в каждом API, чтобы подтвердить, что токен аутентифицируется:
GET /user (Octokit users.getAuthenticated) в GitHub,
GET /user (Gitbeaker Users.showCurrentUser) в GitLab — ни один
не пытается перечислять репозитории или что-либо, зависящее от области сверх
базовой аутентификации.
PAT всегда через token: в Gitbeaker, никогда oauthToken:.
GitLab проверяет их по-разному (заголовок PRIVATE-TOKEN vs.
Authorization: Bearer) — они не взаимозаменяемы. oauthToken остаётся
зарезервирован для потока OAuth проекта
(project_git_connections, Фаза 2 пункт 4+), который эта сессия не
меняет.
Собственный endpoint только для регистрации; GET/DELETE переиспользованы.
POST /users/me/git-credentials (новый,
GitCredentialsController) — единственный путь в этой сессии, потому что лишь
регистрации нужна синхронная проверка подключения. Листинг и удаление
уже существовали в CredentialsController (Фаза 1, LLM) над той же
таблицей/репозиторием — UserCredentialRepository был расширен с
LLMProviderName до CredentialProviderName (это был оставшийся отдельный
LLMProviderName, ломавший сборку, см. delete() в
user-credential.repository.ts) вместо дублирования
list/delete для git. Никакого @RequireRole в новом controller: речь
об учётных данных самого аутентифицированного пользователя, тот же шаблон, что у
эквивалентного endpoint LLM.
Сбой подключения отображается в 422, а не 400 и не 409
(git-provider-error.filter.ts): это не некорректный payload (400) и не
конфликт с существующим состоянием (409) — это семантически
невалидная сущность (токен, который никогда не аутентифицировался), классический случай 422.
Последствия
- Регистрация git-учётных данных теперь делает синхронный сетевой вызов к API provider перед ответом — endpoint медленнее "слепой" регистрации, намеренно (см. Решение).
UserCredentialRepository(и его миграция0007) теперь обслуживает два домена (LLM и git) через одну таблицу/enum типа — любой будущий provider учётных данных (git или нет) входит черезCredentialProviderName, а не через новый enum.- Проверка подключения не повторяется (
GitCredentialConnectionTesterне используетwithRetry, см. 0003) — временный сетевой сбой при регистрации требует, чтобы пользователь вручную попробовал снова; приемлемо, потому что это разовое интерактивное действие, а не фоновая операция.