Commit Graph

3077 Commits

Author SHA1 Message Date
coopops a8882bdc2f [105-14][@ant] feat: подсказка про пароль при первом входе на экране инвайта — новый пайщик задаёт пароль единым менеджером миграции, ключ+пароль через один vault-путь без дублирования логики в мастере
SDK cross-runtime / cross-runtime (pull_request) Failing after 16s
Typecheck / desktop (pull_request) Failing after 9m57s
Typecheck / controller (pull_request) Failing after 8m32s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:45:08 +00:00
coopops 4a51f69a2f [105-14][@ant] feat: вход по паролю и встроенный менеджер миграции «ключ→пароль» в форме входа + карточка установки пароля в настройках — перевести действующих пайщиков на пароль без потери доступа, не ломая вход по ключу (Stories 11.5/11.6)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:42:11 +00:00
coopops 179c9eb803 [105-14][@ant] feat: PIN-код устройства на столе пайщика + запрос PIN при авто-локе и перезагрузке — дать пайщику необязательный барьер от посторонних поверх входа по паролю (уточнённая модель PIN Эпика 7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:35:32 +00:00
coopops 903926e34c [105-14][@ant] feat: страница «Настройки» на столе пайщика с управлением активными сессиями — дать пайщику видеть устройства входа и завершать чужие сессии без обращения в поддержку (Story 3.7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:27:58 +00:00
coopops 2575e9545d [105-10][@ant] feat: персистентность токенов CoopID + recovery как потребитель моста подписи (Эпик 7) — чтобы CoopID-сессия переживала перезагрузку не хуже легаси и восстановленный пайщик сразу входил
@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>
2026-06-15 08:43:50 +00:00
coopops d1d7410731 [105-10][@ant] feat: мост подписи CoopID на десктопе — WalletPluginCoopId + CoopID-сессия в session.init (Эпик 7) — чтобы подписывать он-чейн без извлечения ключа из keystore
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>
2026-06-15 08:07:07 +00:00
coopops 0ba0d4c382 [105-10][@ant] feat: вернуть PIN-кэш ключа в @coopenomics/auth (Эпик 7, уточнённая at-rest модель) — чтобы повторно не вводить пароль, а разблокировать локальный кэш ПИН
Супердит «без 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>
2026-06-15 07:52:47 +00:00
coopops c5085b2707 [105-10][@ant] feat: SDK signChainDigest — keystore подписывает дайджест транзакции, приватный ключ не покидает RAM-keystore (мост подписи CoopID, Эпик 7)
Первый кирпич моста подписи 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>
2026-06-15 07:07:37 +00:00
coopops ade9fe20b2 [105-15][@ant] feat: десктоп-восстановление доступа CoopID по magic-link — экран запроса и подтверждения (TOTP+новый пароль) поверх SDK loginWithMagicLink (Story 12.3)
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>
2026-06-15 05:23:14 +00:00
coopops fed20d5fba [105-15][@ant] feat: confirm восстановления отдаёт username и AAD vault'а делаю account-независимым — убрать лишний whoami-by-token и дать вход по magic-link без знания аккаунта заранее (Story 12.1/12.2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 05:09:52 +00:00
coopops 975cc509ae [105-12][@ant] feat: Prometheus /metrics endpoint и auth-метрики входа CoopID — наблюдаемость и основа алертов oidc (Story 9.11)
@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>
2026-06-14 23:09:53 +00:00
coopops d59dc7b796 [105-9][@ant] feat: cron-уборка истёкших access_rules — гигиена таблицы прав без влияния на доступ (Story 6.7)
Точечные права с 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>
2026-06-14 22:32:16 +00:00
coopops 4221ca707e [105-15][@ant] feat: реализовать loginWithMagicLink в SDK @coopenomics/auth — восстановление доступа по magic-link
Был 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>
2026-06-14 22:03:42 +00:00
coopops f240778162 [105-15][@ant] feat: писать новый пароль в authentik при восстановлении доступа — иначе пайщик залочен после recovery
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>
2026-06-14 21:34:04 +00:00
coopops 412a6d268a [105-14][@ant] refactor: снять PIN-слой из @coopenomics/auth (Story 11.8) — модель CoopID «без PIN», ключ в RAM только на сессию и стирается на логауте
Удалён 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>
2026-06-14 20:32:51 +00:00
coopops e02dae4ec7 [105-14][@ant] feat: добавить SDK unit-тест паритета легаси-токенов — зафиксировать инвариант FR29, что выданные access-токены работают до логаута при включённом CoopID
Чистый unit без бэкенда: мок fetch проверяет, что при брошенном accessTokenProvider
(нет CoopID-сессии) SDK отправляет легаси-bearer из setToken, а при успешном провайдере
— CoopID-токен. Доказывает D1/Эпик 7: провайдер можно ставить безусловно, не ломая
действующих пайщиков.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:59:34 +00:00
coopops ff50881fd0 [105-14][@ant] feat: desktop-логика миграции «ключ→пароль» и детект WIF — инфра-независимый seam для формы входа Эпика 11
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>
2026-06-14 19:28:33 +00:00
coopops 6c82544b80 [105-14][@ant] feat: boot-wiring контура CoopID на desktop — configureOidc/configureCoopId + access-token-provider, чтобы подключить вход по паролю не ломая легаси-токены
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>
2026-06-14 19:04:56 +00:00
coopops eaf4125f21 [105-14][@ant] feat: SDK migrate() и контракт username — подпись ключом + POST /coop/migration + saveToVault, чтобы пайщик задал пароль и зашифровал ключ за один шаг
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:36:06 +00:00
coopops 2e16e7d852 [105-14][@ant] feat: backend-эндпоинт миграции «ключ→пароль» — бессессионная проверка подписи против COOPOS + set_password authentik, чтобы действующие пайщики задали пароль без потери доступа
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:31:34 +00:00
coopops 0c68759455 [105-14][@ant] feat: SDK password-vault — POST /coop/vault, saveToVault и локальная копия шифроблоба — чтобы мигрировать ключ под пароль и входить офлайн без round-trip к узлу
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:02:31 +00:00
coopops 4fc6eaf986 [105-14][@ant] feat: встроенный фактор-1 входа через flow-executor authentik вместо signinPopup — чтобы клиент видел пароль для password-vault, не нарушая FR29 (грант остаётся code+PKCE)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:48:33 +00:00
coopops 6415db7085 [105-14][@ant] feat: добавить admin-API адаптер authentik (ensureUser/set_password) — разблокировать миграцию пайщиков «ключ→пароль» и установку пароля при восстановлении доступа
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:26:20 +00:00
coopops 1a97e1208c [105-10][@ant] feat: SDK CoopID-логин-фасад — handshake bind→sign→verify, lifecycle токенов и REST /coop/refresh — чтобы вход шёл по новому контуру при равноправии легаси-токенов (Эпик 7, фаза 3)
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>
2026-06-14 15:58:28 +00:00
coopops a7f1398529 [105-12][@ant] feat: устойчивость COOPOS RPC — пул с failover, finalized-only чтения ключей и M-of-N консенсус кэша — чтобы вход CoopID переживал падение/компрометацию узла без downtime (Stories 9.4/9.6/9.7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:33:18 +00:00
coopops f5bcaa3a69 [105-9][@ant] refactor: перевести account self-service auth-v2 с REST на GraphQL/SDK — единый типизированный фасад фронта
Корзина 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>
2026-06-14 06:22:15 +00:00
coopops d4a34bb067 [105-9][@ant] refactor: перевести capability-sets/access/certificate с REST на GraphQL/SDK — на фронт наружу должен смотреть только @coopenomics/sdk
Решение владельца: 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>
2026-06-13 15:26:40 +00:00
coopops 844725d660 [105-9][@ant] feat: добавить страницу «Персонал» и эндпоинт эффективного доступа — дать председателю выдавать пайщикам роли-наборы через UI и заложить основание гейтинга столов/страниц по правам (Story 6.11)
Фронт-интеграция назначаемых наборов возможностей по канону 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>
2026-06-13 13:10:14 +00:00
coopops 8d63baa53a [105-9][@ant] feat: добавить бэкенд назначаемых наборов возможностей (расширяемые роли) — дать председателю выдавать пайщикам роли бухгалтера/кассира поверх трёх базовых core-ролей (Story 6.11)
Реализация решения владельца 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>
2026-06-13 12:45:34 +00:00
coopops 1498be5213 [105-11][@ant] docs: перечислить реализованные OIDC-аудит-события в event-schema — синхронизировать схему событий с фактическим маппингом Story 8.3 для читателей кода
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:05:50 +00:00
coopops 6fbce27242 [105-11][@ant] feat: писать OIDC-операции и native-события authentik в audit_events через webhook — дать кооперативу единый аудит входов/выдач токенов для compliance и расследований (Story 8.3)
Все 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>
2026-06-13 10:05:09 +00:00
coopops 28c4164a61 [105-8][@ant] feat: поднять authentik OIDC-провайдер кооператива и корневой discovery — дать CoopID работать как стандартный OIDC-провайдер, чтобы любой внешний сервис подключался конфигом без правок кода (Story 5.1, 5.2, 5.4)
Сторона провайдера (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>
2026-06-13 09:27:01 +00:00
coopops 666672d43e [105-6][@ant] feat: подключить финализацию восстановления доступа через registrator::changekey и аудит KeyRotated — дать пайщику реально вернуть доступ ротацией ключа кооперативом вместо 503-заглушки (Story 3.3, 8.4)
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>
2026-06-13 08:25:29 +00:00
coopops 865b5eb550 [105-12][@ant] test: добавить cross-runtime smoke-тесты SDK auth для Node, браузера и electron + CI-гейт — не дать релизу SDK сломать клиентов ни в одном рантайме (Story 9.13)
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>
2026-06-13 05:43:22 +00:00
coopops 50ac2bffc3 [105-12][@ant] feat: структурировать логи в JSON для prod и распространить redaction 8.7 на путь в Sentry — дать агрегируемые stdout-логи без секретов/ПДн (NFR, 152-ФЗ, Story 9.12)
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>
2026-06-11 10:31:25 +00:00
coopops e14e34a36e [105-11][@ant] test: добавить тест чистоты логов и сканер утечек секретов, исправить порядок форматов logger — доказать что в test-run логи не содержат ПДн/секретов (NFR9, 152-ФЗ, Story 8.8)
Сканер 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>
2026-06-11 09:56:02 +00:00
coopops 913153285b [105-11][@ant] feat: добавить лог-санитайзер Winston и ESLint-запрет no-sensitive-in-log — чтобы секреты не утекали в production-логи даже при ошибке разработчика (NFR9, 152-ФЗ, Story 8.7)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:56:11 +00:00
coopops 95ec869f79 [105-11][@ant] feat: добавить авто-аудит admin-действий через @AuditAction interceptor — исключить пропуск admin-события из-за человеческого фактора
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>
2026-06-11 08:36:53 +00:00
coopops c0b7da9f40 [105-11][@ant] feat: структурировать audit-поля с user_agent и каноном схемы — обеспечить полный форензик-контекст 152-ФЗ
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>
2026-06-11 08:22:13 +00:00
coopops 0fd5d098be [105-11][@ant] feat: добавить миграцию audit_events (user_agent + read-only роль) и починить порядок access_rules — закрыть схему аудита 152-ФЗ для Story 8.1
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>
2026-06-11 07:57:49 +00:00
coopops 323e1e94a9 [105-7][@ant] feat: добавить ручной отзыв скомпрометированного ключа пайщика председателем — чтобы пресечь использование ключа по сообщению о компрометации
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>
2026-06-11 07:21:47 +00:00
coopops bd4839b67f [105-9][@ant] feat: добавить полную атрибуцию критических действий и audit-trail пайщика — чтобы контролирующий орган мог расследовать злоупотребления
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>
2026-06-11 06:57:06 +00:00
coopops 5eb7db9bf3 [105-9][@ant] feat: запретить единоличный force-recovery без согласия пайщика или решения собрания — защита от враждебного захвата аккаунта председателем
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>
2026-06-11 06:47:28 +00:00
coopops 35bfe887bb [105-9][@ant] feat: добавить сервис multi-party критических действий с двумя подписями — чтобы исключение пайщика, смена ролей совета и force-recovery не выполнялись одним человеком в одиночку
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>
2026-06-11 06:33:13 +00:00
coopops d7ccbda283 [105-9][@ant] feat: запретить @AuthRoles в auth-v2 через ESLint-правило no-authroles-in-authv2 — чтобы новый контур авторизации не откатывался на роле-ориентированный подход вместо capability-ориентированного @CheckAbility
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>
2026-06-11 06:19:46 +00:00
coopops 3d98728694 [105-9][@ant] feat: добавить единый AuthorizationGuard и PolicyService для четырёх слоёв CASL — чтобы вся авторизация auth-v2 проходила через один детерминированный контур без дублирующей логики
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>
2026-06-11 06:11:12 +00:00
coopops 6e66c5c086 [105-9][@ant] feat: добавить CASL Layer 3 — реестр политик с DB-доступом и фикс схемы access_rules — чтобы сложная авторизация (голосование только в своём кооперативе) решалась через runtime DB-lookup вне статической матрицы
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>
2026-06-11 06:03:17 +00:00
coopops d6f12ed7e9 [105-9][@ant] feat: CASL Layer 2 access_rules с merge поверх статики и Redis-инвалидацией — давать точечные права декларативно из БД без правки кода
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 05:33:03 +00:00
coopops 1a4876a0cd [105-9][@ant] feat: реальный @casl/ability-фундамент авторизации и Layer 1 static ability — переиспользовать зачаток marketplace2 и доделать капабилити-модель платформы
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 05:22:06 +00:00
coopops 5ed8a63839 [105-7][@ant] feat: политика версий схемы удостоверения через well-known + сверка в verifyOffline — отвергать офлайн устаревшие схемы claims, не принимая их как валидные
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 04:52:24 +00:00