API ВЕГА
English

Бэкенд Beancount.io

Обзор

Бэкенд создан на Node.js и TypeScript с использованием Koa и GraphQL. Он обеспечивает работу API и фоновых задач Beancount.io. Основные возможности:

  • GraphQL API на базе Apollo Server и Koa

  • TypeScript для разработки с проверкой типов

  • PostgreSQL и Drizzle ORM для работы с базой данных

  • Redis для кэширования

  • Фоновые задачи для обработки данных

Сведения о требованиях к паролям, квотах API, восстановлении доступа по электронной почте и требованиях к развёртыванию см. в разделе Аутентификация и ограничения частоты запросов.

Явный ответ при регистрации о том, существует ли учётная запись, — осознанное продуктовое решение. Для частой автоматизации Git следует использовать SSH-ключи, а не передавать пароль учётной записи по HTTP снова и снова.

Оплата

Поддерживаемый клиент приложения — beancount-web-prod. Для его настройки используются переменные STRIPE_PRIVATE_KEY, STRIPE_PUBLIC_KEY и STRIPE_WEBHOOK_SECRET.

Все сеансы платёжного портала используют конфигурацию, заданную в SUBSCRIPTION_CONFIG. Она разрешает только управление способами оплаты, счетами и платёжными данными; смена тарифного плана выполняется через changePlan. При обработке платежей учитываются активные подписки и известные текущие или устаревшие цены. По старому периоду, сохранённому в кэше, или неизвестной цене нельзя определить приобретённый тарифный план. Обновлять кэш клиента могут только проверенные актуальные вебхуки. Сбои Stripe не приводят к удалению сохранённых периодов.

События GA4 purchase записывает панель управления в браузере после подтверждения платежа; бэкенд не отправляет данные аналитики. После успешной оплаты в Checkout пользователь возвращается на страницу подтверждения в панели управления (/settings/billing?checkout=success&session_id=…), а changePlan сообщает данные счёта за немедленное изменение тарифа (invoiceId, amountPaid, currency), чтобы панель управления учитывала оплату один раз.

Устаревший клиент beancount-web-dev и переменные STRIPE_DEV_* игнорируются; скрипт для имитации успешной оплаты не предусмотрен. Тестирование Stripe в изолированной среде следует проводить отдельно от платёжного процесса работающего приложения.

Структура проекта

backend-v2/
├── docs/           # API and operator guides
├── scripts/        # Deployment migrations and developer tools
├── src/
│   ├── config/     # Environment-backed application configuration
│   ├── drizzle/    # PostgreSQL setup and tracked migrations
│   ├── features/   # Domain adapters, workflows, services and models
│   ├── foundation/ # Composition, external clients and Redis
│   ├── scheduler/  # Scheduled jobs, including authentication mail
│   ├── server/     # Koa, GraphQL, REST and MCP transport setup
│   └── shared/     # Errors, caching, logging and utilities
├── server.ts       # Application entry point
└── package.json

Начало работы

Инструкция рассчитана на Unix-подобную среду. Если вы используете Windows, см. раздел «Запуск в Windows».

Необходимые компоненты

  • Node.js 20+

  • PostgreSQL

  • Redis

  • Git

Настройка

  • Клонируйте репозиторий:
git clone git@github.com:bex-co/beancount-io.git
cd beancount-io/backend-cluster/backend-v2
  • Установите зависимости:
yarn install
  • Настройте переменные окружения:

Создайте файл .env в каталоге бэкенда и укажите необходимые переменные окружения. Обязательные переменные:

POSTGRES_BACKEND_URI=postgresql://user:password@localhost:5432/beancount
REDIS_URI=redis://localhost:6379
FAVA_API_URL=http://localhost:8000
FAVA_API_ADMIN_USER=admin
FAVA_API_ADMIN_PASSWORD=your_password
AUTH_SECRET=replace_with_a_strong_shared_deployment_secret
SERVER_URL=http://localhost:4104
DASHBOARD_URL=http://localhost:5173
# Optional: login/register JWT lifetime in minutes (default 525600 = 365 days)
# AUTH_JWT_EXP_MINUTES=5
# Optional: reverse proxies in front of this server that append to
# X-Forwarded-For (default 1; 0 when clients connect directly)
# TRUSTED_PROXY_HOPS=1

Задайте для AUTH_SECRET непустое значение и используйте его без изменений во всех процессах API и worker, а также после перезапусков: этот секрет подписывает сеансы и используется для получения ключей шифрования писем аутентификации и подтверждения восстановления доступа. Никогда не добавляйте его реальное значение в репозиторий.

Перед запуском сервера примените миграции базы данных:

