CoopID — единый PR компонента 42 «Удостоверение пайщика» (схлопнутый стек #123–#146) #148

Open
claude wants to merge 94 commits from coopID into dev
Owner

CoopID — единый PR компонента 42 «Удостоверение пайщика»

Консолидация всего стека CoopID (Эпики 1–12) в один PR в dev. Заменяет стек из 13 stacked-PR (#123, #127, #133, #134, #138, #139, #140, #141, #142, #143, #144, #145, #146) — он схлопнут без потерь: каждая ветка стека проверенно является предком верхушки feat/coopid-auth-ui, ветка coopID создана из её коммита.

Объём: 91 коммит, 408 файлов, +28373 / −181.
Затронуто: components/controller (auth-v2, резолверы, миграции), components/auth (CoopID REST/crypto), components/sdk (Zeus + login-фасад), components/desktop (вход/миграция/сессии/PIN), infra/coopid (authentik blueprint, Caddy), components/migrator, components/notifications.

Что входит (по эпикам)

  • Эпик 1 — пайщик заходит в кооператив и видит удостоверение (зонтичный, #123).
  • Эпик 3 — восстановление доступа пайщика, подход 2 (#127).
  • Эпик 4 — типы верификации + офлайн-проверка удостоверения (#133).
  • Эпики 6/8/9 — CASL-авторизация, аудит 152-ФЗ, наблюдаемость (#134); cron-уборка истёкших access_rules (#142).
  • Эпик 9 — устойчивость COOPOS RPC: failover-пул + finalized-only + M-of-N консенсус (#138); Prometheus /metrics + auth-метрики (#143).
  • Эпик 7 — SDK CoopID-логин-фасад: handshake bind→sign→verify + lifecycle токенов + REST /coop/refresh (#139); мост подписи signChainDigest + WalletPluginCoopId + PIN-кэш + CoopID-сессия (#145).
  • Эпик 11 — вход по паролю + миграция «ключ→пароль» (#140); десктоп-UI: рабочий вход, миграция, сессии и PIN на столе пайщика (#146).
  • Эпик 12 — CoopID-восстановление: confirm→username, AAD account-независим, десктоп magic-link (#141, #144).

Готовность к тестам

Работает уже сейчас (без authentik): вход по ключу (легаси, без регрессий); страница «Настройки» на столе пайщика; список активных сессий + завершение сессий (GraphQL AccountSecurityResolver); UI новых флоу (предложение задать пароль при вставке ключа, карточка PIN). Предусловия любого теста: контроллер крутит текущий код, применены миграции auth-v2 через migration-cli.

Заблокировано до инфры: реальный вход по паролю и migrate()/set_password требуют развёрнутого authentik (Эпик 5: публичный SPA OIDC-клиент + env COOPID_ISSUER/COOPID_CLIENT_ID) и SDK-фасада резолва account при входе по паролю (отдельная задача). Без них новые пути деградируют с чистой ошибкой, вход по ключу всегда работает.

Старые ветки стека НЕ удалены — сохранены как резерв; PR #123–#146 закрыты в пользу этого.

🤖 Generated with Claude Code

## CoopID — единый PR компонента 42 «Удостоверение пайщика» Консолидация всего стека CoopID (Эпики 1–12) в один PR в `dev`. Заменяет стек из 13 stacked-PR (#123, #127, #133, #134, #138, #139, #140, #141, #142, #143, #144, #145, #146) — он схлопнут без потерь: каждая ветка стека проверенно является предком верхушки `feat/coopid-auth-ui`, ветка `coopID` создана из её коммита. **Объём:** 91 коммит, 408 файлов, +28373 / −181. **Затронуто:** `components/controller` (auth-v2, резолверы, миграции), `components/auth` (CoopID REST/crypto), `components/sdk` (Zeus + login-фасад), `components/desktop` (вход/миграция/сессии/PIN), `infra/coopid` (authentik blueprint, Caddy), `components/migrator`, `components/notifications`. ### Что входит (по эпикам) - **Эпик 1** — пайщик заходит в кооператив и видит удостоверение (зонтичный, #123). - **Эпик 3** — восстановление доступа пайщика, подход 2 (#127). - **Эпик 4** — типы верификации + офлайн-проверка удостоверения (#133). - **Эпики 6/8/9** — CASL-авторизация, аудит 152-ФЗ, наблюдаемость (#134); cron-уборка истёкших `access_rules` (#142). - **Эпик 9** — устойчивость COOPOS RPC: failover-пул + finalized-only + M-of-N консенсус (#138); Prometheus `/metrics` + auth-метрики (#143). - **Эпик 7** — SDK CoopID-логин-фасад: handshake bind→sign→verify + lifecycle токенов + REST `/coop/refresh` (#139); мост подписи `signChainDigest` + `WalletPluginCoopId` + PIN-кэш + CoopID-сессия (#145). - **Эпик 11** — вход по паролю + миграция «ключ→пароль» (#140); десктоп-UI: рабочий вход, миграция, сессии и PIN на столе пайщика (#146). - **Эпик 12** — CoopID-восстановление: confirm→username, AAD account-независим, десктоп magic-link (#141, #144). ### Готовность к тестам **Работает уже сейчас (без authentik):** вход по ключу (легаси, без регрессий); страница «Настройки» на столе пайщика; список активных сессий + завершение сессий (GraphQL `AccountSecurityResolver`); UI новых флоу (предложение задать пароль при вставке ключа, карточка PIN). Предусловия любого теста: контроллер крутит текущий код, применены миграции auth-v2 через migration-cli. **Заблокировано до инфры:** реальный вход по паролю и `migrate()`/`set_password` требуют развёрнутого **authentik** (Эпик 5: публичный SPA OIDC-клиент + env `COOPID_ISSUER`/`COOPID_CLIENT_ID`) и **SDK-фасада резолва account при входе по паролю** (отдельная задача). Без них новые пути деградируют с чистой ошибкой, вход по ключу всегда работает. Старые ветки стека НЕ удалены — сохранены как резерв; PR #123–#146 закрыты в пользу этого. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude added 91 commits 2026-06-15 13:26:07 +00:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Пайплайн VerifyTimestampService: верификация session_binding_token (HS256), атомарный GETDEL jti до криптопроверки (anti-replay), окно свежести 60s против head_block_time, recover pubkey по каноническому сообщению (зеркало SDK signTimestamp) и сверка с активным ключом аккаунта; успех -> выпуск access/refresh через TokenApplicationService, audit success/failure без секретов. Crypto recover вынесена за BlockchainPort.recoverPublicKey ради гексагонального инварианта auth-v2 (запрет wharfkit в application). id_token/certificate -> Story 1.8.
CertificateService подписывает claims (iss/sub=UUID/iat/exp/coopname/coop_chain[ano,voskhod,vostok]/verification_types/identification/claim_schema_version) ключом permission cert аккаунта vostok через jose+Node KeyObject из PEM (Docker Secret coop_cert_key); coop_chain читается из COOPOS через новый BlockchainPort.getCertPublicKey (single-key, нормализация PUB_K1), кэшируется; size-guard 5КБ под Vision/MIFARE. Эндпоинт GET /coop/certificate под HttpJwtAuthGuard; сертификат также встроен в ответ /coop/verify/timestamp best-effort (завершает контракт 1.7, не ломает логин при сбое). config: COOP_CERT_KEY[_FILE]+геттер. 16 unit зелёных, coopback поднялся чисто. verifyOffline/backward-валидность -> 4.4/2.5.
CertificateService добавляет серийный номер удостоверения (jti=UUID), который требует страница ЛК (Story 1.9). SDK @coopenomics/auth: модуль certificate/ с decodeParticipantCertificate (claims без проверки подписи — для отображения), certificateStatus (active/expiring/expired по exp), VERIFICATION_TYPE_LABELS/verificationTypeLabel (человекочитаемые типы верификации, forward-совместимо); getParticipantCertificate(apiUrl, accessToken) тянет GET /coop/certificate с Bearer по образцу fetchVaultBlob. 13 SDK + 6 controller тестов зелёные, dist пересобран, tsc чист.
ProfilePage (пункт меню «Удостоверение») расширена карточкой: серийный номер (jti, copyable mono), срок действия, статус-чип (Активно/Истекает/Истекло по exp), визуальная цепочка подписей АНО→Восход→Восток→Вы, список verification_types человеческим языком и кнопка «Скачать как QR» (Vision-stub, мягкое уведомление). Данные — GET /coop/certificate через существующий sendGET (Bearer), payload декодируется локально для отображения (подпись — offline-verify Story 4.4). Best-effort: сбой/отсутствие удостоверения показывает EmptyState, профиль работает. Только канон-компоненты и токены, eslint чист, HMR без рестарта контейнера.
[105-4][@ant] feat: RP-initiated logout — отзыв refresh_token и затирание keystore при выходе пайщика
Typecheck / desktop (pull_request) Failing after 8m21s
Typecheck / controller (pull_request) Successful in 13m8s
c6ca8a2fa5
Эндпоинт POST /coop/logout (контур auth-v2): отзывает refresh/access токены
через TokenApplicationService, гасит session_binding cookie и пишет audit_events.
Без guard и идемпотентен — выйти можно и с истёкшим access-токеном. SDK logout()
дёргает сервер best-effort и в любом случае затирает локальный keystore и PIN-ключ
(finally), чтобы расшифрованный ключ не остался в памяти браузера при сбое сети.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 9.1. RedisThrottlerStorage (атомарный Lua INCR/PEXPIRE/block) в
infrastructure (ioredis запрещён в application/domain — порт-токен в domain).
AuthRateLimitGuard проверяет два независимых счётчика per-IP (50/15мин) и
per-account (5/15мин), превышение любого → 429 too_many_attempts в OAuth2-формате
через контурный AuthV2ExceptionFilter. Навешано на session/bind, verify/timestamp
(ip), vault-retrieve (account по subject_id + ip — цель harvest-энумерации).
Изолировано от глобального ThrottlerModule (не задевает прочие роуты). Пресет
magic-link (3/час) готов под Story 3.1; escalating lockout — Story 3.12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
[105-12][@ant] feat: ежедневный pg_basebackup кооп-БД в S3 со связкой retention и аудитом
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
f4701d07c7
Story 9.8. Скрипт infra/coopid/scripts/coopid-backup.sh делает pg_basebackup
всего PG-кластера (authentik_db + coop_domain_db одним gzip-tar) и заливает в
s3://{bucket}/{coopname}/{YYYY-MM-DD}.tar.gz с SSE-S3, чтобы восстановить
кооператив за любой день. Retention 30 дней — через S3 lifecycle rule (режим
--setup), не ручным удалением в cron. Каждый прогон пишет audit_events
coopid.backup.created (s3_key, size_bytes, duration_seconds) прямым INSERT —
бэкап отрабатывает даже при лежащем coopback. Режимы --setup и --dry-run.
README: раздел Бэкапы с env, crontab 03:00 UTC и требованиями к PG. Cron
ставит прод-плейбук. shellcheck зелёный; S3-путь и lifecycle 30d проверены
против MinIO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
[105-12][@ant] docs: runbook восстановления кооператива из S3-бэкапа со smoke-тестом
Typecheck / desktop (pull_request) Failing after 8m17s
Typecheck / controller (pull_request) Successful in 13m3s
a288e8888b
Story 9.9. docs/operations/disaster-recovery.md — пошаговое восстановление
после потери сервера из бэкапа Story 9.8 за RTO до 2 часов: новый сервер с
Docker, скачать архив из S3, распаковать физический basebackup в PGDATA нового
PG-тома (не pg_restore — бэкап физический), docker compose up, и сквозной
smoke-test контура CoopID (вход пайщика → выпуск participant_certificate →
проверка цепочки доверия coop_chain). Документированы RTO и шесть частых
граблей, главная — пароли ролей лежат в самом бэкапе, init-скрипт на непустом
томе не выполняется, нужен ALTER ROLE под текущие секреты. Завершает Подход 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Эндпоинт POST /coop/recovery/request: одноразовый токен в Redis (5 мин),
письмо через готовый Центр уведомлений (workflow reset-key), константный
202 (анти-enumeration), rate-limit 3/час по email и IP с отдельным кодом
TooManyRecoveryAttempts. Источник пайщика — user-домен (таблицы participants
из AC в brownfield нет). Открывает Подход 2 (Эпик 3, восстановление доступа).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Собственный TOTP-движок (RFC 6238, node:crypto, без npm-зависимости, проверен
тест-векторами RFC), таблица two_factor в coop_domain_db (секрет зашифрован
server-key — не ключ пайщика, инвариант vault цел), enroll/activate/disable
под JWT-guard, узкий TWO_FACTOR_VERIFIER для recovery (3.2) и 2FA-входа.
Решение владельца: TOTP — базовый 2-й канал MVP вместо magic-link-toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Настройка recovery_strategy (email_magic_link по умолчанию | offline_code | council)
на пайщика гейтит входные каналы восстановления: requestByEmail (3.1) тихо no-op'ит,
requestByOfflineCode (3.4) бросает InvalidOfflineCode, если их канал не выбран; council
отключает оба (восстановление только через approval-flow 6.9). Смена — под JWT-guard со
step-up TOTP (вместо пароля: в authentik нет password-verify, паттерн 2FA-disable) + audit.
GET/POST /coop/recovery/strategy; таблица recovery_strategy (V2.4.4); 19 unit-тестов зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На финализации входа (этап 2, verify-timestamp) фиксируем устройство: audit-событие
coopid.login.successful (subject_id, ip, user_agent, accept_language, признак нового
устройства) + обновление Redis-набора известных устройств пайщика. Fingerprint =
sha256(user_agent + Accept-Language) — server-side; признак isNewDevice — основа
уведомления о новом устройстве (Story 3.9). Tracking best-effort: сбой не валит вход.
Отступления: screen-resolution (нет в контракте входа) и geo-IP (нет провайдера) отложены.
17 unit-тестов зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сессия = persistent refresh-токен платформенного стора (им финализирует вход
verify-timestamp); GET/DELETE coop/sessions под JWT-guard; отзыв = удаление строки
токена (реально инвалидирует refresh). Метаданные device/IP — Redis side-store за
SESSION_METADATA_PORT (ключ sha256(refresh), TTL=refresh, токен не хранится), запись
best-effort на входе. Дрейф AC↔код: authentik oauth_tokens/oauth-revoke → наш
refresh-токен, история не заблокирована на authentik admin-токене. 27 unit-тестов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SecurityIncidentService.report/reportByToken переиспользует SessionsService.revokeAll
(3.7) + пишет audit coopid.security.suspicious_login_reported. Два входа: POST
coop/security/not-me (JWT, из настроек) и not-me/:token (one-click из письма, без auth —
своя сессия скомпрометирована, авторизация single-use 256-бит токеном NOT_ME_TOKEN_STORE,
TTL 7д, consume Lua GET-DEL). Письмо new-device (3.9) теперь несёт notMeUrl — закрыт
отложенный one-click endpoint. Force password change + ротация ключа отложены в 3.3
(AC относит rotation к 3.3; флаг без enforcement = мёртвое состояние). 15 unit-тестов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
verifyOffline(jws, options?): fail-closed офлайн-проверка structure/alg/exp,
укоренение coop_chain в trust-anchor ano, сверка звеньев с локальным
chain_manifests_cache, подпись издателя через wharfkit + low-S нормализация
(cert от jose/Node может быть high-S). trust-anchor реконсилирован в
Antelope-формат (TRUST_ANCHOR_ANO_CERT_PUBKEY, скелет↔реальность). Отзыв офлайн
вне MVP (Story 4.7/Growth). 11 SDK-тестов (вкл. high-S, no-network).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Контроллерный chain_manifests_cache (порт + Redis-стор, наполнение активными
ключами на каждом живом входе). При RpcUnavailable verify-timestamp фолбэкается
на кэш: восстановленный ключ сверяется со снимком через blockchainPort.hasActiveKey
(псевдо-аккаунт, без wharfkit в application), совпал → degraded-вход (флаг +
enum DegradedAuthReason + audit coopid.auth.degraded), иначе fail-closed
CooposDegraded. Read-only scope токена вынесен в Epic 5 (нет scope в платформенных
токенах). 18 unit-тестов (4 новых).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Срок жизни participant_certificate вынесен в настройку кооператива
coop_domain_db.coop_settings.cert_ttl_seconds (миграция V2.4.6, default 3600=1ч),
зажат в [60с,24ч] на чтении и записи; cert.exp=iat+ttl. SDK получил
scheduleCertificateRenewal/computeRenewalDelayMs для бесшовного перезапроса
за 5 мин до exp через refresh_token. Эндпоинт смены TTL председателем — после CASL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В participant_certificate добавлены claims data_retention_contract=erase_on_exclusion
и retention_deadline_ts=iat+30д; значения вынесены в общий retention-policy.ts
и публикуются через GET /.well-known/coopid-claims-policy.json со ссылкой на договор
присоединения. SDK-декодер зеркалит оба claim. Enforcement (отзыв OAuth-клиента
при нарушении) — admin-действие Эпика 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В MVP удостоверение только полное (с PII): exportToQR заменён на exportFullQR
с двумя барьерами — разблокированный vault и явный high-stakes consent. Метод
exportProofQR намеренно не объявлен, его вызов даёт ошибку компиляции; anonymous
proof_of_membership без PII (exp<=24ч) отложен в Growth. Добавлен клиентский
код ошибки ConsentRequired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.3 Эпика 6 (Подход 4 «Управление и авторизация», CoopID компонент 42).

PolicyHandler-контракт (IPolicyHandler {name; evaluate(ctx)}), декораторы @PolicyHandler(name)
и @CheckAbility(action,subject,{policy?}), PolicyRegistry на DiscoveryService (fail-closed на
неизвестное имя, throw на дубль), политика-образец SameCoopVotingPolicy с DB-lookup членства
по participant-vault. Wiring через DiscoveryModule в AuthorizationModule.

Фикс субстрата Layer 2: forward-миграция V2.4.8 реконсилирует access_rules — init V2.4.0 создавал
плейсхолдер со старой схемой, V2.4.7 (6.2) был no-op → repo упал бы на column subject_type does
not exist. DROP пустого плейсхолдера + CREATE правильной схемы, идемпотентно, канон append-only.

Тесты: 4 registry + 5 policy unit + 1 integration с реальной coop_domain_db; coopback рестартнул
чисто, PolicyRegistry обнаружил same-coop-voting через DiscoveryService.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.4 Эпика 6 (Подход 4 «Управление и авторизация», CoopID компонент 42).

PolicyService.ensure — общий вычислитель: Layer 1+2 (свежая Ability с access_rules,
instance-level ownership через asSubject), Layer 3 (именованная политика после Ability).
AuthorizationGuard читает @CheckAbility, обходит по server-secret, извлекает user и ресурс
из GraphQL и HTTP REST контекстов и делегирует ensure — guard и императивный путь делят
одну реализацию. Отказ — 403 с обобщённым authorization_denied, точная причина (enum) только
в server-log (security: не раскрываем слой/правило). Guard точечный, не APP_GUARD.

Тесты: 6 policy.service + 5 guard unit; coopback рестартнул чисто без DI-ошибок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.5 Эпика 6 (Подход 4 «Управление и авторизация», CoopID компонент 42).

Новый overrides-блок в .eslintrc.json (scope auth-v2/**) через no-restricted-syntax с двумя
селекторами: импорт символа AuthRoles и применение декоратора @AuthRoles(...) дают error.
no-restricted-syntax выбран намеренно — no-restricted-imports в соседнем override занят
wharfkit-баном, разные ключи правил мерджатся аддитивно. legacy auth/ сохраняет @AuthRoles
до Phase-3 cleanup. Тест через ESLint Node API проверяет срабатывание в auth-v2 и допуск в auth/.

Дрейф (прав код): auth-v2 на REST-контроллерах без @AuthRoles, миграция вакуумна; первые
боевые @CheckAbility придут в админ-эндпоинтах 6.6/6.7 и critical-actions 6.8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.8 Эпика 6 (Подход 4 «Управление и авторизация», CoopID компонент 42).

Таблица pending_critical_actions (V2.4.9), порт + Postgres-репозиторий coop_domain_db,
Redis-нотификатор совета (publish события, фан-аут — notification-center). CriticalActionsService:
инициатор-председатель ставит действие в pending со своей подписью и окном 24ч, член совета
(отличный от инициатора, уникальный) подтверждает; на 2 подписях — финализация с аудитом
обоих подписантов и payload_hash; @Cron ежедневно отменяет истёкшие. Контроллер — первое боевое
применение @CheckAbility + AuthorizationGuard: create/confirm CriticalAction поверх JWT-guard.

Гейтинг прав на guard, кворум/окно/различимость подписантов в сервисе. Тесты: 8 unit; coopback
рестартнул чисто, роуты смаплены, @Cron зарегистрирован. Разблокирует Story 4.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.9: force-recovery гейтится двумя независимыми каналами согласия —
magic-link пайщика (Redis single-use токен, Lua GET→DEL) либо on-chain решение
общего собрания; при стратегии «решение совета» дополнительно требуется
подтверждённый critical action 6.8 (кворум 2 подписей). Отказ → 403 +
audit ForceRecoveryDenied, разрешение → audit triggered_by:chairman.
Порт + Redis-store/notifier (ioredis только в infrastructure), сервис-гейт,
контроллер /coop/force-recovery с @CheckAbility поверх AuthorizationGuard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 6.10: разделяю в audit инициатора (с timestamp инициации) и подтверждающих
совета (с timestamps), фиксирую payload_hash sha256 для невозможности подмены
содержимого действия в журнале. Добавляю repo.listByTarget и сервис getAuditTrail,
отдающий все критические действия пайщика с полной атрибуцией через эндпоинт
GET /coop/critical-actions/audit-trail/:targetId под @CheckAbility read CriticalAction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 4.7: председатель отзывает ключ через POST /coop/keys/revoke под CASL
update Participant; MVP фиксирует durable pending-state в revoked_keys (вместо
on-chain updateauth, как допускает AC), гасит все активные сессии пайщика и
пишет audit KeyRevokedManually с reason и chairman_id. Пайщик далее обязан
пройти recovery Эпика 3 для получения нового ключа; разблокирована Эпиком 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 8.1: append-only audit_events с партициями и триггерами создан ещё в V2.4.0;
V2.4.11 добавляет колонку user_agent (форензика, наполняет 8.2), выделенную read-only
роль coop_audit_reader (graceful-degrade без CREATEROLE), форвард-роллинг партиций и
re-assert append-only грантов. Побочно: перенос guard сноса плейсхолдера в начало V2.4.7
чинит ordering-баг (column subject_type does not exist), из-за которого вся цепочка
миграций 2.4.2-2.4.11 не применялась в dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 8.2: AuditService получил первоклассное поле userAgent (пишется в колонку
user_agent из V2.4.11) вместо обходного хранения в context; DeviceTrackingService
переведён на него. Добавлена каноническая дока event-schema.md (колонки, конвенция
explicit-null-with-reason, secret-blacklist, каталог событий) рядом с кодом аудита,
т.к. components/controller/docs gitignored под генерённый сайт.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Story 8.5: декоратор @AuditAction помечает резолвер, AuditActionInterceptor
автоматически пишет audit_events (event=coopid.<category>.<handler>, subject_id из
target_id/id, result success/failure, metadata с выкинутыми секретами в _redacted),
извлекая user/args из GraphQL и HTTP контекста как AuthorizationGuard. Точечный
@UseInterceptors, не глобальный; первые потребители — admin-резолверы 6.6/6.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сканер src/config/log-purity.ts (SENSITIVE_TEST_PATTERNS, findSensitiveLeaks,
assertNoSensitiveLeaks) + интеграционный тест на боевом winston-логгере. Тест
вскрыл дефект 8.7: redactionFormat стоял до splat(), splat() повторно вмёрживал
сырой meta из info[SPLAT] поверх маскировки. Фикс: redactionFormat перенесён
после splat() (перед printf). 19/19 тестов зелёные (7 от 8.8 + 12 регресс 8.7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildLogFormat(isProduction): в production winston.format.json() с полем service
и redaction после splat() (инвариант 8.8), в dev — прежний pretty-printf без
изменений. scrubSensitiveDataFromSentryEvent дополнен redactSensitive по
event.extra/contexts — секреты в Sentry маскируются той же утилитой 8.7. Тесты:
logger-format 7 (обе ветки + маскировка) + sentry-scrub 4. 28/28 зелёных с регрессом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Smoke против публичного API @coopenomics/auth на трёх рантаймах: vault round-trip
(Argon2id+AES-GCM WebCrypto), signDocument→verifyDocumentOffline, WalletLocked,
типизированный not_implemented для login/getAccessToken (скелет Story 1.2). Node и
browser — vitest (общий smoke.test.ts), electron — main-process против собранного
dist (electron-main.cjs + run-electron.mjs c xvfb). Скрипты test:node/browser/
electron/cross-runtime + workflow sdk-cross-runtime.yaml. KDF один раз в beforeAll —
в chromium pure-JS Argon2id это минуты, per-test упирался в таймауты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RecoveryFinalizationPlaceholder (кидал 503) заменён на RecoveryFinalizationService.
Ротация active-ключа — существующим путём registrator::changekey (registrator владеет
аккаунтами пайщиков; подпись ключом кооператива из vault), без нового authentik-пути.
Порядок: новый vault-блоб → changekey → revokeAll сессий → audit KeyRotated
{trigger,old_pubkey,new_pubkey,initiator_id} (Story 8.4) → уведомление пайщику
SecurityEventKind.KeyRotated. vault сохраняется ДО on-chain переключения (новый
приватный ключ живёт только в блобе — иначе сбой залочил бы пайщика). Пароль authentik
в recovery НЕ трогается — это Эпик 5 (контроллер пока только читает authentik). Мультисиг
для self-recovery не нужен (подтверждён факторами пайщика); защита от единоличного захвата —
только force-recovery (6.9). Тесты: recovery-finalization 4 + регресс recovery-confirm 9 =
13/13. ESLint 0; coopback: Nest application successfully started, без DI-ошибок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сторона провайдера (IdP), потребители НЕ настраиваются (решение владельца:
«делаем только CoopID чтоб OIDC работал стандартно, потребителей накинем позже» —
это универсально).

- infra/coopid/authentik/blueprints/coopid-oidc-provider.yaml — декларативный
  blueprint: OAuth2/OpenID-провайдер + приложение `coopid`. client_type
  confidential, RS256 signing-key (5.1), PKCE S256 (5.2), per_provider issuer
  (5.4, per-coop через домен), sub_mode user_uuid, scope-mappings openid/email/
  profile + кастомный coop:verification_types (coopname+типы верификации через
  /userinfo и id_token).
- infra/coopid/caddy/Caddyfile — rewrite корневого /.well-known/openid-configuration
  и /.well-known/jwks.json на authentik app-эндпоинты: RP получают стандартный
  корневой discovery, Host сохраняется → issuer/endpoints строятся по домену коопа.
- infra/coopid/scripts/coopid-oidc-smoke.sh — smoke против поднятого стека:
  все required-поля discovery, PKCE S256, RS256 jwks, алиас jwks.json.

Live-валидация: стек поднят, blueprint применился (статус successful, провайдер+
приложение+scope в БД), smoke через caddy зелёный (issuer/authorize/token/userinfo/
jwks/end-session/introspect/revoke + RS256). Стек возвращён в выключенное состояние.

Отклонения (честно): issuer = https://<домен>/application/o/coopid/ (authentik
всегда включает путь, корневой iss из AC 5.4 ломал бы валидацию у RP; per-coop —
доменом). 5.2 hard-block implicit/ROPC: authentik рекламирует их в discovery
глобально (это возможности сервера, не per-provider), per-client убрать нельзя;
провайдер confidential+code, безопасный путь обеспечен PKCE.

За рамками (потребительская часть/follow-up): 5.3 transport participant_certificate
ES256K, 5.5 COOPOS-верификация в SDK, 5.6 Gitea-тест, 5.7 полный conformance-suite,
8.3 аудит OIDC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Все OIDC-операции выполняет authentik — контроллер узнаёт о них только через
native-события, доставляемые webhook'ом на /coop/internal/authentik-events
(механизм Story 1.5, shared-токен constant-time). Расширяем его на OIDC.

- infra/coopid/authentik/blueprints/coopid-oidc-audit.yaml — event-matcher +
  notification-rule + policy-binding на каждое действие (login/logout/
  authorize_application/login_failed/suspicious_request) + webhook body-mapping
  (action/user/client_ip/app/created) + transport на тот же endpoint. Поля
  сверены со /blueprints/schema.json образа 2026.2.
- authentik-events.controller.ts — mapAuthentikEvent расширен: login→OidcLoginSuccess,
  logout→OidcLogout, authorize_application→OidcTokenIssued (семантические Oidc*);
  прочие native-события → Authentik<Action> (login_failed→AuthentikLoginFailed,
  suspicious_request→AuthentikSuspiciousRequest, failure-result). ip=client_ip,
  context={authentik_action,app,authentik_created}. policy_execution (Story 1.5)
  без изменений. Контекст проходит secret-blacklist аудита.
- authentik-events.controller.spec.ts — 10 кейсов (weak-password, Oidc*, Authentik*,
  null, secret-blacklist).

Доставка идёт по членам destination_group: пустая группа НЕ доставляет (транспорт
не вызывается), поэтому выделенная группа coopid-audit с неактивным сервис-членом
coopid-audit-sink — надёжная доставка webhook'ом без спама реальных админов.

Live-валидация (стек поднят, потом потушен): blueprint применился (successful,
объекты+членство в БД); прямой webhook login→OidcLoginSuccess и login_failed→
AuthentikLoginFailed записались в audit_events; РЕАЛЬНЫЙ login_failed в authentik
прошёл всю цепочку (event→rule→webhook по docker-сети→контроллер→audit_events:
AuthentikLoginFailed, ip, user=akadmin). Unit 10/10, ESLint 0.

Отклонение (честно): OidcTokenRevoked/refresh не подключены — в authentik 2026.2 нет
надёжного native-action для отзыва/refresh OAuth-токена (enum eventmatcher не
содержит); endpoint'ы revoke/introspect работают, их аудит — отдельный механизм
при подключении потребителей. Отложено.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Реализация решения владельца 2026-06-13 по #16. Только бэкенд — страницу
«Персонал» стола совета сейчас не делаем (явное указание), готовим всё нужное
для управления этими правами. Движок CASL не трогаем: добавлен один ИСТОЧНИК
правил. AbilityFactory.createForParticipantWithRules теперь собирает Ability из
coreRoles (+персональные гранты) ∪ активных наборов пайщика.

- migrations/V2.4.12 — capability_sets (реестр шаблонов set_key/title/builtin/
  coopname) + participant_capability_sets (назначение, UNIQUE username+set_key,
  revoked_at/expires_at). Правила набора — в СУЩЕСТВУЮЩЕЙ access_rules с новым
  subject_type='capability_set' (переиспользует allow/deny/conditions/TTL/Redis-
  инвалидацию). Seed: accountant (read AccountingDesk — стол бухгалтера уже есть)
  + cashier (read+confirm PaymentRegistry — реестр платежей, гранулярно не manage).
- capability-sets.port + access-rules.port (+AccessRulePrincipalKind.CapabilitySet,
  +findForCapabilitySets). PostgresCapabilitySetsRepository (lazy DS; assign=
  ON CONFLICT DO UPDATE идемпотентно оживляет отозванный; revoke=UPDATE RETURNING).
- CapabilitySetService (assign/revoke/listSets/listForParticipant + валидация
  набора + аудит CapabilitySetAssigned/Revoked + инвалидация по пайщику).
- CapabilitySetController coop/capability-sets под @CheckAbility('read'|'manage',
  'CapabilitySet') + HttpJwtAuthGuard+AuthorizationGuard. Chairman L1 +manage
  CapabilitySet. ability.types +CapabilitySet +AccountingDesk/PaymentRegistry.

Различие осей: назначаемые роли (accountant/cashier/auditor — выдаёт председатель)
≠ вычисляемые (оператор ПВЗ/председатель КУ — выводятся из контекста на своих
столах, в этот субстрат не входят).

Self-review: unit 27 зелёных (ability.factory +2 set-merge, capability-set.service
5, policy регресс); ESLint 0; SQL миграции + запросы репозитория + join
AbilityFactory проверены на реальном postgres:18 (идемпотентность seed, цикл
назначение→merge(cashier read+confirm)→revoke RETURNING→0 активных, кириллица).

Отложено честно: полный boot coopback (DI runtime) + migration:run-в-стеке —
CoopID-стек был выключен, поднимать полный backend на машине с 3 рабочими
coopback'ами непропорционально идемпотентной DDL; DI-проводка сверена чтением
(идентична рабочей ACCESS_RULES_REPOSITORY), SQL доказан на PG → следующий
подъём/CI. Имена grant-субъектов столов (AccountingDesk/PaymentRegistry) — на
согласование при разводке desktop-gating. Разблокирует модель 6.6/6.7, питает
Эпик 10 (Story 10.4: ключ = ещё один принципал того же субстрата).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Фронт-интеграция назначаемых наборов возможностей по канону desktop. auth-v2
(CoopID) endpoints зовём по REST (sendGET/sendPOST, как coop/certificate) —
codegen/Zeus не нужен; гейтинг прав на guard'е бэкенда (@CheckAbility manage
CapabilitySet).

Бэкенд:
- capability-set.service.listSets теперь обогащает каждый набор его грантами
  (action+resource из access_rules) — UI показывает «эта роль открывает …».
- AccessController GET /coop/access/me + service.getMyAccess: эффективный доступ
  пайщика (активные наборы + плоские allow-гранты из собранной Ability) — это
  ОСНОВАНИЕ гейтинга столов/страниц по выданным правам. Та же модель
  resource:action, что и grants marketplace2 (CoopID-side seam, при мердже
  сводятся, провайдер не дублируем).
- порт: AccessGrant / CapabilitySetWithGrants / ParticipantAccess.

Фронт (components/desktop):
- features/Personnel (api REST + model useCapabilitySets): каталог наборов,
  назначения пайщика, назначить/снять.
- shared/lib/access/useCoopAccess: singleton-композабл, GET /coop/access/me +
  can(action,resource)/hasSet — столы/страницы консультируются для видимости.
- pages/Cooperative/Personnel: страница «Персонал» (канон — q-table :grid,
  Base*-компоненты, токены --p-*): таблица пайщиков + диалог управления ролями
  (chips назначенных + селект добавления + показ что роль открывает).
- extensions/soviet/install.ts: маршрут personnel на Столе Совета,
  meta.roles=['chairman'].

Self-review: бэкенд unit 6/6 (listSets-гранты + getMyAccess добавлены), ESLint 0
бэкенд+фронт. Вёрстку визуально НЕ самопроверял — жду скриншот (канон петли).

Архитектурное (честно): полный механизм grants (getDesktop.grants + провайдер)
и стол бухгалтера живут на ветке marketplace2, не на dev/coopid — здесь видимость
столов по meta.roles. Поэтому «набор → автопоказ стола бухгалтера» не вшит (стола
тут нет); заложено ОСНОВАНИЕ (useCoopAccess.can), которым стол/страница гейтятся,
и которое сводится с grants marketplace2 при мердже. Страница «Персонал» и выдача
ролей полностью рабочие и проверяемы (назначить «Бухгалтер» пайщику → запись +
аудит + его /coop/access/me содержит read AccountingDesk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Решение владельца: SDK — единственный типизированный фасад фронта; нового способа
взаимодействия с бэкендом на фронте появляться не должно, bearer-токен живёт только в SDK.
auth-v2 как параллельный REST-контракт, вызываемый напрямую с фронта, отвергнут.

- Бэкенд: AuthorizationResolver (capability-sets/access) + CertificateResolver поверх тех же
  сервисов; AuthorizationGuard/@CheckAbility уже GraphQL-aware. Удалены REST-контроллеры
  capability-set/access/certificate. DTO snake_case, резолвер маппит camelCase→snake_case.
- SDK: Queries.Authorization.*/Mutations.Authorization.*/Queries.Certificate.getMyCertificate
  + селекторы; codegen прогнан, Zeus-клиент закоммичен.
- Фронт: Personnel(api/model)+useCoopAccess+ProfilePage(api) на client.Query/Mutation, типы из
  SDK; прямого /coop/* REST на desktop не осталось.
- Проверки: SDL (новые типы/операции), SDK build+tsc+ESLint 0, unit 12/12, ESLint 0 везде.

Фазы 2/3 (остальной auth-v2 CRUD → GraphQL; SDK логин-фасад) — отдельными задачами.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корзина C Фазы 2: sessions/2fa/recovery-strategy/security-not-me →
AccountSecurityResolver (9 операций, GqlJwtAuthGuard), а
critical-actions/keys/force-recovery → CriticalActionsResolver (6 операций,
тот же CASL @CheckAbility+AuthorizationGuard). Движок Эпика 6 не тронут —
резолверы поверх тех же сервисов. 5 REST-контролёров сняты целиком, 2 урезаны
до magic-link :token (корзина D). SDK-домены AccountSecurity/CriticalActions +
codegen (SDL 62 резолвера) + Zeus закоммичен. Транспорт (IP/refresh-токен) —
request-meta декораторы, не GraphQL-переменные. Unit 24/24, ESLint 0.

Зачем: наружу на фронт смотрит только @coopenomics/sdk — нового способа
взаимодействия с бэкендом не появляется, bearer живёт только в SDK. OIDC
(.well-known), webhook (coop/internal) и login-контур (Фаза 3) остаются REST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D1: фасад в @coopenomics/auth (handshake/токены/recover), @coopenomics/sdk оборачивает
через Client.setAccessTokenProvider (bearer в слое SDK, без импорта auth — защита Node-потребителей).
D2: /coop/session/bind отдаёт binding_token в теле (+ httpOnly-cookie как fallback).
Бэк: новый REST /coop/refresh (та же токен-машинерия, что и legacy GraphQL-refresh).
Инвариант равноправия токенов закреплён token-coexistence.spec (оба контура — один
generateAuthTokens/config.jwt.secret/guard, без маркера контура). Authorization Code + PKCE (FR29),
ROPC запрещён. Тесты: auth vitest 16/16, controller jest 6/6, tsc(auth+sdk) EXIT0, ESLint 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 11.7 (фундамент). Подключает SDK @coopenomics/auth к desktop и
конфигурирует контур CoopID на старте, оставаясь чисто аддитивным:

- @coopenomics/auth добавлен в зависимости desktop (+ pnpm-lock).
- boot/coopid.ts (только клиент): configureCoopId(apiUrl=BACKEND_URL) всегда
  (нужно миграции/vault/recovery без OIDC-клиента); setAccessTokenProvider
  безусловно (при легаси-сессии getAccessToken бросает → SDK откатывается на
  legacy-bearer из client.setToken — инвариант «легаси-токены живут до логаута»
  сохраняется конструктивно); configureOidc под env-гейтом COOPID_ISSUER+CLIENT_ID.
- env COOPID_ISSUER/COOPID_CLIENT_ID (опциональны) в Environment + createEnvObject;
  пока не заданы — desktop остаётся на легаси-входе по ключу.
- boot 'coopid' зарегистрирован до 'init' (провайдер выставлен до первых запросов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 11.6 (логическая часть, без вёрстки). Не-визуальный, аддитивный seam,
который вёрстка LoginForm/мастера потом просто наденет:

- looksLikeWif(value) в shared/lib/utils — авторитетный детект ключа через
  WharfKit-парсер (5…/PVT_K1_…, отсекает пароли). Триггер «вставили ключ →
  предложить миграцию», а не вход ключом как раньше.
- useLoginUser().migrateAndLogin({email, privateKey, newPassword}): SDK migrate()
  (Story 11.4 — подпись против COOPOS + set_password authentik + шифр ключа
  паролём в server-vault) → затем легаси-вход тем же ключом. Пайщик переходит на
  пароль и СРАЗУ остаётся в системе, без потери доступа и без зависимости от
  готовности OIDC-инфраструктуры authentik. Легаси login(email,wif) не тронут.

Вёрстка (LoginForm email+пароль, мастер, баннер) и вход-по-паролю (нужен
публичный OIDC-клиент authentik + резолв account из сессии) — отдельным заходом
с визуальной проверкой/инфрой.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Чистый unit без бэкенда: мок fetch проверяет, что при брошенном accessTokenProvider
(нет CoopID-сессии) SDK отправляет легаси-bearer из setToken, а при успешном провайдере
— CoopID-токен. Доказывает D1/Эпик 7: провайдер можно ставить безусловно, не ломая
действующих пайщиков.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Удалён wallet/pin.ts и вся обвязка: persistPin/unlockWithPin в wallet, pinStorage
в logout, публичные экспорты unlockWithPin/clearPinProtected, PIN-тесты в
wallet/logout. StorageAdapter (локальная копия vault'а 11.3) и крипто-ядро
encryptWithPassword остаются. Решение «без PIN» зафиксировано в архитектуре;
StorageAdapter был заранее вынесен из pin.ts в 11.3 ради этого снятия.

Desktop PIN не использовал (проверено) — публичная поверхность для desktop
(configureCoopId/getAccessToken/migrate/configureOidc) не затронута. tsc 0,
vitest wallet+logout 8/8. Пред-существующие lint-ошибки encrypt.ts/wallet.test.ts
(import-sort/brace-style/lowercase-title из ранних коммитов) не трогал — вне scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 12.1 (Эпик 12, CoopID-восстановление). RecoveryFinalizationService раньше
молча игнорировал новый пароль (помечено «Эпик 5»): после recovery vault уже
зашифрован новым паролём, а authentik помнил старый → пайщик не мог войти ни
старым (vault не расшифровать), ни новым (authentik отвергает) паролём. Теперь
финализация пишет пароль через admin set_password (порт из 11.1, тот же, что
использует миграция 11.4).

Порядок записей setPassword → vault.store → changekey выбран по матрице частичных
сбоев трёх независимых хранилищ (authentik / vault-БД / on-chain): запись во
внешний IdP — самый вероятный отказ (недоступность, политика пароля), поэтому
идёт ПЕРВОЙ — её сбой не трогает vault и цепь, пайщик остаётся на старых кредах и
чисто повторяет восстановление. vault — ДО changekey (новый приватный ключ живёт
только в блобе, on-chain переключение коммитит его последним и ретраится).

findUserPk + guard: учётка authentik в recovery обязана существовать (recovery
требует включённого TOTP, а TOTP — authenticator authentik); null → защитный
throw (рассинхрон состояния), молча не создаём (нет email-контекста). Пароль
прозрачно уходит в authentik (единственный store паролей), не логируется/не
хранится на стороне controller'а; регресс-тест проверяет, что он не попадает в
аудит KeyRotated.

Тесты: 7/7 (вкл. порядок, null-guard, сбой setPassword=откат, без утечки пароля),
ESLint 0, тип-чек через ts-jest (полная типизация). DI байт-в-байт как MigrationService.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Был notImplemented-stub. Теперь полный confirm-флоу + повторный вход: генерация
новой пары (старый ключ при восстановлении утрачен), шифрование приватного новым
паролём в vault (AAD=субъект, наружу не уходит), POST /coop/recovery/confirm
{token, TOTP, public_key, vault, password} → сервер (12.1) ставит пароль в
authentik, сохраняет vault, ротирует active-ключ и отзывает сессии; затем authentik
новым паролём → unlockWallet (round-trip нового блоба) → timestamp-handshake.
Зачем: без этого фронт-recovery (12.3) нечем подтверждать — пайщик не мог
завершить восстановление и войти под новым ключом/паролём.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Точечные права с TTL (expires_at) при истечении уже инертны — read-path
findForPrincipal/findForCapabilitySets исключает их (expires_at <= now), доступа
они не дают. Но мёртвые строки копились вечно. Добавлен AccessRulesCleanupService
(@Cron ежедневно, прецедент CriticalActionsService.expireStale) + порт-метод
deleteExpired + DELETE ... WHERE expires_at IS NOT NULL AND expires_at <= now
RETURNING (детерминированный подсчёт, как в capability-sets). Удаление != отзыв:
ничьи фактические права не меняются → без инвалидации сессий и аудита.
Зачем: завершает TTL-фичу (6.7) — таблица прав не растёт бесконечно, выборка
прав не деградирует; безопасный не-визуальный бэкенд-слайс заблокированной 6.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willsoto/nestjs-prometheus + prom-client → GET /metrics в exposition-формате
и процессные метрики Node на глобальном реестре. AuthMetricsService даёт
доменные счётчики auth_login_attempts_total / auth_login_success_total /
auth_errors_total{contour,error_code} (success_rate = производное PromQL,
связка для alert Story 7.12). Провязка в единой login-границе verify-timestamp:
попытка/успех/ошибка по типизированному коду, side-effect-only, вход не валит.
Cross-cutting части AC (HTTP-latency-интерсептор, Redis/PG-gauge) отложены —
не вшиваю в общий app вслепую. Тесты auth-metrics 6 + verify-timestamp 28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coopname-scoped magic-link URL :coopname/auth/recover/:token (как invite), новый
feature RecoverAccess + widget/page (канон AuthCard/OtpInput/Base*), вход 'Потеряли
ключ?' ведёт на CoopID-recover. Полный вход в приложение после recovery упирается в
мост подписи CoopID (session.init строит wharfkit-Session из globalStore.wif) — 11.5/Эпик-7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Первый кирпич моста подписи CoopID: тот же паттерн, что у signDocument/signTimestamp —
ключ берётся через пакет-внутреннюю readUnlockedKey(), наружу уходит только SIG_K1_.
Десктопный WalletPluginCoopId (следующий шаг) делегирует сюда wharfkit Session.sign,
чтобы транзакции подписывались без выдачи WIF (как Ledger/Anchor-плагины).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Супердит «без PIN» из Story 11.8. Двухуровневая защита: серверный vault шифруется
паролём (расшифровка один раз при входе), локальный кэш — ПИН тем же
Argon2id+AES-256-GCM (pin.ts: savePinProtected/loadPinProtected/hasPinProtected/
clearPinProtected, AAD pin|<account>). Обвязка в wallet: persistPinCache (после
входа), unlockWithPin (reload/авто-лок без пароля), hasPinCache/clearPinCache;
DEFAULT_PIN='000000' делает разблокировку прозрачной. Модель угроз: ПИН — анти-
«дурак», от кражи блоба защищает пароль. Тесты pin.test.ts 7/7, tsc 0, eslint 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalletPluginCoopId (AbstractWalletPlugin) считает signing-дайджест и делегирует
подпись в @coopenomics/auth.signChainDigest — приватный ключ из keystore не
выходит, как у Ledger/Anchor. session-store: establishCoopIdSession строит
wharfkit Session поверх keystore (без globalStore.wif); ensureUnlocked — единая
точка авто-unlock по PIN-кэшу перед каждой подписью; авто-лок RAM 30 мин
(скользящий); username/isAuth fallback на CoopID-аккаунт; close затирает ключ и
PIN-кэш. session.init: ветка CoopID после легаси — строго аддитивно, при
отсутствии CoopID-кэша no-op, легаси-путь байт-в-байт не изменён. StorageAdapter
поверх IndexedDB (createCoopIdStorage) + deleteFromIndexedDB. ESLint 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coopenomics/auth: configureTokenStorage + restoreSession — пара токенов сессии
персистится в StorageAdapter (frontend — IndexedDB) и восстанавливается на boot;
setSession пишет копию, refresh её обновляет, clearSession стирает (logout). Без
этого CoopID-сессия теряла токен на F5 и была слабее легаси. Тесты oidc-tokens
10/10 (персист/restore/refresh-update/clear), tsc 0, ESLint 0, dist пересобран.

Desktop: session.init восстанавливает токены CoopID (configureTokenStorage+
restoreSession) перед establishCoopIdSession — сессия переживает reload (токены из
IndexedDB + ключ из PIN-кэша). Recovery — первый потребитель моста: confirmRecovery
строит CoopID-сессию поверх keystore (у восстановленного пайщика легаси-WIF нет) +
PIN-кэш; RecoverConfirm ведёт по каноническому boot-пути на рабочий стол вместо
тупикового signin (который требует WIF). ESLint 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-06-15 15:04:31 +00:00
merge: dev → coopID + регенерация SDK-клиента и фиксы typecheck CI
SDK cross-runtime / cross-runtime (pull_request) Failing after 2m43s
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
da08e22686
Слияние dev в консолидированную ветку CoopID (PR #148). Разрешение 5 конфликтов кодогена: 4 zeus-файла (controller/zeus + sdk/src/zeus) перегенерированы через generate-schema+generate-client из union-схемы; sdk/src/selectors/index.ts — union (criticalActions + expense). Фиксы красного CI:
- controller typecheck: добавлен deleteExpired в моки IAccessRulesRepository (ability.factory/capability-set.service/policy.service spec) — интерфейс получил метод в истории cron-уборки access_rules, моки отстали.
- desktop vue-tsc: store.ts — типизирован wallet (Awaited<ReturnType<unlockWithPin>>); DeletePaymentMethodButton PropType-фикс пришёл из dev.
- SDK cross-runtime: pnpm 9.15.9 → 10.33.0 в sdk-cross-runtime.yaml (mismatch с packageManager pnpm@10.33.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude closed this pull request 2026-06-15 15:05:00 +00:00
claude reopened this pull request 2026-06-15 15:05:01 +00:00
claude added 1 commit 2026-06-15 15:17:05 +00:00
ci: electron-зависимости (GTK/ATK/ALSA) в sdk-cross-runtime
SDK cross-runtime / cross-runtime (pull_request) Failing after 3m11s
Typecheck / desktop (pull_request) Successful in 14m4s
Typecheck / controller (pull_request) Has been cancelled
2a167ee458
После фикса pnpm-версии cross-runtime дошёл до test:electron и упал: electron не стартует без libgtk-3.so.0. playwright --with-deps chromium их не ставит (chromium на другом тулките). Добавлен поштучный apt-install GTK/ATK/ALSA-библиотек (noble t64-имена, || true чтобы расхождение имени не валило шаг). Node+browser прогоны cross-runtime уже зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude closed this pull request 2026-06-15 15:17:17 +00:00
claude reopened this pull request 2026-06-15 15:17:18 +00:00
claude closed this pull request 2026-06-15 15:41:29 +00:00
claude reopened this pull request 2026-06-15 15:41:30 +00:00
claude added 1 commit 2026-06-15 15:41:31 +00:00
test: синхронизировать electron-smoke с smoke.test.ts (login реализован)
SDK cross-runtime / cross-runtime (pull_request) Successful in 3m0s
Typecheck / desktop (pull_request) Successful in 14m16s
Typecheck / controller (pull_request) Successful in 13m44s
5714e3090b
electron-smoke ассертил login/getAccessToken → NotImplemented (старый скелет Story 1.2), но login реализован (flow-executor Story 11.2): в stub-окружении даёт AuthV2Error(network_error)/(wallet_locked). Node/browser smoke уже обновлены — ждут любой валидный код AuthV2ErrorCode. Привёл electron к тому же; конкретный код не пиннингуем. Это была единственная упавшая из 6 проверок electron.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some checks are pending
SDK cross-runtime / cross-runtime (pull_request) Successful in 3m0s
Typecheck / desktop (pull_request) Successful in 14m16s
Typecheck / controller (pull_request) Successful in 13m44s
Checking for merge conflicts…
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin coopID:coopID
git checkout coopID
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: C9S/mono#148