UInt128 из re-export'а @wharfkit/session перестал совпадать по типам с
API.v1.TableIndexType, а UInt128.from не имеет перегрузки под bigint
(возврат unknown) — dev-запуск controller падал TS2345.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
После обновления pnpm-lock (типы для orchestrator) приехал @types/bn.js,
и UInt64.value стал типизированным BN — BigInt(BN) перестал собираться,
dev-запуск controller падал TS2345. Конвертация через десятичную строку.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tenant JWT ca-auth живёт 30 минут — статический APPS_CATALOG_TENANT_JWT
умирал через полчаса после деплоя, и subscribe mutation навсегда падала
401. Теперь coopback сам выпускает токен (challenge → подпись
coopname@active → verify), кэширует до exp с минутным запасом и
обновляет автоматически. Статический env-токен остаётся как
отладочный override.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WatcherModule инжектит install-pipeline в OnChainWatcherService, но
модуль провайдера его не экспортировал — boot падал DI-резолвом
(скрытый баг E10-5b: с полным federation-профилем orchestrator ни разу
не стартовал).
- buildSubgraphSchema (federation v2) строго валидирует SDL: @auth из
AuthRoles-декоратора без объявления валит boot coopback'а
('Unknown directive "@auth"' x100). Объявление через
buildSchemaOptions.directives — transformSchema продолжает работать.
- orchestrator не компилился ts-node-dev'ом: elliptic/bs58 без типов
(TS7016). @types/bs58 строго ^4 — v5-пакет это deprecation-пустышка
под bs58@5, который шипит собственные типы.
coopback: ORCHESTRATOR_URL + методы listInstalledFrontends /
fetchInstalledInstallScript; GET /v1/apps-catalog/installed-frontends
(JWT); install/:scope/:name переключён на volume-кэш orchestrator'а с
проброской X-Install-Script-Sha256/X-Package-Version (fallback на прямой
ca-admin только без ORCHESTRATOR_URL — degraded dev). desktop
remote-loader: список remote-расширений — installed-frontends (реально
установленные по подписке), sha256 install.js сверяется через
crypto.subtle перед eval.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
install.js активного релиза качается с ca-admin при установке/обновлении
пакета (watcher-события подписки/релиза), проверяется по sha256
(заголовок ca-admin + независимая сверка с coopenomics.frontend.installSha256
из манифеста в ca-auth registry, fail-closed) и складывается в volume.
Endpoints /v1/internal/extensions/frontend{,/:scope/:name/install.js} —
источник для coopback-прокси (E12-3): desktop получает только фронты
реально установленных у кооператива пакетов.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ChainRpcAppsEventStream: поллинг таблиц apps-контракта (releases/subs/
packages) через nodeos get_table_rows, синтез событий из диффа снапшотов;
маппинг on-chain package_id (eosio::name) → npm @scope/name через
packages.package_name; initial-снапшот доводит стенд до chain-состояния;
- CaAuthReleaseMetadata: install-spec из npm-манифеста CA-auth registry —
signed-request (канон X-Signature k1: + canonicalize) → per-package JWT
(выдаётся только при активной подписке) → coopenomics.backend секция;
тот же JWT идёт в docker login (подписка = единственный источник доступа);
- docker run-путь install pipeline'а: контейнер расширения поднимается
прямым docker run (имя/сеть/env из spec'а), без заранее описанного
compose-сервиса; rollback снимает контейнер;
- DI-проводка по env: CHAIN_RPC_URL / COOPERATIVE_WIF включают реальные
impl'ы, без них — Noop (dev-стенд стартует как раньше);
- compose: env-переменные watcher'а у сервиса orchestrator;
- 41 юнит-тест зелёный (8 новых на поллер, 6 на metadata/подпись)
- ExtensionsShowcase: Material-иконки вместо FontAwesome, BaseBanner/
BaseButton/BaseDialog/BaseBadge/EmptyState вместо сырых q-*, скелетон
вместо спиннера; «Подписаться» вызывает Mutation.subscribePackage
с разбором статусов (trial/active/clientNotRegistered/...);
- Опубликовать релиз: добавлен шаг 2 — Mutation.publishRelease (версия +
manifest с coopenomics-секцией + sha256), раньше мутация не вызывалась;
- новая страница «Модерация» (операторский approve/reject восхода) по
канону .table-wrap + BaseBadge + TableSkeleton + EmptyState;
- «Мои пакеты»/«Pricing»: реальные данные каталога вместо хардкод-карточек;
- init-installed-extensions: периодический идемпотентный re-pass — новый
пакет появляется в drawer'е без F5 (Architecture v3 шаг 18, вместо WS);
- иконки воркспейса разработчика переведены на Material
- PublishPackageStatus/PublishReleaseStatus через registerEnumType вместо
строковых литералов (канон: статусы — всегда enum);
- SDK: Mutations SubscribePackage/ApproveModeration/RejectModeration,
Query AppsCatalogPendingModerations, селектор moderationRequestSelector;
- ZEUS перегенерирован от полного merged-dev состояния схемы (закрывает
заявленный в PR долг: sdk на ветке не компилировался — schema отставала
от dev на registration/notifications/voteagainst)
Обычный docker compose up -d сервис НЕ поднимает — только явный
--profile federation. Socket-mount для shell-out docker pull/compose;
порт 4000 на localhost; doc-фикс примера CORE_SUBGRAPH_URL (2998)
- health-probe бьёт в <origin>/_health (канон extension-sdk), а не в GraphQL
endpoint, который на GET отвечает 400/405 — install всегда падал;
- oci-token-client выровнен с реальным контрактом ca-auth (Story 10.6):
GET /v2/auth/token?service=ca-auth&scope=repository:<scope>/<name>:pull,
Basic-auth с per-package JWT в password;
- docker pull через изолированный DOCKER_CONFIG + docker login (стандартный
WWW-Authenticate token flow) вместо несуществующего credential helper'а;
- watcher передаёт coopname в install pipeline (username для docker login)
- GraphQLModule.forRootAsync не видел SubgraphRegistryService из родительского
модуля (boot-crash Nest DI) — провайдеры вынесены в SubgraphRegistryModule;
- supergraph-manager подключён к gateway через supergraphSdl-hook: появление
нового subgraph'а в registry подхватывается без рестарта orchestrator'а;
- composer-impl: _service{sdl}-introspection + @apollo/composition;
- POST /v1/internal/composition/refresh теперь форсит немедленный recompose
(SupergraphRefreshService), а не no-op;
- имена subgraph'ов санитизируются для join__Graph enum'а
Story 10.3b — custom SupergraphManager, который на каждый poll-tick
re-читает subgraph registry и если список изменился — пересобирает
supergraph и вызывает gateway update(newSdl).
Раньше Apollo Gateway получал статический список subgraph'ов на
bootstrap'е через IntrospectAndCompose — добавление нового
расширения (Story 10.4 install pipeline) требовало рестарта контейнера.
Теперь watcher install pipeline → registry upsert → manager на
следующий tick видит новую запись → recompose → gateway swap'ает
supergraph in-memory. Без рестарта.
Идемпотентность через fingerprint (`name:url` sorted join):
- список не изменился → composer НЕ зовётся, ничего не делаем;
- список изменился → composer.compose() → update().
Скрытая ловушка: fingerprint обновляем только ПОСЛЕ успешного
compose'а — иначе exception в composer'е навсегда залипает в
устаревшем состоянии (следующий tick думает что состояние актуально).
Покрыто continuity-тестом.
7/7 unit-тестов покрывают: initial, idempotent tick, add/remove/url-change,
continuity на ошибке composer'а, cleanup. Используют jest fake timers.
Интеграция с Apollo Gateway (gateway.module.ts) — отдельный follow-up
(`feat/E10-3b-apollo-integration`), требует @apollo/composition impl
для SupergraphComposerPort.
(cherry picked from commit 6ac6a0319b)
Story 10.5 — orchestrator слушает события apps-contract и автоматически
синхронизирует установленные расширения с тем, что в блокчейне.
Обрабатываемые события (discriminated union по `kind`):
- `release-published` → scope check → fetch metadata → install pipeline;
- `subscription-activated` → fetch active release → install pipeline;
- `release-withdrawn` → registry.deactivate;
- `subscription-expired` → registry.deactivate (для нашего coopname).
Все три триггера install'а идут через единый InstallOrchestratorService
(idempotent, upsert в registry). Apollo Gateway за ≤10с подхватит
изменения polling'ом — supergraph рекомпозируется без рестарта.
Порты (AppsContractEventStreamPort, ReleaseMetadataPort) изолируют
watcher от реального chain-чтения. В этом PR — Noop-импленты в
WatcherModule + контракт; реальный parser2/RPC-impl поедет в
follow-up'е (`feat/E10-5b-chain-rpc-impl`).
11/11 unit-тестов покрывают все 4 типа событий + scope-фильтрацию
(all/cooperatives/subnets/empty) + coopname-фильтрацию по своему
кооперативу. Тесты гоняют сценарий через ин-мемори фейки портов
без реальной chain-ноды и Postgres.
(cherry picked from commit b926dc2a15)
Story 9.3.b-ui. Закрывает UI-петлю Эпика 9 «MVP demo-app pilot».
SDK (@coopenomics/sdk):
- mutations/extensions/publishPackage.ts — wrapper над Mutation.publishPackage,
селектор {status, requestId, error}.
- mutations/extensions/publishRelease.ts — wrapper над Mutation.publishRelease,
селектор {status, requestId, transactionId, error}. Готов для отдельной
страницы публикации релизов (следующая story 9.3.b-rel-ui).
- mutations/extensions/index.ts: добавлены PublishPackage / PublishRelease.
- Codegen (controller/sdk): pnpm generate-schema + generate-client + sdk build —
zeus подхватил PublishPackageInputDTO / PublishReleaseInputDTO.
Desktop:
- features/PublishPackage/{api,model}/index.ts — каноническая пара по mono-ai-N
FSD-конвенции: api тонко оборачивает client.Mutation, model задаёт
IPublishPackageInput = Mutations.Extensions.PublishPackage.IInput['data'] и
composable usePublishPackage (isSubmitting / lastResult / lastError / submit).
- pages/DeveloperPublishReleasePage.vue: форма из BaseForm + BaseInput
(packageId @scope/name + ownerUsername Antelope-name + compatibleSubnets
textarea с chain_id). Валидация регуляркой ДО отправки. BaseBanner с
warning если роль текущей сессии не chairman; кнопка submit disabled.
Зелёный banner при applied, оранжевый — при conflict. Канон BaseBanner
variant='warn|pos', BaseCard variant='default'. Session-данные читаются
через useSessionStore (pinia auto-unwrap, без .value).
Что не делалось:
- Форма publishRelease (manifest + sha256) — отдельная страница; mutation
в SDK уже готова под неё.
- Реальная привязка к списку «Мои пакеты» — пока кнопка отдельной страницы;
список и интеграция (post-success refresh) — последующая итерация.
(cherry picked from commit 1a0ac54f88)
Story 9.3.b-sub: пайщик в десктопе нажимает Подписаться в ExtensionsShowcase.
Backend проксирует это в ca-auth POST /v1/subscriptions/activate с tenant
JWT кооператива-партнёра. ca-auth активирует подписку (trial → active по
free_trial_period_seconds; pay-period после expired); on-chain
apps::regsub подписывает ca-auth от имени каталога-оператора. После
активации pricing-watcher на coopback партнёра подхватит подписку
(Story 9.6).
GraphQL:
- Mutation.subscribePackage(data: SubscribePackageInputDTO!)
→ SubscribePackageResultDTO. Discriminated outcome:
activated / alreadyActive (409) / clientNotRegistered (403) /
unavailable (423) / failed.
Защита: chairman кооператива-партнёра (GqlJwtAuthGuard + RolesGuard +
AuthRoles).
Config:
- APPS_CATALOG_AUTH_URL — base URL ca-auth (отдельный от admin).
- APPS_CATALOG_TENANT_JWT — JWT кооператива-партнёра.
Tenant читается ca-auth'ом ТОЛЬКО из JWT — body НЕ может его
переопределить (закрывает «активировать от имени другого tenant'а»).
На degraded-mode (нет AUTH_URL/TENANT_JWT) — mutation возвращает failed
с явной причиной, а не silent no-op.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(cherry picked from commit dbe349f742)
Корень бага «push приходит только на одно устройство»: autoSubscribe
гейтился store.isSubscribed (= у аккаунта есть подписка на ЛЮБОМ устройстве),
поэтому второе устройство никогда не подписывалось. Введён endpoint текущего
браузера + computed isThisDeviceSubscribed; autoSubscribe теперь идемпотентно
регистрирует каждое устройство. Добавлены resubscribe()/refreshDeviceState()
и строка статуса push с кнопкой «Включить/Переподписать» в виджете колокольчика.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Релей переехал в отдельный репозиторий C9S/email-relay, собирается локально
плейбуком (playbooks/email-relay) — релиз mono его больше не пересобирает.
Прод-образ mono-base (prune --prod) не подходил для standalone-сервиса.
Удалено: components/email-relay, build_service в release.yaml, dev-сервис в
docker-compose. Контроллерный relay-режим (EMAIL_RELAY_URL/TOKEN в
email-channel.adapter + config) ОСТАЁТСЯ — он и активирует отправку через релей.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-образ mono-base делает `pnpm prune --prod` (Dockerfile:74) — devDeps
вырезаются. ts-node был в devDependencies → в проде `sh: ts-node: not found`,
контейнер в restart-loop, /health не отвечал. controller держит ts-node в
dependencies — повторяем паттерн. (Коммит из закрытого PR #115 не попал в dev,
вношу прямо.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mailpit был только локальным SMTP-перехватчиком для теста — в поставке не нужен.
email-relay сам ходит в реальный SMTP через nodemailer; конфиг (RELAY_TOKEN,
SMTP_*) выносим в components/email-relay/.env по образцу coopback, без отдельного
контейнера и inline-параметров.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Хостинги коопов режут исходящий SMTP → письма controller'а уходят в timeout
(раньше хабом был Novu, его убрали). Новый компонент @coopenomics/email-relay
принимает письма по HTTPS с Bearer-токеном и форвардит по SMTP. Ставится на
сервер с открытыми портами (плейбук в playbooks/email-relay).
- components/email-relay: express + nodemailer, POST /send + GET /health,
Bearer-токен (constant-time), опц. IP-allowlist, конфиг из ENV
- controller: email-channel.adapter получил relay-режим — если задан
EMAIL_RELAY_URL, письмо уходит POST'ом на релей; иначе прямой SMTP как
раньше (opt-in, без регрессии)
- docker-compose: дев-сервисы email-relay + mailpit (SMTP-перехватчик для E2E)
- release.yaml: сборка образа dicoop/email-relay
E2E проверен локально: controller-формат письма → релей → SMTP → mailpit,
тело с <br> и заголовки целы; 401 на отсутствие/неверный токен, 400 на пустое тело.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В колоколе уведомлений на тёмной теме элементы сливались с фоном панели:
разделители на --p-line (0.06 white) почти невидимы, а непрочитанные
отличались лишь цветом текста + точкой — всё тонуло в --p-surface. На светлой
контраст тёмного текста на белом спасал.
Канон-токенами (обе темы, светлую не ухудшает): панели — рамка --p-line-1
для контура; разделители между уведомлениями — --p-line-1 (заметнее); у
непрочитанных подложка --p-primary-soft + левый акцент-бар --p-primary,
hover/focus — --p-primary-line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заменяет ненадёжный триггер от lifecycle service worker'а (не срабатывал
на iOS standalone PWA, при первой установке и когда бинарь SW не менялся)
на сравнение версий: запечённая в бандл package-версия (process.env.APP_VERSION)
сверяется с self-report SSR-ноды (/version). При расхождении — существующий тост.
- quasar.config: APP_VERSION из package.json в build.env; middleware 'version'
- src-ssr/middlewares/version.ts: self-report /version (JSON, no-store)
- entities/AppVersion: стор useUpdateWatch (реестр компонентов, сейчас shell),
опрос /version раз в 5 мин + при возврате на вкладку; SPA-safe (нет ноды -> no-op)
- init-app: старт version-watch рядом с systemMonitoring
- register-service-worker: убран SW-триггер; applyUpdate устойчив (waiting -> SKIP_WAITING, иначе reload)
- LeftDrawerMenu: версия приложения в футере рейла
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«исчезнет» → «исчезает»; убрано предложение про отзыв заявки на повестке —
кнопка доступна только когда аккаунт уже в цепи (after оплаты), решение совета
тут неприменимо.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прежний allow-list статусов исключал Registered — но именно туда попадают
отклонённые советом и оплаченные брошенные регистрации (отказ/возврат
оставляют статус registered; failed/refunded users.status никем не
выставляются). То есть чистка не работала для своей же цели «убрать
незавершённые/отклонённые регистрации (тестеры)».
- backend deleteAccount: убран allow-list по статусу, остаётся единственный
гард — НЕ принят в кооператив (нет participant_account).
- frontend isDeletable: !participant_account. Кнопка удаления теперь и у
Registered (оплачен/на рассмотрении совета/отклонён/возврат).
- confirm-диалог: для blockchain-registered (Registered) предупреждение, что
on-chain аккаунт неудаляем (имя в цепи занято навсегда) + про отзыв заявки
на повестке, если ещё на рассмотрении совета.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На мобиле (и в узком окне десктопа) canon-таблицы выходили за ширину: левая
колонка обрезалась, появлялся внутренний горизонтальный скролл — из-за
фиксированной min-width (780–1000px), которую виджеты ставят локально, а
скелетон inline-стилем.
Централизовал в каноне components.css: `.table { min-width: 0 !important }`
снимает форс-ширину на всех экранах (!important — иначе scoped/inline
перебивают канон), таблица всегда влезает (width:100%) и переносит содержимое
на следующую строку вместо скролла. На узких экранах (≤599px) дополнительно
table-layout:auto + перенос в ячейках + плотнее паддинги, чтобы многоколоночная
таблица читалась переносом, а не клиппингом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пользователю в тост летел сырой ассерт контракта, напр.
`walletop TRANSFER: недостаточно средств на кошельке w.wal.share`.
Ему не нужны ни scope-префикс операции, ни служебное имя кошелька — он
нажал конкретную кнопку и так понимает, о каком кошельке речь.
Добавлен универсальный санитайзер sanitizeBlockchainError, встроенный в
единую точку показа ошибок FailAlert:
- снимает ведущий технический scope-префикс (`walletop TRANSFER:`,
`ledger2::apply:`, `o.mkt.supply:`) — распознаёт идентификатор с
`.`/`::` либо слово+ALLCAPS-опкод, кириллицу не трогает;
- убирает служебные имена ledger2-кошельков (`w.wal.share` и т.п.);
- нормализует пробелы и ставит заглавную первую букву.
Результат для примера выше — «Недостаточно средств на кошельке».
Обычные (нетехнические) сообщения проходят без изменений.
Полный технический текст остаётся разработчикам: FailAlert дублирует
сырой message в console.error, бэкенд пишет его в логи контроллера.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при «Получить возврат» с суммой больше L3-баланса платёж писался в
gateway-БД (AWAITING_AUTHORIZATION) ДО on-chain транзакции. Контракт wallet
отклонял заявку (недостаточно L3-средств), а в разделе «Платежи» оставался
висеть фантомный исходящий платёж со статусом FAILED.
Фикс — порядок операций в WalletInteractor.createWithdraw:
1. prepareWithdraw — все валидации (символ, дубликат hash, платёжный метод)
и сборка записи, БЕЗ записи в БД;
2. on-chain wallet::createwthd — при недостатке средств падает здесь,
ни одной записи о платеже не создаётся;
3. persistWithdraw — фиксация платежа в БД только после успешной заявки.
GatewayInteractor.createWithdraw разбит на prepareWithdraw/persistWithdraw
(старый combined-метод сохранён как обёртка). Порт и оба адаптера обновлены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Программные оферты (program_id > 0) с Эпика 2 уходят на цепь через
wallet::signagree (commit 21e94ce0ed), а не soviet::sndagreement —
soviet::newagreement по ним не создаётся. Агрегатор пакета документов
(document-package-v1) искал приложения к заявлению только по
soviet::newagreement, поэтому подписанная оферта молча выпадала из links
и не отображалась как приложение к заявлению на повестке совета (при этом
сам документ виден в реестре).
Резолв каждого linkHash вынесен в общий хелпер buildLinkedAggregate;
носитель подписи ищется сначала в soviet::newagreement, затем фолбэком в
wallet::signagree. Унифицированы три дублировавшихся цикла (statement /
decision / acts). Добавлен регрессионный unit-тест.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In-app тело инбокса рендерится как текст ({{ n.description }}, без v-html —
payload несёт ФИО/заголовки, v-html = XSS-вектор), поэтому HTML-тег <br> из
шаблона показывался строкой «№C53E<br>От: …». Перевёл 2 in-app шаблона
(new-agenda-item, approval-request) с <br> на \n и добавил white-space:
pre-line на .notification-center__item-desc (line-clamp:2 сохранён).
Push не трогаю — его тела уже plain-text без тегов, отображается верно.
Email оставлен на HTML (<br>/<strong>) — рендерится почтовым клиентом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Временный shim очистки persisted-localStorage от устаревшего «КПП банка».
К 01.09.2026 старые незавершённые черновики регистрации истекут — утилиту
и оба вызова (CreateUser/AddUser model) можно удалить.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>