node --env-file=.env scripts/migrate-deploy.js
yarn start

В среде развёртывания для этого используется команда yarn migrate:deploy с уже заданными переменными окружения. Она включает миграцию очереди auth_mail: без неё API не сможет ставить письма для восстановления доступа в очередь. При обычном запуске сервера worker отправки писем аутентификации запускается автоматически и выполняет проверку каждые пять секунд.

В средах разработки и тестирования ConsoleSendGrid подавляет отправку и не выводит содержимое писем, адреса, ссылки или OTP. Для проверки электронной почты, регистрации и восстановления доступа человеком требуется полностью настроенная почтовая среда: в production-клиенте используются SEND_GRID_API_KEY и настроенный отправитель. Автоматические тесты перехватывают синтетические письма, не обращаясь к почтовому провайдеру. Требования к worker, Redis и восстановлению доступа см. в руководстве для операторов.

Для OAuth в production требуется OAUTH_JWKS — полный JWKS с закрытым ключом P-256/ES256, предоставленный менеджером секретов среды развёртывания. Его нельзя добавлять в репозиторий или копировать в .env.example. Если переменная отсутствует или содержит некорректное значение, конечные точки OAuth возвращают 503 oauth_not_configured; существующий API и вход по устаревшему JWT остаются доступны. В средах разработки и тестирования создаётся временный ключ, действующий только в рамках процесса.

Чтобы сменить ключ подписи OAuth, установите новый секрет, выполните развёртывание и удалите заменённый открытый ключ из JWKS, который отдаёт сервис. Удаление исторического закрытого ключа из Git без замены действующего принятого ключа не аннулирует токены, подписанные этим ключом.

Быстрый запуск REST API

Персональные токены доступа доступны на платных тарифах. Войдите в систему, откройте раздел Персональные токены доступа, создайте токен с минимально необходимыми для вашей интеграции правами и скопируйте его при создании. Позже получить исходное значение токена будет невозможно: храните его в менеджере секретов и никогда не добавляйте в репозиторий.

Для начала используйте безопасную конечную точку со списком реестров:

export BEANCOUNT_API_KEY="bcio_your_token_here"

curl --fail-with-body \
  --header "x-api-key: $BEANCOUNT_API_KEY" \
  https://api.v3.beancount.io/api-gateway/v1/ledgers

В интерактивной справке по API перечислены доступные конечные точки, схемы запросов и необходимые разрешения.

Условия развёртывания OAuth

В production DASHBOARD_URL — это общедоступный адрес сервера авторизации и начальная точка для страницы согласия. SERVER_URL — прямой адрес бэкенда, используемый локальным стеком разработки. Это обычные общедоступные параметры конфигурации, а не секреты OAuth. В production для обоих параметров по умолчанию используется официальный сервис; в среде разработки — локальные адреса бэкенда и панели управления. HTTPS обязателен, кроме случаев, когда среда не относится к production, а адрес — localhost, 127.0.0.1 или ::1. Если в production обратный прокси публикует Beancount по префиксу пути, укажите этот префикс в DASHBOARD_URL и не добавляйте в конце косую черту.

В локальном стеке для разных сервисов используются разные порты, но одинаковое имя хоста. В production обе службы обычно следует публиковать через одну общедоступную точку входа HTTPS.

Обнаружение начинается с URL по RFC 9728, сформированного на основе ресурса API. Для issuer https://books.example.test/beancount ресурс API и цепочка обнаружения выглядят так:

resource: https://books.example.test/beancount/v1
protected resource metadata:
  https://books.example.test/.well-known/oauth-protected-resource/beancount/v1
authorization server metadata:
  https://books.example.test/.well-known/oauth-authorization-server/beancount

Прокси должен направлять well-known-пути и конечные точки с префиксом issuer /api-gateway/oauth/* на этот бэкенд, а страницы взаимодействия — на соответствующие бэкенд и панель управления.

На хосте панели управления (APP_DOMAIN) путь /.well-known/* должен вести на этот бэкенд, чтобы Apple и Google могли получать подтверждения для ссылок на нативное приложение с того же origin, что и ссылки /ledger/....

Проверка учетных данных извне

Проверить учетные данные, выданные Beancount.io, можно двумя способами. Они отвечают на разные вопросы, поэтому выбирайте подходящий способ осознанно.

Офлайн, по опубликованным ключам. Токены доступа OAuth — это JWT с алгоритмом ES256 и полями iss, aud и kid. Открытые ключи доступны по адресу /api-gateway/oauth/jwks; найти этот адрес можно в документе RFC 8414, указанном выше. Поэтому любой стандартный валидатор, например createRemoteJWKSet из jose или фильтр JWT на API gateway, может проверить такой токен без обращения к нам.

Это подтверждает, что токен был выдан и срок его действия не истёк. Однако так нельзя узнать, не был ли отозван грант пять минут назад. Кроме того, этот способ вообще не подходит для двух других видов учетных данных: токены сессий используют HS256, а публикация ключа позволила бы создавать такие токены; API-ключи с префиксом bcio_ — не JWT, и у них нет подписи для проверки.

Онлайн, с запросом к серверу. POST /api-gateway/v1/token/introspect (RFC 7662) проверяет все три вида учетных данных, в том числе определяет, действительны ли они прямо сейчас:

curl -sS https://beancount.io/api-gateway/v1/token/introspect \
  -H 'x-api-key: bcio_your_own_key' \
  -H 'content-type: application/json' \
  -d '{"token":"bcio_the_key_you_are_checking"}'
{
  "active": true,
  "sub": "usr_7wXzK9mN",
  "scope": "ledger.read ledger.write",
  "jti": "akey_7wXzK9mN",
  "iat": 1767225600,
  "exp": 1798761600,
  "bio_credential_kind": "apikey",
  "bio_assurance": "delegated",
  "bio_ledger_scope": "alice/main-ledger"
}

Для учетных данных, которые нельзя использовать, сервер возвращает только этот ответ:

{ "active": false }

Учетные данные с истекшим сроком действия, некорректные, отозванные, никогда не выданные или принадлежащие другому пользователю приводят к одному и тому же ответу. Это сделано намеренно: если бы ответы различались, вызывающая сторона могла бы выяснить, какие учетные данные когда-либо были действительными и когда именно их отозвали.

Перед интеграцией учитывайте три особенности:

  • Анонимные запросы не поддерживаются. Для обращения к этому endpoint нужны собственные учетные данные. Иначе любой украденный токен можно было бы проверить и получить сведения о том, что он действителен, принадлежит пользователю X и дает право изменять ledger Y. Если вызывающая сторона не прошла аутентификацию, сервер возвращает 401 или 403, а не {"active": false}.

  • Проверять можно только собственные учетные данные. Токен другого пользователя будет признан неактивным. Поэтому endpoint подходит для gateway или среды выполнения агента, работающих от имени того же пользователя, но не является сервисом проверки учетных данных для нескольких арендаторов.

  • scope показывает фактические возможности, а не исходный грант. Токен сессии не ограничен scope и сообщает обо всех трех областях доступа к ledger. Не трактуйте grant, который выглядит пустым, как отсутствие разрешений.

Поля с префиксом bio_ добавлены нами и не входят в RFC 7662; остальные поля соответствуют стандартному документу. Та же операция доступна в GraphQL как Query.introspectToken.

Это намеренно не инструмент MCP, и в метаданных RFC 8414 операция не объявлена как introspection_endpoint. Причины описаны в ADR 0017.

Проверка работает и в развертывании, где OAuth никогда не настраивали: API-ключи и токены сессий по-прежнему распознаются, а для токена OAuth возвращается active: false, что в этом случае соответствует действительности.

Ссылки для нативных приложений

Необязательная настройка. Если она задана, сервер обрабатывает следующие запросы:

  • GET /.well-known/apple-app-site-association — Apple Universal Links для io.beancount.ios на /ledger/* (APP_LINKS_APPLE_TEAM_ID)

  • GET /.well-known/assetlinks.json — Android App Links для io.beancount.android (APP_LINKS_ANDROID_SHA256, список SHA-256-отпечатков сертификата Play App Signing, разделенных запятыми)

Если не задать одну из переменных, соответствующий маршрут вернет 404. Поэтому на собственном сервере без сборки приложения для магазина приложений ссылки на приложения Beancount.io не публикуются. В рабочем окружении Beancount.io задано APP_LINKS_APPLE_TEAM_ID=PTLM7BZQMM.

Подтверждение домена приложения OpenAI

GET /.well-known/openai-apps-challenge возвращает в формате text/plain токен, выданный панелью OpenAI Plugins для заявки приложения Beancount.io.

Токен не является секретным, поэтому он задан как константа в well-known-route.ts, а не как переменная окружения. Токен возвращается только в развертывании, где DASHBOARD_URL имеет значение https://beancount.io; во всех остальных случаях сервер отвечает 404.

Подтверждение домена в MCP Registry

Необязательная настройка. MCP_REGISTRY_AUTH_PROOF содержит открытую часть ключа, которым подписываются публикации Beancount.io в официальном MCP Registry (registry.modelcontextprotocol.io). Значение должно быть полной записью v=MCPv1; k=ed25519; p= (k=ecdsap384 для ключа P-384).

Если переменная задана, сервер возвращает ее при запросе GET /.well-known/mcp-registry-auth. Так реестр проверяет, что публикацию в пространстве имен io.beancount/* выполнил владелец этого домена. Если значение не соответствует записи в точном формате, запуск сервера завершится ошибкой.

Если переменная не задана, сервер отвечает 404. На собственном сервере оставьте ее незаданной: публикация ключа Beancount.io на вашем домене позволила бы Beancount.io публиковать записи в пространстве имен реестра вашего домена.

Описание публикации находится в файле server.json в корне этого пакета; порядок публикации описан в руководстве по MCP.

Статический клиент beancount-mobile является публичным и не содержит секретов. Он принимает только гранты с кодом авторизации и обновлением токена, требует PKCE с методом S256 и регистрирует только URI io.beancount.ios:/oauth/callback и io.beancount.android:/oauth/callback. Endpoint авторизации также принимает screen_hint=signup и передает его на страницу /oauth/mobile-consent панели управления, чтобы нажатие кнопки Sign Up в приложении открывало регистрацию, а не форму входа. Это подсказка для отображения, а не правило доступа: неизвестное серверу значение отбрасывается, а не отклоняется. Поэтому приложение с более новой версией, чем собственный сервер, все равно увидит форму входа. Подсказка никогда не передается в MCP или на страницы взаимодействия с identity-client.

Для возможностей OAuth в GraphQL, REST и MCP действует единая фиксированная матрица операций:

чтение требует ledger.read, обычные мутации — ledger.write, а операции управления ledger, например удаление ledger, управление соавторами или открытыми ключами, — ledger.admin. Scope накапливаются: ledger.write включает права на чтение, а ledger.admin — права на запись и чтение. Управление жизненным циклом учетной записи пользователя не входит в эту матрицу: никакой scope для ledger, идентификатор клиента OAuth или связь с ledger не дают права на user.delete.

Для операций с жизненным циклом ledger, соавторами, выходом из ledger и открытыми ключами SSH уровень ledger.admin задает только верхнюю границу делегированных полномочий учетных данных. Централизованный PDP также требует актуальной точной связи с самим собой — через User или ledger в Gitea — на границе application-service/workflow. Класс операции admin используется для метаданных ограничения частоты запросов и аудита, но не предоставляет дополнительных прав. Отказ в доступе из-за отсутствия связи скрывает факт существования ledger. Если источник данных о связях недоступен, сервер возвращает ошибку недоступности сервиса и не выполняет мутации в Fava или Gitea, очистку Plaid и побочные операции с базой данных. Контракты GraphQL для существующих dashboard и мобильных приложений не изменяются.

Авторизация на уровне доступа к данным ledger

Для метаданных ledger, отчетов, журналов, счетов, файлов, истории репозитория, BQL, архивов, записей, квитанций и pull request используются действия ledger.*, независимые от транспорта и входящие в тот же PDP. GraphQL, REST, инструменты MCP и ресурсы MCP остаются алиасами: классы операций сохраняют действующие лимиты запросов, а защищенный сервис или workflow выбирает каноническое действие и проверяет scope учетных данных, привязку к ledger и актуальный факт владения, соавторства или публичного доступа в Gitea до чтения данных или выполнения изменений.

Анонимное чтение по-прежнему доступно для ledger, которые сейчас являются публичными. Для анонимных запросов к закрытым ledger сервер требует аутентификацию; при отказе на основании подтвержденной связи возвращается ошибка доступа с возможностью устранить ее. Если источник данных о связях недоступен, сервер возвращает ошибку недоступности сервиса и фиксирует ее в аудите как ошибку. После авторизации продолжают действовать доменные инварианты: проверка путей к файлам и имен архивов, формирование коммитов, принадлежность квитанций и их очистка, атомарная обработка записей. Решения об авторизации не кэшируются, поэтому отзыв прав или изменение видимости влияют на следующий запрос.

Пользователь, вошедший в систему, может задавать вопросы о публичном ledger или ledger, к которому ему предоставлен общий доступ, в том числе когда интерфейс запрашивает режим агента. Сначала агент проверяет право на выполнение действия только для чтения ai.ledger.ask, затем отдельно запрашивает повышение прав до записи для ai.ledger.agent. Если в повышении прав отказано, агент все равно отвечает в режиме только для чтения — без инструментов редактирования и доступного для записи удаленного Git-репозитория. Если источник данных авторизации недоступен, запрос отклоняется.

Авторизация пользователей

Защищённые операции с профилем пользователя, жизненным циклом учётной записи, управлением API-ключами и оплатой соответствуют действиям user.*, независимым от транспорта, в централизованном TypeScript PDP в src/server/api/authorization/. Адаптеры GraphQL, REST и MCP делегируют обработку сервисам приложения для учётных записей, API-ключей и подписок. Их защищённые публичные методы принимают окончательное решение до чтения данных домена или выполнения побочных эффектов. Отношения пользователя с самим собой определяются по разрешённому стабильному ID пользователя; при отзыве API-ключа его принадлежность устанавливается по текущей записи в базе данных, без копирования данных в хранилище кортежей.

Существующие контракты учётных данных сохраняются. Mutation.deleteAccount по-прежнему не принимает аргументов и доступен с браузерными сессиями и учётными данными пользователя OAuth, поэтому исправление удаления учётной записи с мобильного устройства не требует изменений клиента или дополнительной проверки подлинности. API-ключи не позволяют удалить пользователя или создавать новые ключи на замену. Поиск и обновление профиля доступны только через сессию; чтение профиля и управление API-ключами сохраняют прежние ограничения по области действия. После авторизации по-прежнему применяются ограничения для платных тарифов, сужения областей действия и списка закреплённых элементов, срока действия и обработки одноразовых секретов. С 2026-10-09 управление API-ключами и SSH-ключами, удаление учётной записи, тарифы и сведения об использовании вообще не доступны через MCP; эти функции остаются в GraphQL и REST (поправка к ADR 019).

Статический каталог квот тарифов намеренно открыт и не задействует PDP. Статус подписки, сессии оформления заказа и портала, отмена, возобновление и переход на другой тариф доступны только через браузерную сессию; это правило для учётных данных задаётся в каталоге PDP. В op-class.ts отдельно сохраняется прежний лимит в 300 запросов в минуту для каждого псевдонима платёжных операций, включая открытый каталог. Привязка клиента Stripe, настроенные продукты и цены, а также принадлежность подписки по-прежнему проверяются в платёжном домене. Если источник данных об отношениях недоступен, система возвращает ошибку недоступности сервиса, а не отказ по политике; это происходит до начала работы со Stripe или локальными платёжными данными. Существующие сценарии в панели управления и мобильном приложении не требуют новых действий со стороны клиента.

В системе нет среды выполнения OpenFGA, SDK, сервиса, новой базы данных, контекстных кортежей или кэша авторизации между запросами. authz/model.fga и тесты FGA CLI задают декларативные границы отношений; текущие данные проверяются локально, а при их недоступности доступ закрывается. Подробнее см. authz/README.md и ADR 0010.

Ротация ключа подписи выполняется в два этапа: замените JWKS, хранящий секрет, разверните изменения, а затем убедитесь, что /api-gateway/oauth/jwks показывает только новый публичный kid. Токены, подписанные удалённым ключом, сразу перестают проходить аутентификацию. Никогда не помещайте закрытый JWK или токен в журналы, команды, записываемые CI, или файлы, добавляемые в репозиторий.

Отзыв токена обновления запрещает его повторное использование, но не поддерживает список блокировки для каждого запроса уже выданных автономных токенов доступа. Максимальный срок их действия — один час (ttl.AccessToken); при выходе клиенты должны сразу удалить локальную копию токена, а операторам следует использовать ротацию ключа подписи только при реагировании на инцидент, когда требуется немедленно аннулировать все токены.

Срок действия сессий

src/features/oauth/data/config.ts — единый источник настроек клиентов OAuth, аудиторий и сроков действия. Здесь заданы статические клиенты Mobile и Discourse, профиль динамической регистрации для клиентов MCP, аудитории относительно издателя, сроки действия токенов и разрешений, а также ротация токенов обновления. AppConfig.oauth содержит только параметры развёртывания — издателя, адрес источника взаимодействия, ключи подписи и необязательный секрет Discourse, — поэтому тест или развёртывание не могут незаметно изменить ID клиента, URI перенаправления, аудиторию или TTL. Браузерные сессии панели управления не являются клиентами OAuth и поэтому не входят в этот каталог.

Указанные ресурсы — это идентификаторы OAuth, а не HTTP-пути. GraphQL и REST сохраняют существующие URL транспортных интерфейсов; исторический идентификатор /v1 обозначает общую границу авторизации, хотя по этому URL не размещена ни одна конечная точка. Он остаётся неизменным, поскольку выпущенные нативные клиенты и сохранённые разрешения на обновление привязаны именно к этой аудитории.

Клиент/профильРегистрацияУчётные данные на конечной точке выдачи токенаАудитория ресурса
Beancount MobileСтатическаяПубличный клиент + PKCEAPI приложения (/v1)
Форум DiscourseСтатическаяclient_secret_basic + PKCEТолько UserInfo (ресурс API отсутствует)
Интеграции MCP/агентовDCR или CIMDЗарегистрированные метаданные или опубликованные метаданные (обычно публичный клиент + PKCE)MCP (/api-gateway/mcp)
Учётные данныеСрок действияПримечания
Токен доступа (все клиенты)1 часАвтономный токен; отзыв не сокращает срок его действия.
Токен обновления (нативное приложение)365 днейПолностью перевыпускается при каждом обновлении.
Токен обновления (хосты MCP)45 днейДля публичных клиентов при каждом обновлении выполняется ротация.
Токен обновления (Discourse)30 днейСтандартная политика ротации oidc-provider.
Разрешение (нативное приложение)Окно сессии + 1 деньПродлевается при каждом обновлении.
Разрешение (хосты MCP)46 дней, не более 1 годаПродлевается при обновлении, но не выходит за пределы одного года после предоставления согласия.
Разрешение (Discourse)14 днейНе продлевается.
Сессия сервера авторизации14 днейCookie браузерного SSO, а не сессия приложения.

Сессия нативного приложения задаёт период бездействия, а не фиксированный срок. Токен обновления ротируется при каждом обновлении, а связанное с ним разрешение сохраняется заново с полным сроком действия. Поэтому устройство, которым воспользовались хотя бы один раз в пределах этого периода, остаётся авторизованным бессрочно; если же устройство не использовали в течение всего периода, потребуется повторная авторизация в системном браузере. Это сделано намеренно: oidc-provider записывает разрешение только при авторизации и отклоняет любой запрос на обновление, если срок разрешения истёк. Таким образом, именно фиксированный срок действия разрешения, а не токена обновления, ограничил бы продолжительность сессии. Кроме того, стандартная ротация oidc-provider перестаёт ротировать цепочку, возраст которой превышает 365,25 дня. Обе настройки переопределяются функциями политики в features/oauth/data/config.ts. Периоды в этом каталоге заданы в коде и проверяются при ревью, а не передаются через переменные окружения: срок действия учётных данных не должен незаметно различаться в разных развёртываниях.

Подключение хоста MCP (любого клиента DCR или CIMD) работает по тому же принципу, но с двумя отличиями (ADR 019 D5). Период бездействия составляет 45 дней: бухгалтерские интеграции работают в ритме ежемесячного закрытия, и период в 30 дней заканчивается у тех, чьи периоды закрытия разделяют 31 день. Кроме того, раз в год требуется повторное одобрение: срок разрешения продлевается при каждом обновлении, но не может выйти за пределы одного года после согласия пользователя. Это ограничение введено потому, что такие хосты хранят на собственных серверах токены обновления, дающие право записывать данные в бухгалтерский реестр. Для публичных клиентов токены обновления ротируются при каждом обновлении независимо от их возраста, как того требует спецификация MCP; единственное исключение — токен, привязанный к отправителю. Для Discourse сохраняются настройки по умолчанию oidc-provider.

Длительную сессию можно завершить досрочно с помощью отзыва: при выходе отзываются учётные данные для обновления, а вместе с ними — и разрешение.

Сброс пароля завершает все сессии одновременно. В одной транзакции с изменением пароля удаляются все сессии панели управления и CLI (строки jwts), а также все разрешения OAuth, токены обновления и сессии сервера авторизации пользователя для каждого клиента (oauthGrant.revokeAllForUser). Если отозвать их не удаётся, сброс завершается ошибкой, а старый пароль остаётся действующим. Уже выданные токены доступа истекают в течение часа, и обновить их невозможно. Персональные API-ключи (bcio_) и SSH-ключи не отзываются: это долгосрочные учётные данные, созданные самим пользователем; посмотреть их список и отозвать их можно в панели управления: Settings → Personal access tokens и Settings → SSH Keys.

Учётные данные, выданные до введения этих сроков действия, продолжают работать по прежним правилам: существующий токен обновления принимается до истечения исходного срока, а выданная при его ротации замена получает новый период действия. Таким образом, установленное приложение переходит на новые правила при первом обновлении — без повторной авторизации и принудительного выхода. Если срок разрешения уже истёк, запрос по-прежнему будет отклонён: увеличение периода действия не возобновляет истёкшую авторизацию.

Прокси Git over SSH (необязательно, ADR 0004)

Backend-v2 может самостоятельно обслуживать Git over SSH вместо Gitea. В этом случае правило main-only и все проверки на уровне приложения применяются и к SSH, и к HTTP. Прокси проверяет ключ клиента по ключам, хранящимся в Gitea, а затем обращается к Gitea по протоколу git-over-HTTP, используя учётные данные самого пользователя. Наши ключи нигде не хранятся и не регистрируются. Прокси остаётся отключённым, если не заданы оба параметра: SSH_PROXY_ENABLED и SSH_PROXY_HOST_KEY.

SSH_PROXY_ENABLED=false
SSH_PROXY_PORT=2222
SSH_PROXY_HOST_KEY=
Ограничение директив для бесплатного тарифа (ADR 0005)

Прокси также обеспечивает соблюдение лимита директив для бесплатного тарифа, заменяя прежний хук Gitea pre-receive. Он запрашивает у ledger-v2 текущее количество записей в журнале и отклоняет отправку, если лимит уже превышен. Однако прокси не может определить, приведёт ли конкретная отправка к превышению: в тонком pack-файле нет хранилища объектов.

Переключателя для этого поведения нет. При любой ошибке проверка завершается с разрешением операции: после удаления хука пользователь, превысивший лимит, может вернуться в его пределы только удалив записи через приложение. Если бы при ошибке проверка запрещала операцию, сразу оказались бы закрыты оба пути.

В SSH_PROXY_HOST_KEY нужно указать существующий закрытый ключ хоста Gitea. Если предъявить новый ключ на порту, которым клиенты уже пользовались, они получат сообщение REMOTE HOST IDENTIFICATION HAS CHANGED — такое же, как при атаке, — и git откажется продолжать работу. Получить ключ можно с помощью _infra/print-ssh-host-key.sh. Это закрытый ключ, поэтому храните его только в незакоммиченном файле .env.

Запуск в Windows

Рекомендуем использовать Windows Subsystem for Linux (WSL). Установив WSL, откройте терминал и выполните те же шаги по установке. Если вы предпочитаете PowerShell, перед запуском команд убедитесь, что доступны nvm и Git Bash.

Разработка

Запуск сервера

Чтобы запустить сервер в режиме разработки, выполните:

yarn start

Сервер будет доступен на порту, указанном в конфигурации. По умолчанию используется порт 4104.

Доступные скрипты

  • yarn start: Запустить сервер с помощью ts-node

  • yarn build: Собрать сервер

  • yarn dist:start: Запустить собранный сервер с помощью Node.js (сначала необходимо выполнить сборку)

  • yarn typecheck: Проверить типы TypeScript

  • yarn test: Запустить модульные тесты

  • yarn lint: Проверить стиль кода с помощью ESLint и исправить найденные проблемы

  • yarn kill: Остановить сервер Node.js, занимающий порт 4104

  • yarn mcp:conformance : Проверить, доступен ли MCP endpoint развёрнутой версии (см. раздел «Подключение MCP-клиента»)

  • yarn mcp:agent-eval: Запустить сценарии знакомства с сервисом в настоящих сеансах Claude Code и Codex, подключённых к размещённому MCP endpoint, а затем оценить ответы и изменения в журнале (выполнение тарифицируется и запускается только по желанию; см. Сценарии MCP-агентов)

Планировщик заданий

Фоновые задания планируются с помощью node-cron и запускаются автоматически при старте сервера:

  • Очистка JWT: ежедневно в полночь (00:00) удаляет просроченные токены из PostgreSQL

  • Почта для аутентификации: каждые пять секунд обрабатывает до пяти заданий из постоянной очереди за один пакет

  • Тестовое задание для разработки: каждые пять минут в режиме разработки выводит тестовое сообщение с помощью console.log

Подробности реализации см. в src/scheduler/, а сведения о добавлении заданий — в src/scheduler/README.md.

Документация API

Бэкенд предоставляет GraphQL API через Apollo Server. Чтобы изучить схему API, запустите сервер и откройте GraphQL playground.

Создание журнала через GraphQL createLedger, REST POST /api-gateway/v1/ledgers или MCP createLedger по умолчанию использует шаблон STARTER (в том числе если шаблон не указан или имеет значение null). Шаблон Starter включает настройки и базовую структуру счетов, но не содержит транзакций и начальных остатков. Для демонстрационного журнала с заполненными данными выберите SAMPLE. Изменение шаблона применяется только к новым журналам; существующие книги не обновляются.

Прокси моделей, совместимый с OpenAI

POST /api-gateway/ai/openai/chat/completions принимает max_completion_tokens или устаревший max_tokens в виде положительного целого числа и передаёт указанное поле без изменений. Если переданы оба поля, запрос отклоняется как некорректный. Если не передано ни одно из них, прокси отправляет max_completion_tokens: 1500, а не полагается на значение провайдера без ограничения. Явно заданный предел заменяет значение по умолчанию и при этом не может превышать ограничения выбранной модели.

Для вызова маршрута требуются учётные данные с авторизацией и правом записи. До обращения к модели проверяется месячная квота вызывающего пользователя на использование AI. После успешного вызова сохраняется общее количество токенов, указанное провайдером. Эта совместимая по протоколу конечная точка сохраняет документированные исключения для GraphQL и MCP.

Распознанный отказ провайдера из-за квоты возвращает HTTP 429 с error.code: "RATE_LIMITED" и error.metadata.quotaScope: "shared_service". Это ограничение относится к общей мощности моделей развёрнутой версии и учитывается отдельно от месячной квоты пользователя на использование AI. Если провайдер указывает допустимое будущее время сброса квоты, blockedUntil содержит его временную метку в формате UTC ISO, которая также включается в сообщение. Необязательное поле retryAfter содержит положительное целое число секунд. Значение берётся из числового поля провайдера, целочисленного заголовка Retry-After или временной метки сброса — именно в этом порядке. Для неуспешных запросов использование токенов не учитывается.

Данные о квотах провайдера распознаются в ограниченных по размеру объектах error и message. Остальные ошибки провайдера сохраняют свой HTTP-статус, но возвращают общее сообщение об ошибке продукта; сообщения и ссылки провайдера, а также учётные данные не копируются в это сообщение. Текущая версия API месячного использования не сообщает об общей мощности.

Карта сайта общедоступных журналов

Анонимная конечная точка GET /api-gateway/sitemap.xml возвращает список страниц общедоступных журналов. В руководстве по работе с картой сайта описаны полный процесс постраничной выдачи, кэширование и обработка ошибок, канонические URL и проверка после развёртывания.

Подключение MCP-клиента

Бэкенд предоставляет не сохраняющую состояние конечную точку Streamable HTTP по адресу /api-gateway/mcp. Подключитесь, используя OAuth grant для этого ресурса или ключ API с префиксом bcio_. Для инструментов работы с журналами укажите ledger: "owner/name". Учётные данные, привязанные к журналу, по умолчанию используют его и не позволяют выбрать другой. Для учётных данных без привязки журнал нужно указывать при каждом вызове; инструментам для работы со счетами журнал не требуется.

{
  "mcpServers": {
    "beancount": {
      "type": "http",
      "url": "https://your-deployment/api-gateway/mcp",
      "headers": { "Authorization": "Bearer bcio_your_ledger_scoped_key" }
    }
  }
}

Размещённая версия Beancount.io представлена в официальном MCP Registry под именем io.beancount/beancount. Клиенты с поддержкой реестра позволяют выбрать её из списка и выполнить вход через OAuth. Подробнее см. Подключение через MCP Registry.

Каждый результат содержит читаемый текстовый блок и типизированное поле structuredContent. Каждая ошибка возвращается в формате {code, message, hint} с установленным флагом isError, поэтому агент может выбирать дальнейшие действия по коду, а не по тексту сообщения.

runBqlQuery возвращает канонические строки из единственного выполнения запроса и сохраняет исходный строковый результат. Многострочные ячейки и директивы считаются строками результата. Если сервис журнала не предоставляет количество строк, в читаемом тексте указывается row count unavailable. Подробнее о текстовом представлении и обработке структурированных запросов см. в разделе Формат результата.

Чтобы найти транзакции с пустым описанием, прочитайте ресурс beancount://{owner}/{name}/narration-transactions?narration=. В примерах поиска транзакций описаны явная передача значений и сохранённый псевдоним пути для непустых значений.

В примере текущей прибыли и убытка в балансовом отчёте показан один и тот же URI для чтения в Codex и Claude Code, включая закрытие счетов капитала только в отчёте и область действия фильтра счетов.

В руководстве Beancount.io по MCP описаны настройка, разрешения OAuth и ключей API, передача запросов в журнал, все 43 инструмента и 64 шаблона ресурсов, четыре бухгалтерские подсказки и их использование в Claude Code и Codex, формат результата и коды ошибок, запись директив в виде текста Beancount, предварительный просмотр изменений файлов, импорт банковских данных, примеры протокола и диагностика развёртывания. Также в нём описаны ограничения проверки соответствия и текущие различия между возможностями для клиентов и REST.

Чтобы оценить, как настоящие агенты-программисты работают с этим интерфейсом, запустите Сценарии MCP-агентов. Они выполняют три задачи знакомства с сервисом в Claude Code и Codex, проверяют ответы и состояние журнала после выполнения, а также фиксируют вызовы, количество итераций, время и стоимость. Запускайте их до и после изменений инструментов, ресурсов, подсказок или инструкций сервера.

Описание принятых проектных решений см. в документах ADR 0007 (контракт транспортного уровня) и ADR 0008 (инструменты, ресурсы и соответствие интерфейсов).