From bcca392a08dbcf658e24746efbd464d7a51d63d1 Mon Sep 17 00:00:00 2001 From: Ivan Chikishev Date: Fri, 9 Oct 2026 02:45:51 +0500 Subject: [PATCH 1/5] docs: refresh Collaborative README and public branding --- README.md | 234 +++++++++++------- docs/assets/brand/README.md | 40 +-- docs/assets/brand/collaborative-logo-dark.svg | 11 + .../assets/brand/collaborative-logo-light.svg | 11 + docs/assets/brand/collaborative-social.png | Bin 0 -> 51839 bytes docs/assets/brand/collaborative-symbol.svg | 8 + docs/guide/deployment.md | 1 - docs/index.md | 4 +- 8 files changed, 197 insertions(+), 112 deletions(-) create mode 100644 docs/assets/brand/collaborative-logo-dark.svg create mode 100644 docs/assets/brand/collaborative-logo-light.svg create mode 100644 docs/assets/brand/collaborative-social.png create mode 100644 docs/assets/brand/collaborative-symbol.svg diff --git a/README.md b/README.md index 0787359..84ba410 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,167 @@ -# Asmblyr Collaborative +

+ + + Collaborative + +

+ +

+ A collaborative workspace for your PostgreSQL data.
+ Collections, an admin interface, access policies, and an API — on your infrastructure. +

+ +

+ Build and checks + MIT license + Early beta +

+ +

+ Quick start · + Deployment · + Documentation · + Contributing +

+ +Collaborative turns PostgreSQL data into a workspace your team can use together. +Define collections and relationships, build forms and table views, and control +which records and fields each person can access. Use the same data through the +HTTP API or a typed TypeScript client, and extend the product with plugins. + +**Early beta.** Each installation is intended for one team. The interface supports +English and Russian; most detailed guides are currently in Russian. SDK and CLI +packages are available under the npm `beta` tag. + +## What you can do + +| Area | Capabilities | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Model your data** | Typed fields, M:1 / 1:M / M:N relationships, custom fields on supported system collections, and read-only materialized views. | +| **Build a workspace** | Configurable forms and tables, relevance search, nested filters, saved views, tags, and translated collection and field labels. | +| **Work together** | Discussions, notifications, live presence, saved-record updates, advisory field locks, and conflict handling that preserves local edits. | +| **Control access** | Policies for actions, fields, and rows; invitations by link; passwords, passkeys, and external SSO; service accounts and OAuth/OIDC applications. | +| **Connect services** | S3-compatible storage, encrypted connection secrets, optional Yandex KMS, personal Google Drive/Sheets connections, and optional Sentry monitoring. | +| **Extend the product** | Server handlers, UI pages, field editors, collections, and migrations through the plugin Kit; a TypeScript SDK and schema-generating CLI. | +| **Use an assistant** | Optional streaming chat with conversation history, permission-aware data tools, and explicitly exposed plugin actions. | + +See the [feature guide](docs/guide/features.md) for details and current limits. +Workspaces organize collections within an installation; they are not tenant +isolation boundaries. Plugins are installed with the application, and their +server code runs as trusted code inside Core. + +## Live collaboration + +Open the same record in two sessions to see who is there, which fields are being +edited, and changes after they are saved. Tables and record editors receive +updates over server-sent events. Unsaved edits stay in the form; conflicting +changes to the same field can be reviewed before saving. + +Field locks help coordinate editing. Conflict checks protect draft saves; +ordinary API writes retain their documented behavior. Live events come from +operations through Core, not direct SQL writes. +[How live collaboration works](docs/features/realtime.md). + +## Quick start + +For local development, install **Node.js 22+, pnpm 11.13.1, and Docker Compose**. -**Данные команды, права доступа и расширения — на PostgreSQL.** - -Asmblyr Collaborative — открытая платформа для команд, которым нужны настраиваемые -коллекции, редактор записей и HTTP API. Разработчики могут добавить серверные -обработчики и страницы через `@asmblyr-collaborative/kit`; интеграции работают -через API, TypeScript SDK и CLI. - -**Статус: ранняя бета.** Одна установка предназначена для одной команды. -Интерфейс и основная документация сейчас на русском. SDK, CLI, Kit и контракты -доступны в npm как `0.1.0-beta.1` под тегом `beta`; выпуск не объявлен стабильным. - -[Сайт и демо](https://asmblyr.io/) · [Рабочая консоль](https://console.asmblyr.io/) · -[Быстрый старт](#быстрый-старт) · [Документация](docs/index.md) · -[Участие](CONTRIBUTING.md) - -## Что уже работает - -| Область | Возможности | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| Данные | Коллекции, типизированные поля, связи M:1 / 1:M / M:N, формы, таблицы, поиск, вложенные фильтры и сохранённые виды | -| Совместная работа | Комментарии, живое присутствие, обновление записей, блокировки полей и проверка конфликтов при сохранении | -| Доступ | Политики для действий, полей и строк; отдельные права на настройки; приглашения, пароль, passkey и SSO | -| Файлы и интеграции | S3-совместимое хранилище, сервисные аккаунты, GitLab CI federation, OAuth/OIDC provider и личные подключения Google Drive/Sheets | -| Разработка | H3-обработчики, страницы, редакторы полей, коллекции и миграции через Kit; типизированный SDK и CLI `asm connect` | -| Ассистент | Опциональный потоковый чат, инструменты с правами пользователя и действия расширений через внутренний MCP | +```sh +git clone https://github.com/Asmblyr/Collaborative.git +cd Collaborative +pnpm install --frozen-lockfile +pnpm db:up +cp -n apps/core/.env.example apps/core/.env +``` -Подробности и границы — в [карте возможностей](docs/guide/features.md). -Workspaces группируют коллекции, но не изолируют компании; публичного MCP endpoint -и установки расширений из интерфейса пока нет. +Set `ASMBLYR_SETUP_TOKEN` in `apps/core/.env` to a random value of at least +32 characters, then start the application: -## Collaborative Live +```sh +pnpm db:migrate +pnpm dev +``` -Откройте одну запись в двух браузерах: участники видят друг друга, активное -редактирование поля и сохранённые изменения почти сразу. Таблица обновляется -через SSE; занятые поля показывают имя редактора. Если два человека изменили -одно поле, сохранение черновика возвращает конфликт и предлагает выбрать -значение. [Контракт и ограничения](docs/features/realtime.md). +Open [localhost:3000/setup](http://localhost:3000/setup) to create the first +administrator. The copy command above is for a POSIX shell; on Windows, copy the +example file without overwriting an existing `.env`. +See [first-time setup](docs/guide/getting-started.md) for configuration details. -## Ассистент и расширения +## Deploy with Docker or Kubernetes -При настройке OpenAI-совместимого провайдера ассистент работает с разрешёнными -данными и явно опубликованными действиями расширений. `defineModelContext` -публикует H3-обработчик для ассистента и внутреннего MCP; обычный HTTP-обработчик -не становится инструментом автоматически. Доступ к данным дополнительно зависит -от прав вызывающего и настройки публикации коллекции в MCP. Личные подключения -Google Drive и Sheets поддерживают чтение по запросу и изменение после -подтверждения пользователя. +Deploy **Core and UI as a matching pair**, built from the same commit. PostgreSQL +and S3-compatible storage are connected separately. Both components can share +one public origin, with the API available at `/api`. -[Как работает ассистент](docs/development/assistant-architecture.md) · -[Первое расширение](docs/development/first-extension.md) · -[Контракт Kit](docs/reference/kit-guide.md) +- **Docker Compose:** [deployment instructions](deploy/README.md#docker-compose). +- **Kubernetes:** [Helm chart](deploy/helm/collaborative) and [configuration](deploy/README.md#kubernetes-и-helm). +- **Images:** `ghcr.io/asmblyr/collaborative-core` and `ghcr.io/asmblyr/collaborative-ui`. +- **Operations:** [upgrades](docs/guide/deployment.md), [backups and recovery](docs/development/operations.md). -## Быстрый старт +Pin both images to the same release and their respective digests. Container builds +compile the SDK, Kit, contracts, and bundled plugins directly from this repository; +they do not require those packages to be published to npm first. -Нужны **Node.js 22+, pnpm 11.13.1 и Docker Compose**. Из корня репозитория: +## Connect a TypeScript project ```sh -pnpm install --frozen-lockfile -pnpm db:up -cp -n apps/core/.env.example apps/core/.env +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +npx asm connect --url http://localhost:3000 ``` -В `apps/core/.env` задайте случайный `ASMBLYR_SETUP_TOKEN` длиной не менее 32 -символов. Затем: +The CLI opens your installation's sign-in and consent flow, downloads the schema +available to your account, and generates collection and plugin types. No custom +compiler or bundler plugin is required. Schema authorization does not grant data +access; API requests still need their own authenticated session or credentials. -```sh -pnpm db:migrate -pnpm dev -``` +[SDK guide](docs/reference/sdk-guide.md) · +[CLI guide](docs/reference/cli-guide.md) · +[HTTP API](docs/reference/http.md) -Откройте [localhost:3000/setup](http://localhost:3000/setup), чтобы создать первого -администратора. Копируйте шаблон `.env` только если файла ещё нет. Подробнее — -[первый запуск](docs/guide/getting-started.md). -Для Docker и Kubernetes предусмотрены отдельные образы Core и UI, собранные из -одного коммита, Docker Compose и Helm-чарт. PostgreSQL и S3 подключаются отдельно: -[установка и настройка](deploy/README.md). CI публикует согласованную пару -`ghcr.io/asmblyr/collaborative-core` и `ghcr.io/asmblyr/collaborative-ui`; -для установки закрепляйте оба образа одной версии по digest. -SDK, CLI, Kit и общие контракты доступны в npm как `0.1.0-beta.1` под тегом -`beta`: [установка и подключение](docs/reference/packages.md). Docker-сборка -не зависит от предварительной публикации этих пакетов. +## Assistant and plugins -## Развёртывание +Connect an OpenAI-compatible provider to enable the assistant. It works with the +caller's permissions and collections explicitly made available to its tools. +Personal Google Drive and Sheets connections support reading on request and +proposing changes for user approval. -Основной способ установки использует отдельные образы Core и UI. Совместимый -монолитный образ доступен для существующих установок. [Docker и обновления](docs/guide/deployment.md) -описывают сборку из исходников, Compose и эксплуатационные границы. -[Опубликованные пакеты](docs/reference/packages.md) и -[CLI](docs/reference/cli-guide.md) предназначены для внешних клиентов. +Plugins can expose selected actions through `defineModelContext`. Ordinary HTTP +handlers do not automatically become assistant tools. The MCP integration is +internal; there is no public MCP endpoint. -## Как устроен проект +[Assistant architecture](docs/development/assistant-architecture.md) · +[Build your first plugin](docs/development/first-extension.md) · +[Kit guide](docs/reference/kit-guide.md) + +## Architecture ```mermaid flowchart LR - Browser[Браузер] -->|страницы| UI[Next.js UI] - Browser -->|/api| API[Fastify Core API] - SDK[TypeScript SDK и CLI] --> API - API --> DB[(PostgreSQL)] - API --> S3[(S3-совместимое хранилище)] - Kit[Расширения через Kit] --> API + Browser[Browser] -->|Pages| UI[Next.js UI] + Browser -->|/api| Core[Fastify Core] + SDK[TypeScript SDK and CLI] --> Core + Core --> DB[(PostgreSQL)] + Core --> S3[(S3-compatible storage)] + Plugins[Kit plugins] --> Core ``` -Core отвечает за HTTP, авторизацию и данные; UI предоставляет админку. -Браузерная сессия обрабатывается Core. Серверный код расширений исполняется внутри Core как доверенный -код. [Архитектура](docs/development/architecture.md) и -[структура репозитория](docs/development/local-development.md) раскрывают границы -компонентов и хранения. +Core owns the HTTP API, authentication, permissions, and database access. UI +provides the admin interface. PostgreSQL coordinates realtime updates between +Core instances. +[Architecture guide](docs/development/architecture.md). + +## Contributing -## Документация и участие +Bug reports and proposals are welcome in [GitHub Issues](https://github.com/Asmblyr/Collaborative/issues). +For code changes, fork the repository and open a pull request. Discuss substantial +architecture or API changes in an issue first. -- [Руководства и справочники](docs/index.md) · [HTTP API](docs/reference/http.md) · [TypeScript SDK](docs/reference/sdk-guide.md) -- [Локальная разработка](docs/development/local-development.md) · [Участие в проекте](CONTRIBUTING.md) -- [Безопасность и сообщение об уязвимости](SECURITY.md) +- [Contribution guide](CONTRIBUTING.md) and [local development](docs/development/local-development.md). +- [Documentation index](docs/index.md) and [documentation workflow](docs/development/documentation.md). +- [Security policy and private reporting](SECURITY.md). -## Лицензия +## License -[MIT](LICENSE). Сторонние зависимости сохраняют свои лицензии. Дополнительное -локальное S3-хранилище использует MinIO под AGPL. Встроенный шрифт Geist -распространяется по [SIL OFL 1.1](apps/ui/src/app/fonts/OFL.txt). +[MIT](LICENSE). Third-party dependencies retain their own licenses. The optional +local S3 service uses MinIO under AGPL. The wordmark uses the bundled Geist +typeface, distributed under [SIL OFL 1.1](apps/ui/src/app/fonts/OFL.txt). diff --git a/docs/assets/brand/README.md b/docs/assets/brand/README.md index c97495d..5adf422 100644 --- a/docs/assets/brand/README.md +++ b/docs/assets/brand/README.md @@ -1,17 +1,27 @@ -# Визуальная идентичность +# Collaborative brand assets -Текущее узнаваемое имя в интерфейсе — **Asmblyr**; полное имя проекта — -**Asmblyr Collaborative**. У проекта есть [публичный сайт](https://asmblyr.io/) -с логотипом и [рабочая консоль](https://console.asmblyr.io/). В README используется -[растровая версия предоставленного логотипа](./asmblyr-wordmark.png) с повышенным -разрешением; исходный векторный файл и снимки продукта пока не хранятся в этом -репозитории. Не заменяйте знак новым символом или скриншотом с пользовательскими -данными. +The public wordmark is **Collaborative**. The symbol combines four connected loops +around a square opening, with a mint accent on the upper-right loop. -Когда исходный SVG с сайта станет доступен, храните его здесь вместе с вариантами для светлого и тёмного -фона; компактный знак должен быть читаем как favicon. Снимки продукта размещайте -в `docs/assets/screenshots`, схемы — в `docs/assets/diagrams`, когда файлы -понадобятся страницам. GitHub social preview следует экспортировать из -официальных исходников и установить в настройках репозитория; сам файл не -назначает preview автоматически. Перед публикацией проверяйте контраст, alt-текст -и отсутствие реальных данных, токенов и внутренних URL. +## Files + +- [`collaborative-symbol.svg`](./collaborative-symbol.svg): standalone symbol. +- [`collaborative-logo-light.svg`](./collaborative-logo-light.svg): wordmark for light backgrounds. +- [`collaborative-logo-dark.svg`](./collaborative-logo-dark.svg): wordmark for dark backgrounds. +- [`collaborative-social.png`](./collaborative-social.png): 1280 × 640 repository social preview. + +The SVG files are the editable source assets. They use vector paths, including +outlined lettering, with no embedded raster images, external fonts, or scripts. +The wordmark uses Geist, distributed under [SIL OFL 1.1](../../../apps/ui/src/app/fonts/OFL.txt). + +Use the same symbol geometry at every size. Keep clear space around it, preserve +its proportions, and use the appropriate wordmark for the background. Graphite +(`#1B2930`), mint (`#88BFAA`), and warm white (`#F5F3EE`) are the primary colors. + +The root README switches wordmarks with the reader's light or dark preference. +GitHub's repository social preview is configured separately in repository settings; +adding the PNG to Git alone does not select it. + +Product screenshots belong in `docs/assets/screenshots` and must use fictional +data. Do not include personal data, credentials, or private infrastructure URLs +in public assets. diff --git a/docs/assets/brand/collaborative-logo-dark.svg b/docs/assets/brand/collaborative-logo-dark.svg new file mode 100644 index 0000000..449c148 --- /dev/null +++ b/docs/assets/brand/collaborative-logo-dark.svg @@ -0,0 +1,11 @@ + +Collaborative +Four connected loops with a mint accent beside the Collaborative wordmark. + + + + + + + + diff --git a/docs/assets/brand/collaborative-logo-light.svg b/docs/assets/brand/collaborative-logo-light.svg new file mode 100644 index 0000000..b007ed5 --- /dev/null +++ b/docs/assets/brand/collaborative-logo-light.svg @@ -0,0 +1,11 @@ + +Collaborative +Four connected loops with a mint accent beside the Collaborative wordmark. + + + + + + + + diff --git a/docs/assets/brand/collaborative-social.png b/docs/assets/brand/collaborative-social.png new file mode 100644 index 0000000000000000000000000000000000000000..1800a58eb7886aa502f0568666b00170fc66f93c GIT binary patch literal 51839 zcmeFZc{tSV`#(H%OE;Ai5u#GrvQ_r24Mh<$ma#=B%P{tx?(V|aibUCxBKwvZ3`S*4 zb|vdzGHzxca`Go+KwMyL`g}qDKheuQ*fsnkKHUg5TjF8oJ;!X4@=Qx? zF2DZ+ee@LC(%{TXyK57ZjF9<_y41_9yHw8w&Y1;HRE1P(F*si_*`K9wF?9ciuzxqx6g^z3(_FVt%r^2JX~KVPo?f)UdJ=0m^w4Q$s93D|KN&i}l_ANSVzj)nKv zFT0RM^Bmb*&*$~WF9UEtoB7ypr{j&e-J$>v?(T*BNu3`@bv_^FOaGBJ3a5_WY(gYdfsqqR1Yl zUoFL5@&B?&EdRW=7}(`sukGFC;QgsgrVsQEc>F5uJ8%7$Mfxur!u20F#MG96;;xe2 zX~)KXwG=ONVgI~H-2c3`1I&AWy|ynuul>6L|DkIAy8(ZZ^8aqYe>dRI{qf%p`2T(f zToPVij*5%wmzEZl3Y}f0P&Tmpry~RgH^2O0yU`q1wifjZ40CtY0Q9Tm(skeQL%t5u zHQka$LG*Sw(t~5~FAzxQY=VF&%t0rpfZk&1lJ7jYz4*uMuFJo`V&}I`va*FeKj=oH zy2*T|1);9t?(}y`csm8&x znY?P=wz5u}4Ytb#*9cSh4n=)765+B#J~y>cWmHo(108tc)VHoItRzXS#4WV_1VA1# zj~vkazStuD4(%T(=zevfc11yQn=FLv^a%{+gBAS=5Z~8w^?V8Idr(TxT8m> zt*#BTFU+&L@$lThV-(%yC7akocM+0RiX|G!vV|2ZDr94B(te(9z z$Olwl^=i%XmnDK;VB9Ce^(`NPkikeoc*}5GdV(@xv_r!<`h}dB?l&5VlP98PCH;mdk(FkvAILKc32j(O!53@ptAk&UgP*f-=(Sj_Bh=cf*0L$9 z+7k{PF+WoozJ3rn)6s|)1w8?OtyNp{fE3#i4S$bS)p2xX&}aeTw1f0&;oPgo`N&$a z!1SN>$IeeLcCpmI*#Du@T^KVkA+@;gbi3kYP0z-kRh~U<5$p=K`L6RP$TpP0+ywK< zLlf=Ke{mCL9^fXrC|}aIgyL&Udi7G3ovtsexz&A2#~~YXG9;z7ZWPm+@%JgSh|~L@ zZ*%^Ovm;gk(;F1B!r=Kp zCTUF<3`2^LlX0dcz3b`1`jS33kLr0f=oisdOVzghi)&?*w|*v&F-2n+m`Zj(kVJM4 z`O_F>JoAQ)Vp;92>Akl&6{ccAS_OWSvQIs?@?zGX`{y8LNq|%@cyEK7sHT?j%o-B$ z^t)Mg{cQBfe(W9TzBjQ^g3s+G-1oWs{-3h&U7zC+am|$6nPN~_Q(T_Z+L&n0zU%!? zp^<|=cO~@Wgla5P6|g&OVVqfxH)b||wT5IlS=-G$W;-kY`H0tzyzlCV$ za-NyI=p!7%X==ou`Q_bPPzpvra3MGfacc~EwajW(i`yjyuhRN2*3c6;HhoseI|aBxFVn_H8@UnlHG$)<5w7h}`O@DvM$8D!lOSL}pDPk;Y)dyk z56mvcELD2nD0Q+Q0Fg4qMm*C8(x^ERN|6@bF|T zf4Jh)W_eCr47Puw75~1$7(0CH+7^Y9Fj)1AekAM(*)M=Bd+{#D(YUiw!eQvC+S)HR-^mV;PLJ+;cN!2+OBdCy+YZcC zDCniVsEOgRM%nA{r5G5Aye3<1S%9A(nlG?PSzM-;OQNO#3lmT>y zy3RnO_M}F9O^jV*>|EqURi}W>ZtoV2jbr!tCtE|0`SW*V^zciZ|HaeYl>p`F$nG|& zt=Wv4%GEZL3=!ASSDN}FZlgk4anq4Z$^jFv#|Rbp&?U_;^6JZ9{}^*sb#$usWc$UN zy}bcN{A4igLEKd<^=p*}hE;5gtI+w~cb-9se-~Uzlz3(720aE_*mpB#gJK)bi zVE~zu$sLv@QTcXk29)NE$3Imy88AeiW&I7skDOs1d8R_MqR&yiCSm4Grex-k6k44@ zbg*9lc(c~<`6hMwe|q^j#|}L!SabwFAJ(@c0qaP4Q5VeFA?bg7tM4a7Qh(txQ;E<4 z)iwGnkl|FfQrA+KewAwjcUf%I*uyx^q*`07e`*~g;7d6=MC-Oa>{m;FmNxG{Vz$vZ zw!R4f)tbVF<|)m3s8$r1mRc~CaG1dA)(3P6{k~+=rWrh_8xBG|zevbi@w<}3?(kW# znz$eOp10mTER2SBa9%JR){_rguQy^Bm#|8o`uxAV0Na|COnu=69A9mOjvJ9h?0hf3>a*2!KenXAZo2b z)KZR~xKwIV2#Gc$yB4W^qe$H*{M~(GdQgF){nzJ5iAp?{@dc8yzDf65j(Seq>2TB_ zkntO@ZiQYs6EgnhughH~);Bm8OEM6Y7u2GbZS}uaDCYH#-tl%gc(RQG)uMZxGL-NX zq*HT*PnNj4n7HKD?CfuX-<4X=`AN1>h>Fl~|8cuz_xW@5B<1j~HEpp?17);K?WayH{dUsL z=^Y{HWTBRjZMF5z2mR!5P(L5VGO^UtgMW-tU&ysWO>rzvk^ZTfJ&Ym9nh~ls&QcZ< z$DU)I5`5yI$750$^RWlcWtp%~TJsy%K3&A^=7K$!b%QJhZA<{fC37|wklZ?~E>Kr) z!p@k`aZ^vs25g9~?G8+XedKJI4!5oyoEe(CB>v1_gHSXmyHPv(**qICN!L z>e{lMxYyl>w!EcQ&w@(Zd#gS^j~Elf3~>&Nh(*@kf-XoHYL z=)jUs1s*Rn0o!4TVr_9Yo`iq=pqjUYSteBIt6TxBmCSl+|BUTOa>CBdnLz47=(?TB zKrSqqjE0)g-5wcMkf<*YQGDtLAHK8N$5P*p4h871&~JJrshq^)agztg=^I-wi!!0- z`BUI4A&l8(Tm7#Md0rVpGdFg3ej}zO*CzKwWy1Q*n7)@iSGc1fJ@!%$1bb=iqUB{K za5H<9-u`bPa3L)UVFFBHG5iajmR{PnVY7eu8&GGDL-N-Nv+89UAPCOL&+Vtac zZ=FT|ilevekGTGwps_#TFw?t{1r~_hi3S=JCS77GsilysJl&)Y*AUfqp5y9Ruo>tmF2s{up+pw^=_{{$}sr zubQ#_@pgaFExx0Z00TeTHI>RP-EL({O}q&cuvnnI(stlh7*tWgxgKZrmis#upg3Om zbkTC}vJR8`=kg9NlNcdu@$}&N=d&A!f8gWu@u2pWWNXL+bay%sRmgLR$=#iRU%=|O z%$t9@n6?j!!h>p1wxAUNOH*qIO-Sw$r#{((r*B4(EzDVZ+fva@3&l`_ zZ;uPMyxNA6j?C%}))?A9FQtC0&ix;l&53K%Lz!|>( ziN@b63pASxS)&kN{RG=wPOv8cDsg6eT$|rsy1HA83$|1G-S_?FE_92-@Xa6D;qXvk zIsX54X^6W!jXKVp@fmRTTm68ryu|c<2R}6mpwm?DAvB>F|6*wBX7n045a;Lcxr^{I zu2o__`CLw=ed97^GsO+bpw8mv&jnLS0pTVoSx2XI%}RM3hV87bOcLVpbzk@to)q&VtKKA*C))`= z*C_V;Mwmo5PG;D5=yO8?1Og{yt5-`+S0cQ7eh#5Uu^!bRh6?Qj(sw+t#(PsA07a)p zF4nFc?>JveC?FxX!WZf`C+%9LcxK3Xx|iJwz4P zlzC7Be#VVkTr0!(V(Hp@Z7wP!h>B_5UhBY!`}8~RP@+BCow-#4Dnxm$)p;ZXsw-zZ zLZxEqL$oN#kcz;Cw3XfXOB?u*YJcbbQT!cQZ=ud^Mvh~)83+nl{uWqxsqOis_03uT z`s0K+1#hMMZb{O{aJwwmun(6$T!J|dDU>ZM!fy=;xeW%0t`lV) zc|7NFIqTJ|CZXD#=h&_|#?}7Ngb0rijIzSP#+}dxPVI{A4ZrV?aiV+~oCdS=Cn*_o zV{)Uc+ZRG<<@y>k1H6%cEX(y;nh1D)5hKXs_DR%Xx3gX25@>FVQO_7p9MikSd3}~V zZ{JX&`!odIa*$e|ixlyl6}Bzm(6TK$v!u-V;JI5Ct)))y3YK`UHPN0_Ln)t+E{^c) zll5+lrf_5txo~lc?l&_y@=r8UVn06O5s6lY?cY4aR5#v~gr&<(Zc;u>8=*C$GFp)CqE~{NaDWO>M#zFDbkgp7(&Le?41+zm z`H2*OdpBgZ0xeC>_}N~}_p55csGM%Q%oVsgxbxU{P%gn-LLA4aywxs#GsogT~B9oAD zwc^3W`qBY;igN{?=osv^E&md6Th-ZjYZ0~(Bq}}-ok%sV3IEC|QL}p~WP5W!;*B+R z<&Ey{74vT;v(AM1`^eZXzqhwAZ41XLXI_yZ*1M`S7D2&L8Rw@f2fWFQC=LCZ(=BU~ zpgo^2gDS87?%P34wL9-w(dCU+eF4oz-Eqa$gqvcpSg0QW?UNFJq?g;1N8LQgPARmv zeYj>hFTZzlmxMii>ro0Oy|WHpUN~p~i%gVTcv#BsA92FD0Omj1)?2+Q$PH8Juynbs zJpJyRc?eEvM;kTTj-%xTsrV+@&&zK9ytS)=u7SPpkriL$lWljuP16~Bkz!gbWH$7A3d-h8qdsUfRX1RSlV zznc!_*Aq#w)q^b#M4vOJ6ud+o65)m@R=y&ygvjng;X=N)iyY4jeTMBii#7Uq<>t#M zskyx;V?1fKV*X6UX}v-!t~&XhzTK`0Jm-X}ael}qcBz$5G9k9D4L+}t+x2GL3$GPI zrmmoltyvs&jB(we1}~0%Bl2U}bW79T&8+3Z`a;@YRdPKXn931QhilRwN6m%T!eP;p zu^N4MqI5hDA&Rq1jl!2kUVZPKfAYK7(t(HdceT^l!*2V(#2>GrB3EaLJh)U1ceti1Bj&@c zBG(R7&-H%kCNW!vwF_yJ$zpuV80I2vjdCFEMj(@h8uKDf+$rcOw3V}&VYZ>yDi#+Z zsv>0tihE-yy%$Q?6O+s*(zSn3OXhn6z7ty1%Qr&Uw)V7!ip8ZYN6 z2MHFS6@|{*f3pVAkJ+Z7bf+i`n5@+g( zBINP|(D3;n3H0vweS0o5-DGlq8xzyl~j*E-)-nWZF}UQ8u@7&y^hn@539pJl06EO%dVMca44 zBoMIZbL}3kBte%e-|nnh`aYXiV5#?DsjozBYRJ>ga_geG-HkhLpx){9vznq18#mYI z{ReY}5m-BuAra`dz2v}_+C3B|!v*7pg*W#_NEaH&d}3%immqHED|;!QK)Oo?#_B7a zfH%Fa6V+sLA7V)pZ+}!MPC49nE1$=Z8+6-hI!mgbQTDJ`oKqev&5 zPC|}GBOFgDIh;bSwW;~+F3;PhN}Jpo_{AN6=!+=)!&@mOFYV(jiD9Uo9IFW6Wl8>-d+FM;F_r-k*ca-{qZ=m`!b}X7w`82q~g;rB$X~k&~ ze^@pzakg;5>{yKHF_YGQ@=fwvdhB5|{F=`~SE28xVTC#MV^JybDsv#nq3!o@WV!j! z@AtnjcoF*f$tiHn)LrI2B>(=Xvy{w_f?G$Q2Dk2&e^r98&j85$<>zkD9QnLyuXSGowKqJwvh%q1 z>`n^VXV8r;Y@9u;)Y6L~iIMCM-(~q{f;eTct_&v-qOkbx%UlHhNnpV{{!)*!ybv&4 zxR8|(koozk-G64n-opB5cEagrC$}#pQp4M47QX0qB&j=!I|>9WFI>Z%6Nl|jO&S-f zwccd4Z11Cvd~`N|71%X!%mGMvD#b|T`tx{(T4E%B&W#o;n1!*|HE*uVA|OGJo$|GQ z8mTAE0`#LkTb#e@5}jRV?VCh#U8;-K=BliSJ!$n5-M`=MSi86ECpvM$5b&A;IH4B-Z78V`zwv6F2%2zxMU72g4hQ59`iHTF?W87DK0J^4oV|JfAYwjiy$-X~LkSI>dH%4l<%l8obW0KQESyOO zxf#oR%{1C|L)K?PO}SVut|9M5n{S`ohI8Y-V*Aie%?ySUjq@6bJ6I@mDK$z;%wgEa zdEvY8lDX9Xuz?v?C4*$CK>zFg%p-6W+LJ<9@?t?p<>z&P3(QGUy{j*Tq(ku)n`hB~ zDw!~e!?ryIRn*-4NJDGl-JuH|Nk<+yEBBVoHV;5D@K@EU(UNaX(M12uoLD}iLAAAE zrjC=*m+V!OBWadmlTXho0d?R))?tWPUljQ&3l+O9Im5%Xl$JXM7JRLR-V0f0;V0gx zuPdQr)5dN$SzF&L^t;v_3#9bV;sN_7ru|Z_{l={uMJ(?f)TnN$ya}UErbbuUxWw1> z&nBtxu&0}L`edkXkBt`Ux*5L9Y;B7-=7HH6>u_oH8vCJl)EZ3-7O!q>mQv2FVXBQe zS!zdjz7r|kCOjN76?TMSR8H)5^F3#9+?6g&GWPF`V9}S{^G1*9DF@$L=+Bwseuj4n zdgek{h!Bo-!jWxN=wIIG_8cjVFTGCMud@2*#oG24wk7@ys&nxj37C}{%67Wfs_c%t zXT6QxmzCZmq|tNk54jUf>%&JR?rQjje&+}s4eW32FGoo1YN-neax0*mP=;_t^)ukB z+=aZr%LdYcp(t;wX_WV>_cUqV6k>#~*yAA$M4r!~q+@N(d6j18F|HM)SZLbVL?vSI zeOi4@m}I{bLJ~;Bcfu#o!f69gocPO(T!=#y6C4k%b_-5q9Mm{(Sde~nE2qWT1QuO2 z^$3By7f3MU(lNBca54s`&B6*Lv-sW8XB}vI9m#0pMijeCUeHrB?*151%!vM6KBAZqqzpfp=?Voc+y6N0NU z|E@_I*qJlsv+&+Bj?;BV?WHl1;i+kLK6(CUeN+a$J5pmwoGMz70zds@ENv-!W(+7Y zr=W6J`FA;tv$OkJH7T-jzkEdskEBjhFO+_tepo2&DK^kG@2`Yf&_;8gD^*(tSXw+W zyclos*_`vq362F8e3y?XY}-zqqOFCdEIR`R8!*`D(^qB{T16bL5QjC!y@%-SEvr{k z33+PRA%AUcSBQz~VRX(5c0O&=E{H`7?=gJmNFQsekbpH7PJS-pvdcp+eLg|4z?aQM zEXdPK$WM_iUyqy{?C&zNA`{j6DJS#<CYjeq^J?Hj44NpVkDD- z+agSBpUzk`0*(6d>l?DZ-d}cu#@I^yU07dwh-b9u`BN9Vpie7J2butMYB9X$6+o8LZB?}N zFOG+bB-Jfek<-lA3Q}k9$ZcMDLODH_rLSHQZvmzOQu-*LduCg?6Au*=DzB^5ZH>kt z>|Q7I#&VNT{#ws_<5+Fd)L)x*`AKh;jMeB$6}qS&get-ps3+g~yC-8yIUU;*8E5&d zKL`e`XPOu31kG<~J_xAoi+xV94F~PiUT`=h@Ls&oeA@Pu5sXd@ddhfz5W%Hky>acV zkinS_Z+>?$ZeU5WdUi%SBY5$4{0R$sz3bYgOhpN4O?8JMAH}?c9WuLjh8VFV0*IPacWkU zjovA-yYsSFDkZMlZ8K(xn`-*q$+u;!cul2p*||~v{nw?d%O>b@&9$92xAMcV(R0); z{b#2-Ixfg=*0Aen2thi{EZyg3|=z3t{VVqvN1wV?t_sD35FZ>JW^x)MN4_0pN{t#%5k5?VFpi57-rif7CtKx1uH zAKv^n6??7MI-}+WTi8(WTP#2sN7sxkbc}phh7Yl4Os%iz1J{s=v>){(<-PuMdAPR( zG_vPQ{82~$7jBD=5&eyDMBESo5vCm{NlBnkpDc-q%7v?e6a1?M~Wm6lhpFp&J4|BbkZVbLd^D z^kyXqu&)#1wy`gRb$hYq7n8ypi+0f#oc13oiWpTMJVG+($j3C9d zTl*VwU&L6S6O4aW z%Eh77Y(Zaz7}%-`?Q;cf<|=1tzA({s`iBAm{5UU}6puw`TEL>E`EtpD(31$aKNly3 zD(Iy=V+22qsf4|D5meqdIDD<)xr#lBFP|{re}kodJ|#NpGNo~%(pVWSsL`)#S?YEp z|GNh_EN+|8lsI_~86!z*{}^lOdmA|uedLYxSR%c_={-ypNZpWBfnT@TR(DDcceb>wFb}zIYz;U&G zF<6GN+K`~TW{uNNr}8fGs16*`j5g-vDSmp7iowykd#GZtY$^`0W$q=R6tc3$y@qev zhj$p|g=ITen=E0ojpIt3_8qBwt?=>Bla@D?g-Xp!`N1$ClT{b%J)0p#b7CstiA(jH zoKrwQ#|{kyj9`WY0~39c#lb}02zCZ{LQ3k-BeTxIt4-m+Fhrv-V%N7hai^PW$ez+t z_|3{axejfxzdskinkP_QCmVmFTHi|xd^waVS2&nb43i+nAZu-F$_szqkR#`0(wje zis~+ARD5NyGCfca!f|$;P{p!=DpvC9$0y}MLf28Ty8YWxG&| zJ>7cNHOAO+t|pz?W-FLW;v_}2*qY|+K8?7&RSWZ*9P;(HAADLXCx>w;rpi%>sN#Y= ztD1E}kx+1F2TT1rk{272U6Y#A449-olMLbG10X~1oAKT+rnH*|y>?p20RSt}oY?9FzV`|lA=N<_b?^6=)$7{u7-?ewdt%TqVHaK1;H0VG1? z6{3c3sTAkH`tCXh8mI0uagZb4lz_Ddk8Br3r?~p?!#A^OA?C)69)zwgc13}hVY+vP zdE`S5&*6_8TQ^o^2a%xL<-;n$1p3WDIkj$!V`lHSv-zD_l}9BkTIzZ~#;M5Sc94Xu zC7~vDZ3s2WPC+gCI^WCRJ_qmCokL5H8D&q=h%w=lcoFBeiSf-b7S61QMJk zLtU4bn%ONlQ4)6*7E8URum`;Rf5~(3Ky(5Jp;@&1_owQw!*cF55wFty3!=;`H?Fv&BV6P zA{pk&LdCV)P9}>BboP#CNM{^&aTREW1Fg>s|8Sp2Ax?uWWeog4y57z2rh!mdyh0Q5 znf?9E_O@q-w9caXXi)3mHjaBpnQ{r*LMNfDO#a`p()=1czlNOFEpDtO&}SvjQ94(S zn3D|joCTJR0jLrbDJd*?X{_bpO*afyZIe@3>T}OIc-arqd-WBhyW1-}Z(9~~68)dH z5upZ{7<9(()!?&_5wbkdM-73fM%V(TipSnH+VsgsDHX3;$skLuL3pjL#k#WrODk!_ z*MiR4Gqa<&<0FvycqX{f?0FUOCiW7O3^_=0>}5Y>(|iAS^#YszMyD7&pT1II5>Uq> z6u4OW#h>>ardH^K{fYMP5?wutmrK%mjbH+b(hyr+7+MJ#SP|kXJqU_0YH9fahni{7 z;sWcNs6K!sA#1@#@&>SxV~6dY`|O`!b>}i}p0L}tTf&`JAv`(oUEqWo7DKBv}5Des( zT6-_;e{mGd%?_k9-4=yqGu+3{kWU`f$ZlS>)(~*70h0`!$>Z(srnL}u1p9u>yyquQ z*V=665sk^QX-)k^#U$fPX$bI z;}O43g)&H~YIAb0TqSt4^-MF^D(T0n z=9?`JKI|=X4gUTu{LvLq;*?5v0W;cgP2Y|J-nH;`4efpvqJN$ud6L{M7s(oBAoF1|g)QZzyf8sEPmRcFsVV%&m77vk-uA8)K4 z-d0qRg>!OX>NRd&$;`Q~;=)wq$RtB4u?sPAlG<5ysx?)!ny78B^oD;~!!F0*p9yL& zSeB6fe(sF_d2aUyub)8c_qtE78qkPD>=Kx%ds#e5Z-z0C@j*}<^>C-6GQuIAF1gv0_==ImwBnI1>T-V;a0UU#G;Iu0) zCF4Gc9l{G?uV=^Q$mB7e!fo0b^T<LHPjrAiPvaY6)6Q-$OlRsTfjxYak@gkq8MD zn)KSTQANPThRVkVB8&Ml66=&yP9WRnv~Hh10hcqW&8v3d@AVnI3r6|A1A$kjEnI;g zs|n4>Z?tp~lIl9mQojkr2GjVno;^TXU(^VdU~4yMpf0-+E)#VcyRAOzj_>qt9UWY$ zx&Reo@NqTF;NyQXV}XC@;+IcD8JE>4i|DdoURGt3Dn?#wQg1{k*}cmfS@8lt^7)JZ zIw2NkJYa$vb?%pMJmNW5SHwKBGeP-2s;qG}*f{mYF7liA$ps!%yb|!0cTItIJd}oc~Na z-5;L2L9g{CJ2BF5^J2cL4K>=mb%C$C6oH;GJ~`H*J~I>aPw|Hi;|zu4L3r%vaMWQO zuI$DJ7?W{9vuL31EL}FPQbdg%c|-CYK9z)FR0DlKB?>Y_ zLmWR{3f4$1c1}d*pul`k7wZO&4+t$&38-E6)^4M&J}z4VT|PBWy#*G&o@3vtgYhby z?Ok|AtvvY_U_9-e?J?W8*34z%=|MA1C-d{-)HlAGw_g?$tky!6^IJi9p7$(hhJTCi z3Rn56wmjcz4=VFIjwj9$i5WpP@v8)mKafLt&u!(#Y>;eoCn+8YrcKFN8Ng@dCTOdv z*-sZj(SL7pZD5Njn`^cNU6@-s*Yi;DsvG}Pm4xCU+iNaLQaH79C#00~sFyQDkNFt# zPE|15^c(9K2g?gWGi14;LS`F_h_5jra}U9UxUe+;W!LIHmG7NB`_X4T4H743372~n zwclMADyZvL@@~)+UvLj3Uyc*~VFd!3Tb8$f2PA}CPd@v``?O1HH*W@h0Vksx1et&~ z@cfT}d%E*2^ce=bdm-im9yE1)=YT|D%1GXlsiB5gEWaEcH>|IVm2AiHwNeQ;eC@3z z(77FI__y*rnL%6Hfg)JFXOggOFY3|w@#A&TlbfHn8a{~VtX)j)P8w(g#Dl~G=!|$3 zXl3ye6JHI+i8rncDh{_(8#x-wijOEH5Y24FmN+NxyMhxm{m4*S1@nkotTcXn2NLF7 zyW&qpAx0}=L7$C(datVoH{YN5BN|t@Qj}e-d8@W3QgmyHUzbqa9G0wGjH02+b+m*E z(nz(yGBnCHN@;DVi@O6twI_ejO*t^|uHP5G+i^pq($K{-k8*5>>tRgg^jS>JzKIpe zJ%4l|s&rg&+-RgS=oo>cPET4)3=FN+!6>$*RlGcKVi-})0b2O+4f#^Yud@n9%-!hN z_p!a+1Ur58XLknwB>S+VP+dOgpw!T&3@7ZX!q!u#fa_v#Ys6#e#+3rEJjCbMc|aPl zG|RprYo)O@_OW@Z!h|hMm%Q$ryHVT9hO4A!&#Wccrbqi^3)(?6bXxc}<=e_)+!w*M zAA-j?$j#Z0_g8_L#XMh;LVs#HAV^}jlOO;GgY(c;x#T{lNwxT(4RbY$L&y8NkoNVs zeWB0jwb-T2h0&ePtT^ddHTyf%=|SM!bt}Zo+%ZmACkr$r<(R%P#z^C#dG|u%39N~J zYI1`KZc;kYe+Czj8q_#-(m#U#nL?ij?qu-N`>na5ZJanV-C(;Wcl}b@2_fPA8fQuN zhahZK7~(~oG%Gl$`%Nz7j*sCxJxkbitI#gVX zk)Y9%0%tArJ>^HwNfMgLoZ152iS!@3@_CTb!DruEg={?RiQ9zO-HNaxku~?NJS(d4 z8oeaKQ^_!-Ur^^s-pFD7)RuIAuaA=0lZfuG^8{wpXGu(#EH{ka8RYNA3I*vCpOFrr%qqv(2!(=c!iGQbr zl$XXGlu_?!F0!@cmJ-b!j(z;Vfd$Hv+ol=8n+DWrW3(2Y`2`R&^rZu+g@Xqghp4_HR!U>ANE}uye6fU78p_{zgJ^c;V3u!)f9Zyp|=~~ zieH>*F~d$b%zT5G@4GJ37-NiN>5Iw)Ru(a&Z_Edu9FmBM^+uBI!w*>DVQV0p=1cu{ z*LSvoZLG5DtTDeWA+(05w<#79jVv{q3||l3x~=8H8f(WkptdCKqw>0s?P%%Td|+b0 z+s_~wWK889#iYa|!ktQuPL8h+08?B1q=Zz_wa*wujlyP{do6%I=S~_~-1(~4pg7mV z8#T%;zct=|ZQv-onsRVGkH6kZ6(5aAi<@8P^ys_}Ii>}#a8F8F`1Ow$o(rtrxGgsG z{DI;u9XQE#LLLX;8<)+-rQe&XVTQ+h3v+s@%D<_luM#)B>^m$re2%a8Po__cau`Gm zy60P;cGfKrRRv-wI>{;^q_CJ@V}3LjC2;L=nNaia*czZ@;4dr`J>S1cs(_;#n8x#M zO9<~hrK(SwWi*(XC&lsiz7U4HwY-4GG;-8&71y>QKceH3e0}6Qp`$PTXDhPjK1--? zy_ILFkJuS>FQ>FFN#C{H^A6+FV5%|y^+^+(WiH(9O=YJ4%6h*KUK|n)&mb_Xk}C`N z9s4h1wq0{RI;#UuNM+ip%jQfnR??dD&@3f3>J!9n`sWjY?N+4C#DechSul0SmIOCA zUDrz#U+kWoz7MRH)d9fWx;RLDuO-0K*@p5V@A6EjP!qgBeDJ|L*r6cAhz`*T`dY^q zRri<(6^Ya#Ge zK%=vx>N|x3=3B05{RgdQM_FCDgRNG>ZG)JKURk1gD97m3zPZ9XYUaGk+u z%1XiAj{ux&T|t9q^lYz)&(8qLZ&|?%-L>vx(KWn+*FjW|-Mnn~wT8iGluvv9$B3K# zK&CzWEu~0LHNy41^UEvN3x ztu=EK7R_B#1yl>v{cH2;f}(W*{vmiU4Hz19+iavH>yXR}4+YP_$00d7~mwt^j_Pf4OqgOCS?ziwx*SRN-->~6 zAybtr;NU%3l1FF1gh<00{Y#C8SU^Q62?$ zXwMmnn2SJcYG4@;Bk6%@;uDf@nlKx?H{(5@Ml*%gTVe4#pz#to)OpPYqovAROx~JM zC@${<0>@H66}2-kHE9OA=KeFqo3>AVPQ*5t)&`@VUyueyzww4VywQqM0u{ZE8Y)zh zL%u4YJ`W=!Lc4mN?T8Qi0*TNhc>lzFiZQ3emr=@NW7nuxq38`!V+e`Fv+EAE>f(7| zqn>Sn1A=1`-AZo}!1b%I+KZG=ul;~WfoC%;y* zV2}u1?0|n91&Cb6tLcGNFkTdxTlG@D#FZ?~VTQo558yx18vHTL8DfAVk9}Js!s1Pq z3UCeIgPuZ_cptJ_R>@KA`+aIp!J)SY4isoz_88~Q?Yp$U=gzVAmuT1Ys={6+^^Nrh z=Bezw`J8b|(@%Bw&j`7+Rx?&@0pdzih8PIIueT-Px|O z%__up7{V+nPS_D|e=J`Prk7YDzRgrk@^F*MeoN*+6mAJw?GJfO>R!C*1pxw6uAY@d*L+(@?kWtiG9u}zP*cshVJ5B_dTQDn|&TRGNZTWUYC_WNzFKkOY1Z{M2s5af$-K#6DW)1A~{&g;qnTZeDFyuEq)_=?# zTzH_=+m^O4?z~c{uOtlL6{}ABgQB4@c&0@stxqeN`ny*RQk6;Hn`D;*s$yIcp1k?( zg;V}b6_$Eqp4ZWgu5nAt=|#v2(4oH8s!Te}As14%fvJ#$H6G5B<2GwPEZxgpI^aId zAKIoKcSW^)YMN3n95=mbz`hXhBwVwjh-8SK0>!xjXhDZnXtmb8n__IS55zhft7@NQ zDT6baltAMkIGkY_BFY6b30gH+)NP7Y{`Pv-&Pj%o$6{PLR000ABUO!*hv!o~;L%wi z18}_@?69>%^~NdPPZv|Oo{@r!uc-D5J@Gr$_X5~*+Q7H1fQJf71ZU-&|t=bX8Jd)=JS{Wj#r{QoF5^Nq#`mt!CP87eJF#`{w zF*EE?Pfy%qQ3?UwugCTpR1$o!&$pQcFw8)(eE8~kfpFu6cbe{x&jJW63edEnGqOTM zJr*cY9lk4!6F5CI6Q&(ZH5~y#D>te)y3H zp%e@=RzZ&(w6p@9?0{IH?&=$LxazF;_iu#{dV7uke5>0q6TKyvj7as`e0v95qSbJ? zOT#kQ+qwJN{8}X@A_OgzjtaDP>5UZHDm6Thenv$<)tDU(_jmamc`jyKabgypDa_Wr zAahT~wF;bsdSj)5oTvd*8}Trkiy?&7*H)kWKiGTAxTw}WY!+Qqr znT#_AO1t7vk-^j;0vvj&lE0?E4}@FklykRlnnuM~@G*>6H@1CU<^J4ZbI6EE)jf2c zw{BM8X##x>2>mgswyy_6%+z8kC4VW z9l1ia{T-Mb%9Y%P18^=!K;-ztxqBZ8;tt1`A7B4rw?Sy1*W7N0PKg)tpRX|P@2M`Q z<<}``8=B$_!r^~_ooeT!q%ZrN3UB-F7F~Qv8y3VMox9We3@mCHI=to}>kvFqMBNS? z`K*?_p&;XF^7-23KIFP%p_~B7eb&+7b`FdwuO$Hd*fE~VC!MVXq)snl@s0V(^kGdx zSBN_UNo8H?wWww{JB3Rge*3l&Bb)SCKMJeOCBD0_C@*#hM!|5SqbE{SJ57IF9k+zE?p+07kKBE#`+7ty6U08@+1w zIjMF(mdh9BWF0?;DK|YmebJqo^hbuWRB%khgN-r_n7U6MJgUx(Fxp7F2~PM4f4jXD zOLUM-FSBLT4N@irQShw4mz>G$Dt(-)nc8LjVYo(dTmEwhuAGFsMyM^Znp=L}Su-}0 z_Ws5-p>$&;Zq1CCR7^gC#&9ue7e3`h|Ew7DxogYCkQIciAx7&YG7+$YECV1rB&qz>)$mmA%kuUN^^cS&- z$BpYhAWp2YO`y0zxMRi*XQ=AY)OeHW73am6=qoi7KDe(jpG7@vcow|=Lg7M#ppq~f zm68O%we&eg-L29*55%Q(>jH2)Ci^?wKuJ`cnoI5M87*scQ8~Re?A32n<9}5YBZwW)pEfBvtlFD8NYQ!H zxj)(2A4L#%_$kUm=ms;CgKfVDHC*=y%2T^;e=R(KZR`I0eY~9 zaznIpTTe<<&megFYZRA*zsXk)%D1zf}rfViR#p!`{hzhGQ^Em`HShX`({;F%9uv1-PR zJ{xUS<6^}?kr&Z=dx2&=b3JH=E1{HNL_i?r|H}{IH<%W>8H!U5Ami{l|5VZ53MWa+ zE*%n#&w0P_Xp!Zc)%C z$SMENGejsOB$>&vZjR5rp&uqx>a(3vy;rNhjC=po*2R6E8I&ck!T;kq2r-tRgX|Xg z5KEBX_%Q~`Z(MVl16=^DF=p7%%SfHV_$Z#h!~fG{34FT`g2_RN6&_~;H6ueXm!FJJ z6RHK7vjo{1VIO9UjBC*U@x%6D6Yop3!-Wkm%_>4?MeBR>uyIf|7isD7m){BV|Mqx^ zUu9|2aH1UooA5?iF{tEIJm_aNRs&k_Hm_m4-Db_vIBCjNWP#oZMbBTUFoCiKYiFB} z?X5PB)B-hBF1Uh6?-Ix##^xIlD$3=vI>@4Iqt^3xZuw2^s0VEmu!(;%L`+T#Eo7il z26~3fnb???(4rxnopJjzTUAQjP^K>NqZ?#T&mj5$OrKxLXiQ5_^}igT?dL4gbuKq~y3{X{4*B)JL1 z5Tq`mT|6TRtT?VSMSrs{Zwa~#?IFU;yW^Gg7ZK1%r)pFIAB_R0J&L5e?ZAmImK z&rx7b?f_lb7fs-o_`NRq3Kn^2{GNSSf*t-Qxbbf@AENw|;8NJ`4^R4Q?p`rUD7QuR zOG`iAQU608;$Oc)E=!9r*HoGO$i1q){oMvj{sH}?ydZZd$zL4(U%3G#=`aXtNNUnc zymM^P3Zr#t+<&?|u>?EDvj4?LhJGo-QM>`c-(Dy_D?<<#GYWwRg+Ds^O{w8u$Nr(b zosdyk2K(2oO#VuSjO!j=@LLPIzX776Dlu9o6ls68TP(W#W>aFeceOvykv5^uYupZJ5Fk9_4{=6(q;N<;+1f?q+33=cNP_} z3Hl0tgz`)~i2Y1$N*Z=P?1GB&I5%v?t|0nl54dWn{$mPyl0m~)7Eahl+yr&7yr>`o zo_e?OC;L(S_ZShJX`vp?KhX>0OqLmpekn-8tl+VmwHqht}%|1oX0LxjeJ z2oxlLjPO(}@NH9GlAZET`2k`JDlqp?7GV?T|HCc`JNznIM=JHnXdZ$r&ACqW)`Qa;-kb@SwQGeNXx6!IkR+uvAG zBJkcuEMCkJ@X(U0B8|&oNJ7Jlj=)tnD%snmBTjonp#CDO z$3QtK3zOb^obGpH`)@XQ0^wuq6a+IN?m3V|kji;qwO$KS>gg~hn|bCS?rL<}f+flUx_kpMGs1H+(%Ohs={{9g&;eY#xndE@<|2<;n-@~2Xjip@w{~j^(CtC>q`Ty24o@P!Q zu+1m@1xxPid_lo-Rbt6Ommg~+1=GwE-~YjsQd)rC_#`VO?aApswVC~f6VF?|ar z75?qlAQ9kF$Qj~4{r8VI91fDnUGV1k=iC2y;nzi4OjwC;5mTN1w}1ZQ%B?w5XL^Z7 zUc{JIQvLpAe|jU?ssD0uIWv+EXQinBeYF3$zz5f}WqEom#Khd_;2%l zNe75(ITiB9;{3~)2)~j=0MUQotAb!vCH=&t($2)7;^}}$L4kDihMz(-?MtdCS3y8XIn|NM9Npc{e8v73eNg$ z8`TN|qN=Nozp|$fzs*IP&kT*bu=0&t)z01%+xxM8a*&5Re*z8i$Jwt+v*QQW^9-DX z8uuz5euYHolLmIAd3<+mWk0svQ)fTW+iW^nn`CDQ++Aiqk2@SZZat$jA%?=I25l&P z30NPf94eZA1L>2M-IwM=9DYXxiKhH+Fq9s$GXNz-_n%GL_-uw2*_#~oQlXC-o%Ut+ zKz$MhXXNzMfdO7!p-FJPW7vevxpsN2_2f96${H$Zc&is*IkxhmKBTy2TF{}U_y~5g zEZwRe@N>C!E@&%7skpA9xGBID8{kEStU`Hp)EvyJ2RVC>YU`@*Oz}7EO}Iu(;#iJl zPc}o!r%#xtr!Bqvq`hKx0+D((0h`sLWAh!5>GN|{FvoJwO+Uwm{u0QE;~&Q_>Ya=L zup)7BZL=Al1#@h&^;s(5!W`>6xq&ydWT7@9YaFzbGt1SySN(`Sm^656+PG$vGO!Sly9P~SxJ~^D4 zC5m8`Ib4P{G$Dy*e$>@(f9;CLW2=XaB)ztN8&HesT7W4qvemBgG?AuUAy&$M4K_%J z3nBU-GZcDcQEB2ly#J#Qa6KOHs&OSU9Xv0)J5pT&3o83*{iS)t4cdf09)0(~Lj0DP zi;bB_w3TG+&XnpjHKkB`wp0DWP#XHIFkr}H00!rU^Uo^e-H!21zjkL7+ja=i%tZu; z2^M0Kj<>091Ba0hZN{?qNmW)9EeZgsCpBd|T)@?AiRB5%Fx<%;s4F|dgZ+T>C$p#6 z-xp}2%U3q1Fh(A;s;x-4Q?p~l5Ka^@Og$O~N89oE*G9P{y>{wU3tO$&l(*F$4twN^ zp%Fm8d5^QH$-%b(kC`1lxmGQXY!;s!s_4fU7C%t2aLe=wvQ7#3b`7wU>7oiafPC4N zl`V5#c3GAU@@|?fENv_s@)iONM~?*Xx-}*kyM2I2XtoZ&6nq zw!ElRc;B-QdZ$IF0r87e01b~H*S2b&S`JG^=7!&G-P-^b*qorxqhdnxHVy% zF8uai4nMtAShSaQ#AtWQW(_v_4!k2dr|p1gQ`*)6w*?c~&6l$VE{&qNKATFa>y!Wz zu@PhqyZ23gadUcV=Xf2r^MNx~&w+vo;B!0)T-0Q~8go<{gdbVYh+yl~IUl%SFijQ1 z=DjyNiaUa`JHDZ$9;c(dEO)q_h!k`?YX{Vc<4cKV4xt&p_6xV~#2M0HX=klT#hba; z_y>lMJ}buG<*(lD%%Vz|{h4u()hL_^&+6Z~o@8=TpW2d2`)GIwSXXyx($R5Fd$#+2 zA@rLcHfY9m`o(!Z>@E!Qk))OO<{DP&Jx+Y};~6Y_y7y9n6-lei85UuwgHW1qv1#|M z_0^BdnoWE|K2G77jevak44%bjVxtwp-{@NBzF;AQKi?;_)!=S^$Qdi`TsM4th9bzb zGo#f6=KerpRFsyfa!f`~e0Dy*Yq*0iqm|JHW)#>Lw)^o=j$CAzo#;movZ=t(sQSS9 z47<<#dN-u^#^eze*;qX@?V{$4^jms48>mPCO*mx~GlHrmo%H5jke5gPSdP};^bm%rVx zN!19K;FmgabRm}8-Dp@tR7#P~c%3Lv-NXg)bRRUIB#v*^J4D=L4U*p<{b>^j;j5ef zQ4T#RYGh6$FAJ)bh6S$%EC*cQKZG9Q=|5p+ulkz6iLnhQ>Ae9TAB>_LR_psVMMQyl zuXOu+j_!e<5f?f_(UfKSMZ~Xsh_z$S4DK^v?xk9|K5F$@$;P+IlLU{9`XtCqt z0|)*@`laQPld#u5l;h!@BIDN*O_KfDjpHpmK(M)v_LN;;Q)OT4cb7+!yi(VPWXThx zbxr)#^7>kvS(|jm-2}&|o)orj?rTDi@~8qF2QAj>mSbmeL7Q8z&FW;wtw$$_TMYNw z8tsf?wNFn@AERk_{Ka~ zfR4}M*B&$fR<$6Z&oWHXvFio<2cb~~9TnX+m)Xuz&?iiTPLwG+B30KOQidgzQ zu_gZgfi!lro0=xPR*_y9LB`GJKix2u*CF$B*7EHK(>FkznZRM1>FphyK1p!s1JqVs zoONva1cs~Ip}|{4*~9j~22sHLb_*V{As*Y2f6c@~K-cd1 ztZ&lmIZ|aRVy3do&$V3q%kHeauMXEpzpA3TIBgRjeX!(8GiD^dM`)NyTO^(|xXz-a z1{yS5_yq}N1}>VH!*YzH11h`1+_#OejriDgpHL=LPog2GH=KBHqUOB~&>3^__)&uM z5`!QXh$E}c57Oi2@@1Yk$n6f@iq^0P!?|7(u(Ne7f>0B|2HnT-OCgMPrq7ZVG zW+4wYz`HYj=+e+w`L&Vo*~EKlsVboZLv6T6;ki% zq`DWNmYXR|_M#O9>%D1b8#$vZ0R&(`GnsACZQxFml=@KHC%skWfrq@DD_zsDsO0J!|K8n()z|(~^khci za-I?ybJ_l>KwNX)*md6?yHhPG&dud4C8@qr3NB+j%I>q4lbYie#~Ux$DHYZ;^>G=jaMO-d%mj@K@bI7+~xbHVchbmG#t4X-_=lpq5bKi-a> zyVIYWoh$U-6X;wFoiG=gtIB5@_`@6(|C;Ahw=cLZMZAq4z44@q6XyyUTGXioC= z7oN|xM^V2AADgm2Kc}f}Yv@h$Xo2TzQfiWqPhH(C@wGd4SsU*=&H)8g-A0aTG{9|s z0}u1BUbZQHH^j~fJAuLTI|aji)lf5vn4YlrS{HMtvR3Ecmj&-P<$wKgHE#cS3~g)K zn}fzvKf_#S#WY;JLlY|`%R#G!AUhAg{A4hU}8hMKPHpKo@LAeLYhBcPH-6XuVZn^vJ(^-O?~o$ z`O!*>b~Kvd`UqUM#GZv}xwPYx48lGSd%S0}fMrn7-*2)Q**PP#fi3Pxbt9Gwgt?2I z)greC)3t$bW7zi9QB@rS9SVm0jx<1SE0^%oY?RFYUqIC4;v)nR4 z_E#Ukh+BFRSK}s0O^(*ua~|$qqqDI~i58&;@vE+m=4;Nq(W4O(?>47UbNcu5_YYFV z266WSZ@vkaJwnVMcgZX53|pL5*gj^_W${yrU0RkaTNQP5*XZxj(o2F- zw2`Ey-__pe!)6%at0P%NYu%yKmduIw%5MU61797%4HroQ?W~!XC|IzH~sI z(-q(83Da64zDf_cmnW0)oMX>yyKc*T;F zN?qRi2cY{4*)&~lt!o7F^7V4koYpRQpgZBVA>gcS>wHp*R=rYZEpto0*dCd>EuV~) z>=lfA_T4V;I!s|v!g+xa&-?MwmITFqS6Fx`XF-7L6p?8_Cnx>rEkMJGv$?;KSdOnG ztM99l58Paw%m?upjqSm#`@Lh%?WJr%L-cHUeEqjmF03B54_h@d)OnEL=NZ|i{kb(5mo|&hDq1pkyf_8 z&V!Skmtc~fM6^c!7nv|T9d(`2d@LzuoL&bV1%WG1^d$?UrfriLh`bUpl|5P5AyHnU zgiMqhKW#ULSPp(g^X^CCn5FMpN}6ldx{onQJIy;X@%~cy2hKTqu^Om0NBcS( z6XF)W==@tAKxdpf-LC1Kb2>4sUiW>4-@Xt0sz%g;y{o11Ia8>|XQIxIE}&$h{07;r z{5X?*fZ7DL5l6N>frZ#fXzI&41D#qFM)Z0a^3w1!M}sh0ley3|Z8^zDXGa=8e#G7# zo^8eqexUQ#Xe3D?&_mW+jf2PBxquv-ekac z^MU76q2QN1q)+ILD857*H|+bgGoDvy8xaRhTx1^@LC_K*9#o*IV+`krZ{7*v6#pXa zg=yNoaWjwMljz8ST2wVaCfqHTM-(CP=Bdg%vp^Ki%xbQtkB(LL9DnlQw}7(% z{nB$;G9b^1BF$mSvBg76zis2gYUL9IeUk~KtuLPp_KuuswDjz%cy49nMfx8seiDJ` z-7j{;E!=e?&w^Ppp zWNU-z1}#xrxDlML=Bf+ADUFHfTOt@bek;U23!Ia`4i?5bAv=ovz#nKXKgPqmuM2B$%(&Fd2f$;cI!4z}rKUG~lmf=qUOkd7@4Bqh?};V5pr2 zOhnz)orq)WdJh;uWv??&LkrcD_-!XvX|?HGhu@o)>RXAaL=(%Q-`e@{an))hwS+BI z!D5E{dMx@4d*Wx!P!^DzaGe;RGy7A7T#=Ci^0CMDlZCpJo8?)*Dm^(!Z)d)e!brpE zX>vxu9Yr11i{MG}jYcbvdvp~`vY%RexM?FX`$jHzzZH;;B7F)W)=~QT;dwd1i ztv^%?2#8QBnw*mR^j-_;Y8aW`x{RnMnneY>MTDWpgo!w`(f!>Y zRX)fR0s`6yHZ{fzF@_v~ou5prRMwGLEyMl6z>|so1?*HdZAka%eZ6>Jr{`0P4W6vu zS*s`fYm!zR0EU>V4`g*+KE7HSqn?pY26Kn6W|vSd_Ty6D`p*-w)n2#_;k~HcwBJF`FbUh0$Gjvv;qAm9cw2a4h(hyL~xt~bZY{x{usF8C9FF?=k#LIM$ z4wpCR1_Wwhk2k(89G!dIpo3B*nvplYf15(gN=Af2{D6<*%)=(pk8WB%LkpD}+6+6g z5QAM_VC9bICosm`6+^#PZ5p=HHA3x^Dh$XREx33jUw>iot$rEPnn9>GEZireyI>AW zvF2+7kHzFWY>Gdh7=L}Em?&7Bc$P-~ei}LPme{FdVIq^#NTJhSQj@oxk|_6CWGt>g znLB~$Sqi>5mX0iWH}}5Bt)B`&=h>HwUUd#_w!%*>-UL(jht@hmoS&7$Fm$CE)jcFl zaYP(u?EP&5*c~u5*GSK*k%&PX)%<=4AU_G^%vJwe!2-RXD4ooxw%odT8Y9<|pJ?c) z#io&+6B~8ha=)*%bzZoYq;S}ZFW(KGnTd$6FJV)09Tq9-Bw;RC`m6$05atxiBv^_b zX4&UIq!z8#E3&Zf}DWv03% zXy~C#`xr&NdEi#Dnt7|<*0FbdAOa}!$+F6*?Yp{qbNV2#CMunotG`6FC(Ld5fd*V^ z96hYo6aQ5zZo<$p<(-)hk!cmrZHWFeAD8e4y*$7xx;5LHiHV|q(VFq>^a9bWi;iGX zajlAs7r9>$7CUsbLN6)FoVS1uQ0q}2i2&k$-p=exeR^n-`jeynwBw45(&jPh3fIZjspZ}4z^muI2VvV_GPonwn)#Tw$p^3rNuAq~q71ffbrC zYH!|WK<9%ca!*I+bWPR%{Klx>MEAl^l}S&R%5M!lfn}Ocv~P&+)Q2kGumN2Q1hVXD zWE(U6+3l2Q?73I|m*2vM2Fyu<4@-nD+Klx%{KrIo;f)edTV{$<6Pn#B!QbLPIi6%c zah#tn(8&*<$<`T}Q%s0gM6W&6K2jWvrZ~4!|HBF{ef*izL6u~tZZHP*4F6MS{A%Hw zdP8C5XdXo()8Tkqy5{@N9mO>&#(S?Ivt6#0+*B4+f+jb$DY3N$I!I!KVW6$z@l25y@ zvIGS1LPb}lN-d@D&sZ~ywilnxo~`6Poq8R}c9-2TbUyGrm*T!v&ZkFWYHb!_$DH9% zNgNYa4Dy^(QctH(2rv4kb{F4brd*EE5syXRM(efP6pGP>+psRIw#IKLwY0GFz7GLyzvYPmYE>Th;Lm%bORXp{K8_$gf zQ8ekR=IJQfNw{%VioOxu6F7XduW9|$=12!+LP_hEUaD;damz%bE7%}_EN`8=0oHxG ziy4A;eK3!}4IKVa!~4=vuX3>j{EC>106CG;G{0wwQnl}vSmN~Pfw?B)gXq(`A}j=0 zYxYMLpx0DM98nZe{>{DpMWo)e43nQFVmE)2Rbw*dtm z%m81k({C*%Rzk3UMlTRIKtt1W>U*`utUzI3GE>FpmuB`v@dqq{q1-)#!S6NWXd(w} ztn)Ru8TG70c38r7JoL{Ain*vk+Irf+f{}XRmrq`!ez5iI=>1DJc2N8KJeTt8qjgfV zE+jvr1R6pGLVaKR==81LF7Wr4w&gN9mKL%Ev;T3)W7$joWv4I5S%8?MjW*dl|U=goxS9Z$3U_ z6lE@bn5%r9{x&erB2@pn#qEXtVE*$x%sAge8L&CI;rJ`W|FHOt5F-Z5E9rPV7l>|G zX)ls-@EG=L>I<@}4w@^0jHew9Y$YY;vLy>MbIGE-Rx3=$cizWO;$V%$;Ttv2anHSoK6<#)U&s8UZMaby9fh|V5 z0Uafk9Myr7f&>VL5m2TbH!M1`j4rO+$_}5F)y$wNW-)XL%`U)Pd>}?oVrrnM-H!NL zW}9$OEdpe-k0QC6W^&y5mtbg?P>TMm^9xCVn?LiY)TMR?Hj`BM67c@0rSHPeQ^B&N z$=XEmbMAW7N15+Z&0fXF2Ax1$dt{gjQV2P!WrjRndRPq2xgvJZ*zGpHv!!fC9su+8 z_o%F9l79VEYW}TIuik1o3nr`0!FFOhN(KU^ecT+aHU=UA!W|Bm%9L&@R=#nC_bvCQ zD`JJfeqmb$s{uY7(!Ll_@U}-9CuFh@eF=^DquY!+AeDlKQ9}qlp&&wABaFTm* zh||FZ_OB;W6+_DqxXkB#aDh(VrMnglIq44vt-$^JY_Gh-n*Uwq{^3Z;PR1L=pqhTM zIL=7Vi{!=I?y4=Jb+vy^ZeNtHS-pN=*oB{$o=sUycq}lbp>M0X znbqFd47MmxnbxYX{vk0ui|b==HsPOYMQAKpAR^?oePv1AA6Zx74%nqn^?Ee{y>2!r zUfPz{#eFNo+*Ai90sl}a#`ih7FAL6pXQNo`QSX?Hg7b6pHTPV!^wsT`;q(_~vY&^y zodvRdYBS@l9vwnxhA?~w?h0iLftM&HX0u(vO7k~~YNpHdmKr1ROlvRStLQZMq|Mw5 zA%RFMK|B2I_e8XXQAXH-R9!;~W%?}&_Sm&D&CB#*qRjpQS?^bd!N$_*EtJwvLKFVl z&7ia1Sw1h^=`FTEzgc1wYb6|h=;_mwr7nKipOP<5b5IE?eSBYHo_~iWkUY6Hs8Ctp zf;8s)O@5Qr%tVQ%_DR@KI1rZ+i=q^C&do*>U+wNGo=>j7P`zKl`Ubkt1i_p`z)L3v zNKu*hPxlzrup+%sF+Fg%A`aCIb6&DCdLdvQZC#}ffp9aWcQsa`Fi<+1DCePf0f!|x zszi2gSMUCp!MyOY>wd4PG+$4lkRC%2A^&6(%v_UxE@E`&v}@Q>+;Lvsyhx2TQt z(p!aT5l3lJ`z4scZguG|qJJ!88tA25dnHw?Hm}j+owPX%b{Arr#Rl!)7>#|r$2RJF zWyDae)*i8*XKzF&JYy+Mrf5sSY_%rH15JX(ke5s-DKBL@Oc8N#X`|#>4ZkV3^7xdZ z{iAo3O{i5j_I)VP-z>aMoD=G`wB`ALBHeN}w-q<51kSCA1sVET1s)*!`lstv3y-{; z*hh>}Z4O(ZZ@(-pXOP`JLz|6)oP|o1TkBn-*FPbBQn{w$v^BKTf8&IHkWzigInO?aggIieN(3%nuZh(R{nvvCPGy1Jsr9y5U zBEhW}=^*03szr_aE&1iFCE2HZ-{$0@1*9V~jV71$hR1&$PN0zavbY(t<&X`3Y`=F5 z?I?V|RjXy|*>Xy_NZFi)pqpgPe@@yAt=nuw;Ob*9QLHIa9`ulP+10!9LzO8}=Wy zNfxv9%Xt_|M{*!5EYS5u#({t-`jpM(H#HifsfB_rZXV~88;HK*D~gx!{CK||LuH+L zp8X~47cG7YyFS)_uBHwz_BFDdjCVZN!MEPJ48EF0aY zrme8nksckps908fJ32n61U6Ukt|8lqnXBH=AIA%1r+H~esbAuP#&`3JIM00+6VX+> z6zIu#MW**_-)U0>WIllww*?Mby}qEc!)9Yvu_&%M$gdFCaqh$&QpD>4MC$jRu|{&7pCgkh; z?ify#t6T3*hlH5*+Sn3eLaK8WpL8t}UCUd|@PIEHjTXY10d)dMrQhH&cXmO0$x{9p zL38vN&3F1~gy?DdXA_g@gEoCfofy8FI?i;-3o4W%PAT+G$?>+~yN}fvnvTIj1e2G2 z)>kT>%vb66s=s6(RV*pun%Ir2@3>VearHI2ID4yAv1WcHY3AhmG8g#lQ8440{54~O zm+M-sRiz~cfpaoXN@}we5$zEIio!dTX<%Mui2QR$;7&3YXH?9@%`Fr38090&``v|U`{?cMH+!K zWt%ok*cBfa6j4&HjDh%nos68xCA(=^+eDgh|FpkWUf)e1+fAm0n6iI+!x+#0?PpWI zJXGf`Yi$f9uwxfW7nL+`t?PXEeKTpWurl>4KbrR`94x~Pq``>SEKV^zvbj|+kJD55 z-vqe70m`IepD`$1;00Q%A8vb{Y6-(rRuC#6a)QcW8LIb!s&MG5=&yD<`>(D$*m#c? z$@rB#FK}`TH>^Ibkk)>5%g|pr`oX+B=_B{C@UHF4fF-fJe9i!*K&O+}_miPufn=9a zpXMXZP&hTx!B{_B4THDOoileI4@d{kebiI*E;H^2Sz!T!6VCvA*SnnD!u#}X zWC9D9j}fROadl~i zfX;ydCk>yLf)_M|o2x?O8hma$U%M@)!yw;PrsK@g1C zr~cckB2~%XtW$BBOJ2#Qi5Z_MT2tr!+)s%l)BB{pV~+WT0pgUW!B&4mSQ-0qY06Ck z)KibsYYhavxRXVcOh>2eX_MTb^+CCA7y_LLUd=Mqi@-%{ArC@#KBzZ|}H)?2V&yw|_znX*rY=q>8Fi)E{Y&whb5YfcY3_$4}+N*QR~M8c+s2l1$)?wOE^KG$a(LAw^Ejh zxLD2-bZK{DU(b1o?UP{dDg+{X@o>X$ywUBm580q+Pv3BjxiFqzbHTN@Yu`ke1<43} zKf8JM-gvIq9*E20Ssm)5mteOMe2w-J0aAMezyE4fwY?261-LV>d2yIsS%~Iqs%k8= zD#=oS-l;bfj3pOBBj4jNDK-`wB#)RR%(8;?D1v9+!b!rN%a)vnhwaIMWytFK!D%Ti z>heg^gs}e5CVw6H`SW*vL{^js?AO*r+wo9JAkasQHN!j71@J4|xutb_nH$LcTHES& zH)gJH{(R_W$(6;dhYhFsiY7Wq~L1XL%;C}i8q1g7Yo6)V0bG|&N**f;`7uM-f zTRl^kI_UQ}pku;Tb*NXSD=;)DyiS)sr{9?!?s#tACqSM7&E3_*X}j#2TEBq4eQQQ6 z^5}hF&Zp`we>)=u^H@d}NCEm2$=X@E(b}^!9q(Vb>%?Hc^e{btoO_Bx(Ugdr=gnAb z2rBbjH}4&O$RXcF*EEkY$?-RHvI_TYL~7j!_1;DgUweG@F`Ukx!_vx^vSv$xO1g|= zJYkyA`OZqczmLTBTigP2qDp%AGmS2ZW_QpSMHU14POpnSyFoPvljh@ZC$B!tFi3uR z?vGgV<_VOrnN5wvnFZyjk1J^D^t{w4xcjh=bf2GF-?HK6onf?HmF%?>m4_+rgoWJh{- zFDvRYaj2H3#GtS2w5aj=%M_ihW8)gHgJ6y-L`=R;dUodEZ_UeJy_O;H%4@D`u1rat z!9*YMy<;_4MeGlUj2=_F{F5k0E;HAIVK6NG^G9r-r*NgJw?+A?cRIn z1Opt+!bHZ_G23q$CxQGCV*U~Y8~?_>(4(OQJxCDq$N>#d@q{~mq4Z01H8>o7X44%r z9X-}Ora0QSUK19j1-gGELB9rXDnOX+wM%Q4z&QbKek{*)s4V_jSP$aJJAuOJ;o`~% zLfVgtsK3HX$564PKN1x6S3SUljIHFP+U30Lb^&Gh$=*lkyTXnV+T0MA=>~P_ef+QfZ&b3NDLe}4hp9UV9al#UZc720D-No7F4%X(et+VZg z9gXYXE58JKuW!&6N|!op)mj}l{{|lZ{Z8e|0YZ4isrTaJveljjl}u*MPU$mtnVpq* z$VqoibuHC1am#%k=s0^--$1F?lr}vTa;K}OVhve2iMCo7@4{|?l#y4B9i@HJy!+zB zDaE_vy!(hX|Cy5VX|K@`KkyD}@K_1l>S(p}dS^hZCjT89ZNHBj4-|c)G(+D9dx3e*N_OVlmO`*3g-|Bo7@_4^z)}gdX zE1WF_+qBg=_l~trcOT~y6gWOMcm`wagW`Fiwc51z9RiMsHq>+2#`swvW4CsK*Wrn; z=S*&ci&$kw6bPU3l{-0Ntx56~uOKoFI@LncEA3=yAhhiVRsho9-xGq9LO)^oh-S9< zr44i#L=BG0$JxqkHU)b{Bw-fte}>bb3FfA&Bfy!1Uj@&dk#v-BH}l3tHN-;$76$1f z3|1;1>QT0Rt3GI(ph5ao9pdik%G{RP|0L9fAW^?_@@lpiH>D#ZibEV9?y^T9oA5)k zE_~t^-iu#4&O@b_=Vz6M4e5-lHl>%aJxaGO_YBq=ib?(0Hc6!AwJ~w~sAzVf6fz!A zOQnujFWQu3IZUw#?|?y;){_lNvBF~Xw9Wws$?W-lk2u7=L4H*2uRtVd1!C>yZ?8`@4QXNuBH*hskEs$nc|vy$Sc}R-AsKiGcrRLB~^kq{Lcp^WpPZv%E9`sm*Pg z2;C|Fp7mle6QgfH95avXXuv3ZSY|OhD=)A|3%Eb$o$Pz4<~4ayC*>Ikbk_-5scKFB zx1$R};~cbp%c>0B`Ug4btqm8^Z0dCyK~f1SJJG{v60?BYcwf%X98&k2&U{ojx>7gQ zT)9Hus>yf9ullQkeEGrL*{Y<%vVH$gy>8*EGBum82YJ|+o#xVN_!OdT^X&sl>a96L zeRlT;UtE43bugEFxqDyNL~0Y42Qxw}xrBP}EL|q*oF zfW5{(@=iJk42|iLt7E&$?so7}2-aW7c*pQrYXZ2_?`j|I>`JVCo1Y8Bb<-?(nct;G z+cc~vKeYMI?nJ~vj$KdRAyeEXIxe;^`Y!DRbo%=i^aU*5=(wgJw=4VH5gw-5XCceU zkUA(cIKlNKrcEOm&))HVapP9(1(LlLuI~L2|6XgqRjnxwMy@@kybI<-CVq5<2KPV! znwPbZWwF?)mQmI&$6q{(f0#l#u;&FPSj4o~cgUr5T)U;y1wOF#UH}9lOnHC__x+Ml z@@IT8+V2PT1@$7aQ4&Fl)I99VO96oQ?&? zDPN`GPx1nK!1p=MivT8BC0=C4UFY{^oo`%y|izSvJSewDY50*%kQ@*WG4bW@TK~wk!Z^iF%y^vw1KB zePcZ_{`6AB!hrw_N7Agsp(L#yo;B!mCuBLnzXw=%G3&b9D!GQtax`3o8c4MNxVVF1 z@!GwyKq3)>S;bAt-1bQR`hlnW>da{QQ0X9q@nX+61^M)Y=^9B}*z3o(Ao7cL&GkZG zoMgEE)KQCMYXk0%ug5I?6z(+_J6|DJih{A&xL=-jYskJ!)AQ&+j;mK9%{9V(BQNd1IIx*|-TvKO@LRAnL^?t%GqxB9Q-Jy;c#!SP9u5~-tSeM;>UbsH#f$3>g!;J)9?X)<%R)xQ${X+HJH;Sr) z+w;?QBgIn@8M8Z&6SdPBkNXZi9I*##r0}yQ-6q?6a+h3T4}>B|?;RX^YS|8+^tyJ}U{%C5 zg3c>QEeF{&SMgZReM*a~Onz8=+<2re?p{57{Z*lCxI`~VZ?e4aQ2FWQ?tqYb3Eex< z>4^RB%C#M73rL!pd14vkJhm3<=H5>{N9@YD1nfdYcvKy%`+TbK&2bJuU2;wr4>;s=NumNbV-DsMP)?9qjCttvog(@A#+S? zC<=)Z3FTA{+iZ)Rk{K#ekr;AJ*j|LB<*{Yndp+0lzVW_!|9$_kzwYbWb^meye&56A zd-{B{Eu0U`CyX`itWbm~auJKclP-(knO)!T%Ew_5-$3V}^+av)AV1&`N#cwwaI#K+ zr7HiGD>1_9YUVoLX4y)PzjkSS4FnKz$22vF6|Z?Swj)|?_QA8S1w`1n-o?53>Owa9 zvAN!M&D|n^Xmva-&ngdbJ5bap4+SPy}8r1xYbf!|fN(nq$rH}SfKz(FCCw`L-^W?Q? zL$$Gtgwf+23S}a|!u)wRRM|r*b2UK4(}!lSv!x_jfdH-(pe<33u?8Yo?0F})lt7W1 zVOCJK%&&l}UjqkSB<}}j797GK`kdYpf;9`v`@br86pSAGWK zI6%)mTh5!2%NGGWLN7dA4qBYPPj&Y|*}}omNg8clqQXRt_nDCQkR!TpNEeCBedu#B zyTjsSj5#(C#VCt89Eg}EUD>EksMkbWu}ounINU{6$x$cANA%PM)rV$+!*-nr@RRa~ ziN~_TZG4i&%LhaaP#>pkDFIpc+cyL&aOEolh*!nc%A^FN(BASEUsFIZtd+t`cLRE0 zw-t`~sex-wJWDwti`9&MBksX_h?50yi`oVwRZ9b({n_!I6U;`F?lZKBp-<0Y`@qU4`r2|J)K$s{i}j|S zq8<^<>Y;LYAv{}thcaVJhdW?qVo~^Mylz+C((uWt1o!>2bBqT9^aOIXZ~1(QW#o`5 za9_*cxmH_?k$&tUEyKUe@|1Bv@pLhwuai}l=>|BgzmHM+sGH!`X*@zMsWxk=n#yfb zkPFvmrXoqW%XAM4+LUE+m;^KE+rQWjHmTZ0f+X32X!f)k;7g8-;4yBi7+7HC>pAD# zZsNSbmI_+3|Wsf;=;59Ow5PW+hMv9K(V z_CXH4LMxtr?^u%^CiPyh+OI*yCb5iYyg$!}y%7QC;mqVwpDi?o0%G%+t5y`pWE>|p z)ZGiUlF$Y>3Vy-1znW)m>Y)Uog<9^RZqQC`tu2K z0CweEnXdVqmVYhVNxJLPfHcPuXCNV7)CLKiF`lpREA&E4x|xO63e1#Lp1B)`kA-g| zYl)=Ih`-*`97@^HDOJRSoxs1+23%!JYxC^I%D}nQ@FY%McG%$fi?)V0gPakJ8sTm+_s^rINu4Ap0gGl{R7ZX%$CyY1&n$Je)e){tChnkNDb`=PnUq_(A#+mL7wmkl zA2zi=aEZJ*^{xrf>`^rDwVlwa4T|@aA$x|+xIIY&4M*k96{5sv3PD-QME$-P9a2^s z>V$#HFpQ7?>5P|)a>6NrO{jE+Rq5`Wvk%@)_*V`_^feDTV5;Obz73~J#v1enI|kmk z5d#H6Nt@%)?QzTix`pf!!Bo?4hajpDcij4SoQXT?;`JUBM1Q9-oM}g_(;&9lRF9E989=qVGEs*z^`2kw* z>|=ruNgfU8)=Vv@nu;&~9(28hUW7(wNJeK)m7j9lYGP<3_K1++E>Odnyf0uVBiVOZ zEkf?MgGn+JN9WuW)&Z7~@;;Q^>Db6FB0VEDSa>S{YlKF(3sb5eWl<8U+a|JE{*ZTv z7-ogYps&qXN6ekvI53?0UGJ%;6GV4M*laJdN~cmf_26ramN{5%K3~RjXt0w|K#{B5 zo2?b{vIs^`p?&H0lv^+NZf`GVT(K>Se7fJ|EMef{(oSVNs+(Em9PB|im^`WK7t1Kf zge>e|v<`j2LMfm;eZi>}!t}%r^aXc&MuB*#WBnC2n4<97#kKVoHL~C8aYVRcTRoax z)u93RD4!peYPk4fzGNwJy~V^#J+12z{mFxmlI)h}pvz+G882j6ub&_Z8Wu#Q1UCJUDdjtEmQdt|>>I!nW$il9_Nc0~$NQ2pEAG#dDHp zIUi)pd_@+0g^1P`n}zQd7}yZIVEW)@;`>{9tgjdut}Icg%{x2qA*GX>H<)gOa$TCF zN!)TDbI$lb0PKr%IPOWFCo0xgbx*TFkJpc1&JQWj|CIiOddJ1QUD zbxG|C{W>N|vd^`5z7XYor}N1l#lWYl9hT~>Zv-*b2iB+4G9+j2w#|Awp0HJzu2CcD zqS{8ObumusE-OaqJ*z<86IHEzhff-2R}Lg!&^RLBIBpZQ%Q&()HlBc!((Wf6{UyLf zHvp@YW-dr4oQ{~>Otdqx>x)aSsD_#|{tV>}k|ap6`Z`E#?=ulFjnmq-(C^NW|6{>Z zugMMfr)RAUhI{F@#}NK44^?+)A+M|jMckysl?UG5JI{O(xI0LnPRP=JWKbT>N61q# zx9{yuG6vHMFPkH}1?k>PHQ$~SO(i4=GEKV7ZPy6q($j-kGq+}*or{}7;)lFIjCTo> zFstqgj*Z`mto62S>{{{z+iaR-R+wxzzRB)*9-|{UZ@YlRy$l{Zj1<%7z4sRO584PI zY6r_K(^8fU7W-RTceotBPE8n26FVG;uQsC}=Ej6Kq-Q70)Z?2R-=)3zP~U_QK^(5g z6B&CfI;VVeco8bp%fa`WvQ_)SpXH0)Qu6O^YYy>XSlvUAi#Q*2bhyXan@j}ycVEkL z%U4U7iRSX7gs`^wh?a$4Yv6b#ZhlhYrh^wgpVsrcHWHM9BltAV&MlQM&B8}|nCXr1 zmmZmnmJW7Bll2Bw$+YE&4D5Azco}S!4TuAGvVeu%6e&fsUJ%&Who2RN?e+31mh#k) z28^A5DH)xR1x+kVkh(z@EH?l5y6kqLRT=tLI}*d; zEvTO!r)a=|1Ti`CCvr+ z`DMxMCauaVC<>AI8Ovi-{mmIc79WzYUG)FcMOWrX#sk2{ar2``Y=2O?A0O8)eWiI= z)9xQE2LEwQyO)(0|CX0EGyWf)5^Mf?rKH5H`Rjk9P`=+;17<6=hTmPY2F#XA3Fveh k))4p#N!k8C5cs7fh0v7b(U>$VMF9Mno18pKHbO=J6Pu*46#xJL literal 0 HcmV?d00001 diff --git a/docs/assets/brand/collaborative-symbol.svg b/docs/assets/brand/collaborative-symbol.svg new file mode 100644 index 0000000..c4b2930 --- /dev/null +++ b/docs/assets/brand/collaborative-symbol.svg @@ -0,0 +1,8 @@ + +Collaborative symbol + + + + + + diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md index 4e2c127..7ac800b 100644 --- a/docs/guide/deployment.md +++ b/docs/guide/deployment.md @@ -5,7 +5,6 @@ PostgreSQL 17+ и S3-совместимое хранилище подключаются отдельно. Готовые сборки публикуются в `ghcr.io/asmblyr/collaborative-core` и `ghcr.io/asmblyr/collaborative-ui`; закрепляйте оба образа одной версии по digest. -Также доступна [рабочая консоль](https://console.asmblyr.io/). Одна собственная установка предназначена для одной команды; workspace не создаёт отдельный tenant. diff --git a/docs/index.md b/docs/index.md index b1a34b4..ee82394 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,10 @@ -# Asmblyr Collaborative +# Collaborative Открытая платформа для работы с данными в PostgreSQL. Админка объединяет структуру коллекций, редакторы записей, права команды, файлы и интеграции. Проект находится в ранней бете; одна команда использует отдельную установку. -[Сайт и демо](https://asmblyr.io/) · [Рабочая консоль](https://console.asmblyr.io/) +[GitHub](https://github.com/Asmblyr/Collaborative) · [Релизы](https://github.com/Asmblyr/Collaborative/releases) ## Начать From 3497fdf4bedef2f7ec83af3e32bb7f15adb65a77 Mon Sep 17 00:00:00 2001 From: Ivan Chikishev Date: Fri, 9 Oct 2026 03:40:17 +0500 Subject: [PATCH 2/5] docs: add English-first bilingual documentation --- .github/pull_request_template.md | 13 +- .gitignore | 8 + .prettierignore | 6 + CONTRIBUTING.md | 45 +- CONTRIBUTING.ru.md | 49 + README.md | 10 +- README.ru.md | 152 + SECURITY.md | 34 +- SECURITY.ru.md | 33 + apps/core/migrations/README.md | 6 + apps/core/migrations/README.ru.md | 40 + deploy/README.md | 181 +- deploy/README.ru.md | 195 + docs/.vitepress/config.mts | 156 +- docs/.vitepress/navigation.ts | 128 + docs/architecture/realtime.md | 52 +- docs/assets/brand/README.md | 6 +- docs/development/architecture.md | 132 +- docs/development/assistant-architecture.md | 85 +- docs/development/documentation.md | 73 +- docs/development/first-extension.md | 123 +- docs/development/local-development.md | 94 +- docs/development/operations.md | 153 +- docs/development/plugin-actions.md | 241 +- docs/features/access.md | 118 +- docs/features/assistant.md | 374 +- docs/features/connections.md | 158 +- docs/features/data.md | 471 +- docs/features/files.md | 64 +- docs/features/google-workspace.md | 191 +- docs/features/identity.md | 125 +- docs/features/integrations.md | 95 +- docs/features/localization.md | 71 +- docs/features/monitoring.md | 212 +- docs/features/notifications.md | 105 +- docs/features/plugins.md | 87 +- docs/features/realtime.md | 109 +- docs/features/settings.md | 77 +- docs/features/workspaces.md | 18 +- docs/guide/deployment.md | 130 +- docs/guide/features.md | 42 +- docs/guide/getting-started.md | 46 +- docs/index.md | 109 +- docs/public/api/index.html | 10 +- docs/public/openapi.json | 1086 +- docs/public/ru/api/index.html | 26 + docs/public/ru/openapi.json | 17887 ++++++++++++++++ docs/reference/cli-guide.md | 2 +- docs/reference/http.md | 48 +- docs/reference/kit-capabilities.md | 69 +- docs/reference/kit-fields.md | 93 +- docs/reference/kit-guide.md | 446 +- docs/reference/kit-hooks.md | 123 +- docs/reference/kit-lifecycle.md | 188 +- docs/reference/kit-ui.md | 57 +- docs/reference/packages.md | 4 + docs/reference/plugin-color.md | 31 +- docs/reference/routes.md | 454 +- docs/reference/sdk-guide.md | 286 +- docs/ru/architecture/realtime.md | 41 + docs/ru/assets/brand/README.md | 29 + docs/ru/development/architecture.md | 98 + docs/ru/development/assistant-architecture.md | 54 + docs/ru/development/documentation.md | 61 + docs/ru/development/first-extension.md | 155 + docs/ru/development/local-development.md | 62 + docs/ru/development/operations.md | 123 + docs/ru/development/plugin-actions.md | 223 + docs/ru/features/access.md | 130 + docs/ru/features/assistant.md | 275 + docs/ru/features/connections.md | 112 + docs/ru/features/data.md | 424 + docs/ru/features/files.md | 56 + docs/ru/features/google-workspace.md | 134 + docs/ru/features/identity.md | 116 + docs/ru/features/integrations.md | 102 + docs/ru/features/localization.md | 76 + docs/ru/features/monitoring.md | 143 + docs/ru/features/notifications.md | 72 + docs/ru/features/plugins.md | 82 + docs/ru/features/realtime.md | 80 + docs/ru/features/settings.md | 66 + docs/ru/features/workspaces.md | 17 + docs/ru/guide/deployment.md | 71 + docs/ru/guide/features.md | 24 + docs/ru/guide/getting-started.md | 45 + docs/ru/index.md | 64 + docs/ru/reference/cli-guide.md | 79 + docs/ru/reference/http.md | 46 + docs/ru/reference/kit-capabilities.md | 98 + docs/ru/reference/kit-fields.md | 104 + docs/ru/reference/kit-guide.md | 467 + docs/ru/reference/kit-hooks.md | 119 + docs/ru/reference/kit-lifecycle.md | 200 + docs/ru/reference/kit-ui.md | 88 + docs/ru/reference/packages.md | 63 + docs/ru/reference/plugin-color.md | 29 + docs/ru/reference/routes.md | 234 + docs/ru/reference/sdk-guide.md | 453 + docs/ru/security/access-matrix.md | 42 + docs/ru/security/overview.md | 51 + docs/security/access-matrix.md | 87 +- docs/security/overview.md | 71 +- examples/gitlab-federation/README.md | 38 +- examples/gitlab-federation/README.ru.md | 25 + examples/oauth-lavinmq/README.md | 37 +- examples/oauth-lavinmq/README.ru.md | 44 + examples/plugins/README.md | 41 +- examples/plugins/README.ru.md | 41 + examples/plugins/calculator/README.md | 39 +- examples/plugins/calculator/README.ru.md | 39 + examples/plugins/color/README.md | 37 +- examples/plugins/color/README.ru.md | 33 + examples/plugins/overview/README.md | 89 +- examples/plugins/overview/README.ru.md | 65 + package.json | 2 +- packages/cli/README.md | 6 + packages/cli/README.ru.md | 83 + packages/kit/CAPABILITIES.md | 89 +- packages/kit/CAPABILITIES.ru.md | 102 + packages/kit/FIELDS.md | 99 +- packages/kit/FIELDS.ru.md | 108 + packages/kit/HOOKS.md | 129 +- packages/kit/HOOKS.ru.md | 123 + packages/kit/LIFECYCLE.md | 194 +- packages/kit/LIFECYCLE.ru.md | 204 + packages/kit/README.md | 452 +- packages/kit/README.ru.md | 471 + packages/kit/UI.md | 63 +- packages/kit/UI.ru.md | 92 + packages/plugin-comments/README.md | 155 +- packages/plugin-comments/README.ru.md | 107 + packages/plugin-google-workspace/README.md | 6 + packages/plugin-google-workspace/README.ru.md | 22 + packages/sdk/README.md | 288 +- packages/sdk/README.ru.md | 457 + scripts/docs/generate.mjs | 113 +- scripts/docs/guides.mjs | 74 + scripts/docs/locales/http.en.json | 219 + scripts/docs/locales/typedoc.ru.json | 84 + scripts/docs/localization.mjs | 192 + scripts/docs/localization.test.mjs | 79 + scripts/docs/swagger-page.mjs | 32 + 143 files changed, 30035 insertions(+), 5131 deletions(-) create mode 100644 CONTRIBUTING.ru.md create mode 100644 README.ru.md create mode 100644 SECURITY.ru.md create mode 100644 apps/core/migrations/README.ru.md create mode 100644 deploy/README.ru.md create mode 100644 docs/.vitepress/navigation.ts create mode 100644 docs/public/ru/api/index.html create mode 100644 docs/public/ru/openapi.json create mode 100644 docs/ru/architecture/realtime.md create mode 100644 docs/ru/assets/brand/README.md create mode 100644 docs/ru/development/architecture.md create mode 100644 docs/ru/development/assistant-architecture.md create mode 100644 docs/ru/development/documentation.md create mode 100644 docs/ru/development/first-extension.md create mode 100644 docs/ru/development/local-development.md create mode 100644 docs/ru/development/operations.md create mode 100644 docs/ru/development/plugin-actions.md create mode 100644 docs/ru/features/access.md create mode 100644 docs/ru/features/assistant.md create mode 100644 docs/ru/features/connections.md create mode 100644 docs/ru/features/data.md create mode 100644 docs/ru/features/files.md create mode 100644 docs/ru/features/google-workspace.md create mode 100644 docs/ru/features/identity.md create mode 100644 docs/ru/features/integrations.md create mode 100644 docs/ru/features/localization.md create mode 100644 docs/ru/features/monitoring.md create mode 100644 docs/ru/features/notifications.md create mode 100644 docs/ru/features/plugins.md create mode 100644 docs/ru/features/realtime.md create mode 100644 docs/ru/features/settings.md create mode 100644 docs/ru/features/workspaces.md create mode 100644 docs/ru/guide/deployment.md create mode 100644 docs/ru/guide/features.md create mode 100644 docs/ru/guide/getting-started.md create mode 100644 docs/ru/index.md create mode 100644 docs/ru/reference/cli-guide.md create mode 100644 docs/ru/reference/http.md create mode 100644 docs/ru/reference/kit-capabilities.md create mode 100644 docs/ru/reference/kit-fields.md create mode 100644 docs/ru/reference/kit-guide.md create mode 100644 docs/ru/reference/kit-hooks.md create mode 100644 docs/ru/reference/kit-lifecycle.md create mode 100644 docs/ru/reference/kit-ui.md create mode 100644 docs/ru/reference/packages.md create mode 100644 docs/ru/reference/plugin-color.md create mode 100644 docs/ru/reference/routes.md create mode 100644 docs/ru/reference/sdk-guide.md create mode 100644 docs/ru/security/access-matrix.md create mode 100644 docs/ru/security/overview.md create mode 100644 examples/gitlab-federation/README.ru.md create mode 100644 examples/oauth-lavinmq/README.ru.md create mode 100644 examples/plugins/README.ru.md create mode 100644 examples/plugins/calculator/README.ru.md create mode 100644 examples/plugins/color/README.ru.md create mode 100644 examples/plugins/overview/README.ru.md create mode 100644 packages/cli/README.ru.md create mode 100644 packages/kit/CAPABILITIES.ru.md create mode 100644 packages/kit/FIELDS.ru.md create mode 100644 packages/kit/HOOKS.ru.md create mode 100644 packages/kit/LIFECYCLE.ru.md create mode 100644 packages/kit/README.ru.md create mode 100644 packages/kit/UI.ru.md create mode 100644 packages/plugin-comments/README.ru.md create mode 100644 packages/plugin-google-workspace/README.ru.md create mode 100644 packages/sdk/README.ru.md create mode 100644 scripts/docs/guides.mjs create mode 100644 scripts/docs/locales/http.en.json create mode 100644 scripts/docs/locales/typedoc.ru.json create mode 100644 scripts/docs/localization.mjs create mode 100644 scripts/docs/localization.test.mjs create mode 100644 scripts/docs/swagger-page.mjs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a828c5..66cd1c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,12 @@ -## Изменение +## Change - + -## Проверка +## Validation - + -## Данные, доступ и документация +## Data, access, and documentation - + diff --git a/.gitignore b/.gitignore index af115ac..194079f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ dist/ .turbo/ *.tsbuildinfo .vitepress/ +!docs/.vitepress/ +docs/.vitepress/* +!docs/.vitepress/config.mts +!docs/.vitepress/navigation.ts apps/ui/src/generated/plugin-ui.ts apps/ui/src/generated/plugin-ui.css @@ -64,6 +68,10 @@ docs/public/api/LICENSE docs/public/api/NOTICE docs/reference/sdk/ docs/reference/kit/ +docs/ru/reference/sdk/ +docs/ru/reference/kit/ +docs/public/brand/ +docs/public/assets/brand/ # Machine-specific editor and OS files; retain shared editor settings /.vscode/* diff --git a/.prettierignore b/.prettierignore index 1d97d4c..aaaf6a0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,6 +18,12 @@ docs/reference/routes.md docs/reference/*-guide.md docs/reference/kit-*.md docs/reference/plugin-color.md +docs/ru/reference/sdk +docs/ru/reference/kit +docs/ru/reference/routes.md +docs/ru/reference/*-guide.md +docs/ru/reference/kit-*.md +docs/ru/reference/plugin-color.md docs/archive.md /infra/ /plugins/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f45c17..a238c7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,43 +1,32 @@ -# Участие в проекте +# Contributing -Ошибки и предложения принимаются в [GitHub Issues](https://github.com/Asmblyr/Collaborative/issues). -Для изменения кода создайте ветку и pull request. Крупные изменения архитектуры, -модели данных или публичных контрактов сначала обсудите в issue. + -## Подготовка +[English](CONTRIBUTING.md) · [Русский](CONTRIBUTING.ru.md) -Следуйте [руководству первого запуска](docs/guide/getting-started.md). -Для карты компонентов и команд используйте [локальную разработку](docs/development/local-development.md), -для нового плагина — [первое расширение](docs/development/first-extension.md). -Публичные пакеты SDK и Kit пока используются как workspace-пакеты репозитория. -Описание компонентов — в [архитектуре](docs/development/architecture.md). + -Не включайте в изменения `.env`, ключи, пользовательские данные, дампы, -локальные скриншоты, внутренние планы или сгенерированные сборки. -Используйте вымышленные данные и домены `example.test` в примерах. +Report bugs and proposals in [GitHub Issues](https://github.com/Asmblyr/Collaborative/issues). Create a branch and pull request for code changes. Discuss substantial architecture, data-model, or public-contract changes in an issue first. -## Проверки +## Setup + +Follow [first-time setup](docs/guide/getting-started.md), [local development](docs/development/local-development.md), and [your first plugin](docs/development/first-extension.md). Repository development uses workspace packages; published SDK/CLI previews use the beta tag. See [architecture](docs/development/architecture.md). + +Never commit environment files, keys, user data, dumps, local screenshots, internal plans, or generated builds. Use fictional data and example.test domains. + +## Checks ```sh pnpm check pnpm build ``` -Интеграционные тесты запускаются через `scripts/test.mjs`: он создаёт и удаляет -отдельную PostgreSQL-базу. Для `TEST_DATABASE_ADMIN_URL` нужен локальный сервер -и роль с правом `CREATEDB`. Не запускайте тестовые файлы напрямую на рабочей базе. -Дополнительный live-тест LavinMQ требует отдельно настроенного примера. +Integration tests run through scripts/test.mjs, which creates/removes a disposable PostgreSQL database. TEST_DATABASE_ADMIN_URL needs a local server and CREATEDB role. Never run test files directly against a working database. The optional live LavinMQ test needs a separately configured broker. -Форматируйте изменённые файлы Prettier. При изменении поведения обновляйте -соответствующее руководство; при изменении HTTP или SDK/Kit — также контракты -и генерируемые справочники. Подробнее — [документация](docs/development/documentation.md). +Format changed files with Prettier. Update feature guides with behavior changes; update HTTP/SDK/Kit contracts and generated references with API changes. See [documentation workflow](docs/development/documentation.md). -## Pull request +## Pull requests -Опишите проблему, итоговое поведение и выполненные проверки. Для изменений UI -проверьте обычный и ограниченный доступ и приложите результат на вымышленных данных. -Существующие миграции не переписывайте: добавляйте новую миграцию и учитывайте -сохранность данных. Не смешивайте исправление с несвязанными переработками. +Describe the problem, resulting behavior, and checks. Verify UI with ordinary and restricted access using fictional data. Add new migrations rather than rewriting old ones; preserve existing data. Avoid unrelated refactors. -Инструкции для кодовых агентов начинаются с [AGENTS.md](AGENTS.md). -Вклад распространяется на условиях [MIT](LICENSE). +Coding-agent instructions start at [AGENTS.md](AGENTS.md). Contributions use the [MIT license](LICENSE). diff --git a/CONTRIBUTING.ru.md b/CONTRIBUTING.ru.md new file mode 100644 index 0000000..9b08102 --- /dev/null +++ b/CONTRIBUTING.ru.md @@ -0,0 +1,49 @@ +# Участие в проекте + + + +[English](CONTRIBUTING.md) · [Русский](CONTRIBUTING.ru.md) + + + +Ошибки и предложения принимаются в [GitHub Issues](https://github.com/Asmblyr/Collaborative/issues). +Для изменения кода создайте ветку и pull request. Крупные изменения архитектуры, +модели данных или публичных контрактов сначала обсудите в issue. + +## Подготовка + +Следуйте [руководству первого запуска](docs/ru/guide/getting-started.md). +Для карты компонентов и команд используйте [локальную разработку](docs/ru/development/local-development.md), +для нового плагина — [первое расширение](docs/ru/development/first-extension.md). +SDK, Kit и CLI доступны под npm-тегом `beta`; внутри репозитория используются workspace-пакеты. +Описание компонентов — в [архитектуре](docs/ru/development/architecture.md). + +Не включайте в изменения `.env`, ключи, пользовательские данные, дампы, +локальные скриншоты, внутренние планы или сгенерированные сборки. +Используйте вымышленные данные и домены `example.test` в примерах. + +## Проверки + +```sh +pnpm check +pnpm build +``` + +Интеграционные тесты запускаются через `scripts/test.mjs`: он создаёт и удаляет +отдельную PostgreSQL-базу. Для `TEST_DATABASE_ADMIN_URL` нужен локальный сервер +и роль с правом `CREATEDB`. Не запускайте тестовые файлы напрямую на рабочей базе. +Дополнительный live-тест LavinMQ требует отдельно настроенного примера. + +Форматируйте изменённые файлы Prettier. При изменении поведения обновляйте +соответствующее руководство; при изменении HTTP или SDK/Kit — также контракты +и генерируемые справочники. Подробнее — [документация](docs/ru/development/documentation.md). + +## Pull request + +Опишите проблему, итоговое поведение и выполненные проверки. Для изменений UI +проверьте обычный и ограниченный доступ и приложите результат на вымышленных данных. +Существующие миграции не переписывайте: добавляйте новую миграцию и учитывайте +сохранность данных. Не смешивайте исправление с несвязанными переработками. + +Инструкции для кодовых агентов начинаются с [AGENTS.md](AGENTS.md). +Вклад распространяется на условиях [MIT](LICENSE). diff --git a/README.md b/README.md index 84ba410..40ec9ab 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ + + +[English](README.md) · [Русский](README.ru.md) + + +

@@ -29,7 +35,7 @@ which records and fields each person can access. Use the same data through the HTTP API or a typed TypeScript client, and extend the product with plugins. **Early beta.** Each installation is intended for one team. The interface supports -English and Russian; most detailed guides are currently in Russian. SDK and CLI +English and Russian, as does the documentation (English by default). SDK and CLI packages are available under the npm `beta` tag. ## What you can do @@ -93,7 +99,7 @@ and S3-compatible storage are connected separately. Both components can share one public origin, with the API available at `/api`. - **Docker Compose:** [deployment instructions](deploy/README.md#docker-compose). -- **Kubernetes:** [Helm chart](deploy/helm/collaborative) and [configuration](deploy/README.md#kubernetes-и-helm). +- **Kubernetes:** [Helm chart](deploy/helm/collaborative) and [configuration](deploy/README.md#kubernetes-and-helm). - **Images:** `ghcr.io/asmblyr/collaborative-core` and `ghcr.io/asmblyr/collaborative-ui`. - **Operations:** [upgrades](docs/guide/deployment.md), [backups and recovery](docs/development/operations.md). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..b2b5351 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,152 @@ + + +[English](README.md) · [Русский](README.ru.md) + + + +

+ + + Collaborative + +

+ +

+ Совместная работа с вашими данными PostgreSQL.
+ Коллекции, админка, политики доступа и API — на вашей инфраструктуре. +

+ +[Быстрый старт](#быстрый-старт) · [Развёртывание](deploy/README.ru.md) · [Документация](docs/ru/index.md) · [Участие в проекте](CONTRIBUTING.ru.md) + +Collaborative превращает данные PostgreSQL в рабочее пространство команды. +Создавайте коллекции и связи, настраивайте формы и таблицы, определяйте доступ +к строкам и полям. Работайте с теми же данными через HTTP API и типизированный +TypeScript-клиент, расширяйте продукт плагинами. + +**Ранняя бета.** Отдельная установка рассчитана на одну команду. Интерфейс и +документация доступны на русском и английском; сайт документации по умолчанию +открывается на английском. SDK и CLI доступны в npm с тегом beta. + +## Возможности + +| Область | Что доступно | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Модель данных | Типизированные поля, M:1 / 1:M / M:N, дополнительные поля поддержанных системных коллекций, materialized views только для чтения | +| Рабочее пространство | Формы и таблицы, релевантный поиск, вложенные фильтры, сохранённые виды, теги, переводы подписей коллекций и полей | +| Совместная работа | Обсуждения, уведомления, присутствие, обновления сохранённых записей, временные блокировки полей, сохранение черновика при конфликте | +| Доступ | Политики действий/полей/строк, приглашения по ссылке, пароли/passkey/SSO, сервисные аккаунты, OAuth/OIDC-приложения | +| Подключения | S3, шифрование секретов, опциональный Yandex KMS, личные Google Drive/Sheets, опциональный Sentry | +| Расширения | Серверные обработчики, страницы UI, редакторы полей, коллекции и миграции через Kit; SDK и CLI генерации схемы | +| Ассистент | Опциональный потоковый чат с историей, инструментами с правами пользователя и явно опубликованными действиями плагинов | + +Подробнее — [карта возможностей](docs/ru/guide/features.md). Workspaces группируют +коллекции, но не изолируют арендаторов. Плагины устанавливаются вместе с приложением; +серверный код выполняется как доверенный внутри Core. + +## Совместная работа в реальном времени + +Откройте запись в двух сессиях: видны участники, редактируемые поля и сохранённые +изменения. Таблицы и карточки получают события SSE. Несохранённые правки остаются +в форме; конфликт одного поля можно разобрать перед сохранением. + +Блокировки помогают координации. Проверка конфликтов защищает сохранение черновика; +обычные API-запросы сохраняют свой контракт. События приходят от операций Core, +а не прямого SQL. [Как это работает](docs/ru/features/realtime.md). + +## Быстрый старт + +Для разработки нужны **Node.js 22+, pnpm 11.13.1 и Docker Compose**. + +```sh +git clone https://github.com/Asmblyr/Collaborative.git +cd Collaborative +pnpm install --frozen-lockfile +pnpm db:up +cp -n apps/core/.env.example apps/core/.env +``` + +Задайте в apps/core/.env случайный ASMBLYR_SETUP_TOKEN длиной от 32 символов: + +```sh +pnpm db:migrate +pnpm dev +``` + +Откройте [localhost:3000/setup](http://localhost:3000/setup), чтобы создать первого +администратора. Команда копирования выше рассчитана на POSIX; в Windows скопируйте +пример, не перезаписывая существующий .env. +[Подробный запуск](docs/ru/guide/getting-started.md). + +## Docker и Kubernetes + +Разворачивайте **Core и UI одной версии из одного коммита**. PostgreSQL и S3 +подключаются отдельно. Оба компонента работают на одном публичном домене, API — /api. + +- [Docker Compose](deploy/README.ru.md#docker-compose). +- [Helm-чарт](deploy/helm/collaborative) и [настройка Kubernetes](deploy/README.ru.md#kubernetes-и-helm). +- Образы: ghcr.io/asmblyr/collaborative-core и ghcr.io/asmblyr/collaborative-ui. +- [Обновления](docs/ru/guide/deployment.md), [копирование и восстановление](docs/ru/development/operations.md). + +Закрепляйте оба образа одной версии по соответствующим digest. Сборка использует +SDK, Kit, Contracts и встроенные плагины из исходников; предварительная публикация +этих пакетов в npm не требуется. + +## Подключение TypeScript-проекта + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +npx asm connect --url http://localhost:3000 +``` + +CLI открывает вход и согласие в вашей установке, загружает доступную аккаунту схему +и генерирует типы коллекций/плагинов. Особый компилятор или плагин сборщика не нужен. +Разрешение на схему не даёт доступа к данным: рабочим запросам нужны отдельные +API-креды или сессия. + +[SDK](docs/ru/reference/sdk-guide.md) · [CLI](docs/ru/reference/cli-guide.md) · [HTTP API](docs/ru/reference/http.md) + +## Ассистент и плагины + +Подключите OpenAI-совместимого провайдера. Ассистент работает с правами вызывающего +и коллекциями, явно открытыми его инструментам. Личные Google Drive/Sheets позволяют +читать по запросу и предлагать изменения для подтверждения пользователем. + +Плагины публикуют выбранные действия через defineModelContext. Обычные HTTP-обработчики +не становятся инструментами автоматически. MCP используется внутри продукта; +публичного MCP endpoint пока нет. + +[Архитектура ассистента](docs/ru/development/assistant-architecture.md) · +[Первый плагин](docs/ru/development/first-extension.md) · [Kit](docs/ru/reference/kit-guide.md) + +## Архитектура + +```mermaid +flowchart LR + Browser[Браузер] -->|Страницы| UI[Next.js UI] + Browser -->|/api| Core[Fastify Core] + SDK[TypeScript SDK and CLI] --> Core + Core --> DB[(PostgreSQL)] + Core --> S3[(S3-хранилище)] + Plugins[Плагины Kit] --> Core +``` + +Core отвечает за HTTP API, вход, права и базу. UI предоставляет админку. +PostgreSQL координирует события между экземплярами Core. +[Подробная архитектура](docs/ru/development/architecture.md). + +## Участие в проекте + +Ошибки и предложения — в [GitHub Issues](https://github.com/Asmblyr/Collaborative/issues). +Для кода создайте fork и pull request. Крупные изменения архитектуры/API сначала +обсудите в issue. + +- [Как участвовать](CONTRIBUTING.ru.md), [локальная разработка](docs/ru/development/local-development.md). +- [Документация](docs/ru/index.md), [её сопровождение](docs/ru/development/documentation.md). +- [Безопасность и приватные сообщения](SECURITY.ru.md). + +## Лицензия + +[MIT](LICENSE). Зависимости сохраняют собственные лицензии. Опциональный локальный +MinIO распространяется под AGPL. Логотип использует Geist под +[SIL OFL 1.1](apps/ui/src/app/fonts/OFL.txt). diff --git a/SECURITY.md b/SECURITY.md index cabfff6..78a6831 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,27 +1,23 @@ -# Безопасность +# Security -Проект находится в ранней бете; проверка изменения выполняется для актуальной ветки -`main`. Поддерживаемые стабильные release-ветки пока не объявлены. + -## Сообщить об уязвимости +[English](SECURITY.md) · [Русский](SECURITY.ru.md) -Если на вкладке [Security](https://github.com/Asmblyr/Collaborative/security) -доступна кнопка **Report a vulnerability**, используйте её для приватного сообщения. -Если приватная отчётность недоступна, создайте issue с запросом приватного канала, -без деталей эксплуатации и пользовательских данных. + -Не публикуйте действующие токены, пароли, дампы, данные пользователей или подробности -неисправленной уязвимости. Для приватного отчёта подготовьте версию/commit, -минимальное воспроизведение на вымышленных данных, влияние и необходимый доступ. +The project is in early beta. Changes are assessed against current main; supported stable release branches have not yet been announced. -## Размещение +## Reporting a vulnerability -Одна команда использует отдельную установку. Workspaces не являются границей -между компаниями. PostgreSQL и S3 должны быть приватными; для внешнего доступа -нужны HTTPS, собственная конфигурация и резервные копии. +If Report a vulnerability is available under [Security](https://github.com/Asmblyr/Collaborative/security), use it for a private report. Otherwise open an issue requesting a private channel without exploitation details or user data. -Серверные расширения исполняют доверенный код внутри Core. Одобрение capabilities -не превращает пакет в песочницу. Не устанавливайте неподконтрольные серверные пакеты. +Never publish live tokens, passwords, dumps, user data, or details of an unfixed vulnerability. Private reports should include version/commit, minimal reproduction with fictional data, impact, and required access. -Точные границы доступа — в [документации безопасности](docs/security/overview.md), -рекомендации по установке и восстановлению — в [эксплуатации](docs/development/operations.md). +## Deployment + +Use a separate installation per team. Workspaces do not isolate companies. Keep PostgreSQL/S3 private and provide HTTPS, installation-specific configuration, and backups. + +Server plugins run trusted code inside Core. Capability approval is not a sandbox. Do not install untrusted server packages. + +See [security boundaries](docs/security/overview.md) and [operations/recovery](docs/development/operations.md). diff --git a/SECURITY.ru.md b/SECURITY.ru.md new file mode 100644 index 0000000..5b2e8ca --- /dev/null +++ b/SECURITY.ru.md @@ -0,0 +1,33 @@ +# Безопасность + + + +[English](SECURITY.md) · [Русский](SECURITY.ru.md) + + + +Проект находится в ранней бете; проверка изменения выполняется для актуальной ветки +`main`. Поддерживаемые стабильные release-ветки пока не объявлены. + +## Сообщить об уязвимости + +Если на вкладке [Security](https://github.com/Asmblyr/Collaborative/security) +доступна кнопка **Report a vulnerability**, используйте её для приватного сообщения. +Если приватная отчётность недоступна, создайте issue с запросом приватного канала, +без деталей эксплуатации и пользовательских данных. + +Не публикуйте действующие токены, пароли, дампы, данные пользователей или подробности +неисправленной уязвимости. Для приватного отчёта подготовьте версию/commit, +минимальное воспроизведение на вымышленных данных, влияние и необходимый доступ. + +## Размещение + +Одна команда использует отдельную установку. Workspaces не являются границей +между компаниями. PostgreSQL и S3 должны быть приватными; для внешнего доступа +нужны HTTPS, собственная конфигурация и резервные копии. + +Серверные расширения исполняют доверенный код внутри Core. Одобрение capabilities +не превращает пакет в песочницу. Не устанавливайте неподконтрольные серверные пакеты. + +Точные границы доступа — в [документации безопасности](docs/ru/security/overview.md), +рекомендации по установке и восстановлению — в [эксплуатации](docs/ru/development/operations.md). diff --git a/apps/core/migrations/README.md b/apps/core/migrations/README.md index e9c5b23..dde0964 100644 --- a/apps/core/migrations/README.md +++ b/apps/core/migrations/README.md @@ -1,5 +1,11 @@ # Core migrations + + +[English](README.md) · [Русский](README.ru.md) + + + This directory contains versioned Knex `.cjs` migrations for Core-owned tables. Core uses the `public` PostgreSQL schema and reserves the `asmblyr_` table prefix, including the `asmblyr_migrations` bookkeeping table. diff --git a/apps/core/migrations/README.ru.md b/apps/core/migrations/README.ru.md new file mode 100644 index 0000000..b76d724 --- /dev/null +++ b/apps/core/migrations/README.ru.md @@ -0,0 +1,40 @@ +# Миграции Core + + + +[English](README.md) · [Русский](README.ru.md) + + + +Каталог содержит версионированные Knex-миграции .cjs системных таблиц. Core использует +public и резервирует префикс asmblyr\_, включая реестр asmblyr_migrations. + +## Запуск + +Задайте DATABASE_URL в apps/core/.env или окружении, затем из корня: + +```sh +pnpm db:status +pnpm db:migrate +``` + +Для размещённой установки следуйте [эксплуатации](../../../docs/ru/development/operations.md). +Перед обновлением согласованно копируйте базу и файлы. + +Pnpm db:rollback откатывает последнюю группу. Некоторые миграции отказываются +удалять таблицы/метаданные, пока от них зависят данные. Изучите конкретный down и +проверьте откат на одноразовой БД до применения к нужной установке. + +## Новые миграции + +- Добавляйте новый файл .cjs с timestamp и up/down. Не меняйте прежние миграции, + включая форматирование: существующие базы уже выполнили их. +- Сохраняйте транзакционность, учитывайте строки, FK, индексы и defaults. +- Предпочитайте добавления; не удаляйте молча поля, права, аккаунты, историю и данные при откате. +- Проверяйте чистую установку и обновление на одноразовой БД через runner; + см. [участие в проекте](../../../CONTRIBUTING.ru.md). + +Плагинные plugin*<namespace>*<local_name> имеют отдельный реестр. +Их добавочные миграции принадлежат пакету; см. +[контракт плагинов](../../../docs/ru/development/plugin-actions.md). +Отключение не удаляет коллекции и не выполняет down. diff --git a/deploy/README.md b/deploy/README.md index 46f9ab1..a1c08c9 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,22 +1,23 @@ -# Установка Collaborative +# Deploying Collaborative -Production поставляется двумя образами из одного коммита: **Core** и **UI**. -PostgreSQL 17+ и S3 подключаются отдельно. SDK, Kit, контракты и встроенные плагины -собираются из исходников монорепозитория; предварительная публикация в npm не нужна. + + +[English](README.md) · [Русский](README.ru.md) + + + +Production uses matching Core and UI images from one commit. Connect PostgreSQL 17+ and S3 separately. SDK, Kit, Contracts, and bundled plugins build from monorepo source without prior npm publication. ## Docker Compose -Соберите оба образа из одного checkout с одинаковым идентификатором сборки: +Build both targets from one checkout with the same build identifier: ```sh docker build --target core --build-arg DEPLOYMENT_VERSION=local -t collaborative-core:local . docker build --target ui --build-arg DEPLOYMENT_VERSION=local -t collaborative-ui:local . ``` -Скопируйте `deploy/env.example` в приватный файл вне репозитория. Задайте -`DATABASE_URL`, случайный `ASMBLYR_SETUP_TOKEN` (32+ символа), публичный origin в -`AUTH_UI_URL` и `SECRETS_LOCAL_KEY` согласно [настройкам интеграций](../docs/features/integrations.md). -S3 можно подключить через системные настройки или env. Бакет должен существовать. +Copy deploy/env.example to a private file outside the repository. Set DATABASE_URL, a random ASMBLYR_SETUP_TOKEN of at least 32 characters, the public AUTH_UI_URL origin, and SECRETS_LOCAL_KEY as described in [connections](../docs/features/connections.md). Configure S3 through admin settings or env; the bucket must already exist. ```sh export ASMBLYR_CORE_IMAGE=collaborative-core:local @@ -25,29 +26,17 @@ export ASMBLYR_ENV_FILE=/private/collaborative.env docker compose -f deploy/compose.yaml up -d --wait ``` -Compose запускает миграции отдельным коротким запуском Core, затем Core и UI. -Публичный адрес по умолчанию `http://localhost:3000`; API доступен на `/api`. -`UI_PORT` и `BIND_ADDRESS` меняют публикацию порта. Внешний HTTPS reverse proxy -направьте на этот порт. `AUTH_UI_URL` должен совпадать с адресом в браузере. -Первого администратора создайте на `/setup`. +Compose runs migrations in a short-lived Core process, then starts Core/UI. The default public address is `http://localhost:3000`, with /api. UI_PORT/BIND_ADDRESS control port publication. Point an external HTTPS reverse proxy to that port. AUTH_UI_URL must match the browser origin. Create the first administrator at /setup. -В Compose публичный listener Core обрабатывает API и передаёт страницы в UI. -Next не проксирует API. UI получает только внутренний `CORE_URL` и общий -`SESSION_COOKIE_PREFIX`, без секретов PostgreSQL, S3 и провайдеров. -Если меняете префикс cookies, задайте `SESSION_COOKIE_PREFIX` в окружении Compose. -Для production закрепляйте **оба** образа одной версии по digest. +Core's public listener handles API and forwards pages to UI; Next does not proxy API. UI receives only internal CORE_URL and shared SESSION_COOKIE_PREFIX, without database, S3, or provider secrets. Set SESSION_COOKIE_PREFIX in Compose's environment if changing it. Pin both production images to the same release and their respective digests. -## Kubernetes и Helm +## Kubernetes and Helm -Чарт находится в [`deploy/helm/collaborative`](helm/collaborative). -Он создаёт два Deployment, два ClusterIP Service, опциональный Ingress и Job миграций. -Все ресурсы относятся к namespace релиза; чарт не создаёт БД, bucket, CRD, -кластерные роли, ingress-контроллер или cert-manager. +The [chart](helm/collaborative) creates two Deployments, two ClusterIP Services, optional Ingress, and a migration Job, all scoped to the release namespace. It creates no database, bucket, CRDs, cluster roles, ingress controller, or cert-manager. -До установки подготовьте namespace и Secret `collaborative-core` с параметрами Core -из `deploy/env.example`. Secret и внешняя БД должны существовать **до** Helm: -миграции выполняются hook Job перед созданием остальных ресурсов. -В приватном values-файле укажите, например: +Prepare the namespace and a collaborative-core Secret with Core parameters from deploy/env.example. The Secret and external database must exist before Helm: migrations run as a pre-install/upgrade hook. + +Example private values: ```yaml publicUrl: https://admin.example.com @@ -75,33 +64,22 @@ helm upgrade --install collaborative deploy/helm/collaborative \ --wait --timeout 10m ``` -TLS Secret также принадлежит namespace релиза. `ingress.annotations` позволяет -передать настройки своего контроллера: таймаут для потоков ассистента должен -покрывать `OPENAI_API_TIMEOUT_MS`, буферизацию потоков следует отключить, -лимит тела запроса согласовать с лимитом файлов Core. Чарт использует стандартные -Prefix-маршруты, не требует regex, rewrite-target или snippets. -При внешнем Gateway задайте `ingress.enabled: false` и повторите маршруты: - -| Путь | Компонент | -| --------------------------------------------------- | --------------- | -| `/api`, `/sign/sso`, `/connections/google/callback` | Core, порт 3001 | -| `/oauth/interaction` | UI, порт 3000 | -| `/oauth` (включая `/oauth/complete/:uid`) | Core, порт 3001 | -| `/` | UI, порт 3000 | - -Самый длинный Prefix имеет приоритет. Cookie взаимодействия OAuth ограничена -`/oauth`; страница согласия и серверное завершение находятся в разных ветках. -Внутренний запрос UI к Core при рендеринге содержит сессию пользователя. -Публичные callbacks и HTTPS origin остаются на том же домене. - -`coreEnv` задаёт несекретные настройки Core. `TRUST_PROXY` — список адресов/CIDR -доверенного ingress через запятую; по умолчанию forwarded IP не доверяются. -Указывайте фактические адреса прокси и ограничивайте сетевой доступ к Core: -доверие всей сети без NetworkPolicy позволяет подделать адрес клиента. -Для приватных образов используйте `image.pullSecrets`. - -Для ключей OAuth-сервера и других файлов Core используйте отдельные заранее -созданные Secrets. Каждый ключ Secret станет файлом в `/run/secrets/`: +TLS Secrets also belong to the release namespace. Use ingress.annotations for controller-specific settings: assistant stream timeouts must cover OPENAI_API_TIMEOUT_MS, response buffering should be disabled, and body limits must match Core uploads. The chart uses ordinary Prefix paths without regex, rewrite-target, or snippets. + +For an external Gateway, disable ingress and reproduce these routes: + +| Prefix | Target | +| --------------------------------------------- | --------- | +| /api, /sign/sso, /connections/google/callback | Core:3001 | +| /oauth/interaction | UI:3000 | +| /oauth, including /oauth/complete/:uid | Core:3001 | +| / | UI:3000 | + +Longest Prefix wins. OAuth interaction cookies are scoped to /oauth; consent and server completion use separate branches. UI server rendering forwards the user session to Core. Public callbacks and HTTPS origin remain on one domain. + +CoreEnv holds nonsecret Core configuration. TRUST_PROXY is a comma-separated allowlist of trusted ingress addresses/CIDRs; forwarded IPs are untrusted by default. Use actual proxy addresses and restrict Core network access: trusting an entire network without NetworkPolicy enables client-IP spoofing. Private images use image.pullSecrets. + +Mount OAuth signing keys and other Core files from precreated Secrets. Each key becomes a file under /run/secrets/<name>: ```yaml coreSecretFiles: @@ -112,78 +90,47 @@ coreEnv: OAUTH_KEYS_FILE: /run/secrets/oauth/keys.json ``` -В этом примере Secret `collaborative-oauth` содержит ключ `keys.json`. -Создание файла описано в [руководстве эксплуатации](../docs/development/operations.md). -Файлы подключаются только к Core и Job миграций, только для чтения. UI их не получает. -После изменения ключей или env выполните rollout Core; каждый экземпляр должен -использовать одинаковые настройки. В Docker Compose аналогичные файлы подключаются -через приватный override с `volumes` для Core и при необходимости миграций. - -Проверки Core: `/health` для жизни процесса, `/ready` для БД и миграций. -UI проверяется через `/healthz`, независимо от БД. Контейнеры работают без root, -без service-account token и с read-only root filesystem; временные файлы и -кэш UI хранятся в ограниченных `emptyDir`. Ресурсы настраиваются отдельно. - -## Реплики и обновления - -Core и UI по умолчанию имеют по одной реплике. Сессии, ограничения входа, -суточные/минутные лимиты ассистента, leases, запросы отмены и подготовленные формы -плагинов хранятся в PostgreSQL и доступны всем репликам. Проверка удалённой отмены -выполняется активным Core каждые 500 мс. Формы ограничены владельцем и живут 20 минут. -Процессные ограничения остаются дополнительной защитой ресурсов каждого Core. -Пользовательские плагины сами отвечают за внешние эффекты и общее состояние. - -Действующий поток ответа принадлежит одному соединению. При остановке Core он -прерывается; автоматического бесшовного продолжения генерации на другой реплике нет. -История сохраняется, незавершённый запрос можно повторить. Sticky sessions не требуются. - -Оба образа собираются с одним `DEPLOYMENT_VERSION` (в CI — commit SHA). Next использует -его для обнаружения несовпадения версии клиента и сервера; это не маршрутизация по -версиям и не гарантия доступности старых JS-файлов во время обновления. Для строгого -нулевого простоя интерфейса нужны сохранение старых ресурсов или переключение всего -релиза после готовности. Не смешивайте произвольные версии Core/UI. - -Перед обновлением делайте копию БД. Миграции должны быть совместимы со старым Core, -который может продолжать обслуживать запросы во время rollout. Helm ждёт Job миграций; -ошибка останавливает установку/обновление. **Helm rollback не откатывает PostgreSQL**. -Удаление релиза не удаляет внешнюю БД, S3 и заранее созданные Secrets. - -## CI и реестр - -GitHub Actions проверяет код, собирает два образа, проверяет их с одноразовой -PostgreSQL и валидирует Helm-чарт. После успешных проверок основной ветки или -релизного Git-тега workflow публикует: - -- `ghcr.io/asmblyr/collaborative-core`; -- `ghcr.io/asmblyr/collaborative-ui`; -- OCI-чарт `oci://ghcr.io/asmblyr/charts/collaborative`. - -Образы получают теги `sha-`, `edge` для main и версию для Git-тега `v*`. -Чарт main получает `0.1.0-edge.`; релизный чарт — версию Git-тега. -Версии и digest опубликованных артефактов указаны в -[GitHub Releases](https://github.com/Asmblyr/Collaborative/releases). -Для неопубликованных изменений собирайте образы локально и ставьте чарт из -репозитория. CI не разворачивает приложение на сервере. +Here collaborative-oauth contains keys.json. See [operations](../docs/development/operations.md) for key creation. Files mount read-only into Core/migrations, never UI. Roll out Core after env/key changes; replicas need identical settings. Compose can mount equivalent files with a private volumes override for Core/migrations. + +Core probes: /health for process liveness and /ready for database/migrations. UI /healthz is database-independent. Containers run nonroot, without service-account tokens, and with read-only root filesystems. Bounded emptyDir volumes hold temporary files/UI cache. Configure component resources independently. + +## Replicas and upgrades + +Core/UI default to one replica each. PostgreSQL shares sessions, credential limits, assistant daily/minute quotas, leases, cancellation requests, and prepared plugin forms across replicas. Active Core checks remote cancellation every 500 ms. Forms are owner-bound and expire after twenty minutes. Process-local limits additionally protect each Core. Custom plugins own their external effects and shared state. + +An active response belongs to one connection. Stopping Core interrupts it; generation does not resume seamlessly on another replica. History remains and interrupted requests can be retried. Sticky sessions are unnecessary. + +Both images share DEPLOYMENT_VERSION (commit SHA in CI). Next uses it to detect client/server mismatch, not for version-based routing or preserving old JavaScript during rollout. Strict zero-downtime UI upgrades need retained old assets or whole-release switching after readiness. Do not mix arbitrary Core/UI versions. + +Back up PostgreSQL before upgrading. Migrations must remain compatible with old Core replicas still serving during rollout. Helm waits for migrations and stops on failure. Helm rollback does not roll back PostgreSQL. Uninstalling the release preserves external database, S3, and precreated Secrets. + +## CI and registry + +GitHub Actions checks code, builds two images, tests them against disposable PostgreSQL, and validates Helm. Successful main/release-tag workflows publish: + +- ghcr.io/asmblyr/collaborative-core +- ghcr.io/asmblyr/collaborative-ui +- oci://ghcr.io/asmblyr/charts/collaborative + +Images use sha-<commit>, edge for main, and versions for v\* tags. Main charts use 0.1.0-edge.<run-number>; release charts use the Git-tag version. [GitHub Releases](https://github.com/Asmblyr/Collaborative/releases) lists artifact versions/digests. + +Build unpublished changes locally and install the repository chart. CI does not deploy servers. ```sh node scripts/test.mjs container-split collaborative-core:local collaborative-ui:local node scripts/chart-check.mjs ``` -Проверка контейнеров создаёт одноразовые PostgreSQL и Docker-сеть и удаляет их. +Container checks create/remove disposable PostgreSQL and a Docker network. + +## Compatibility and local development stack -## Совместимость и локальный стенд +The legacy app monolith target and deploy/compose.monolith.yaml remain for existing installations. Current CI publishes split images. Deploy/local/compose.yaml supplies a development stack with external PostgreSQL/MinIO services. MinIO uses AGPL. -Существующий монолитный target `app` и `deploy/compose.monolith.yaml` сохранены -для перехода существующих установок. Новый CI публикует раздельные образы. -`deploy/local/compose.yaml` содержит дополнительный стенд разработки с PostgreSQL -и MinIO. Это отдельные внешние для приложения сервисы. MinIO имеет лицензию AGPL. -Скопируйте `deploy/local/env.example` в приватный файл, задайте секреты и выполните: +Copy deploy/local/env.example to a private file, set secrets, then run: ```sh docker compose --env-file /private/local.env -p asmblyr-local -f deploy/local/compose.yaml up -d --build --wait ``` -Адрес по умолчанию `http://localhost:3300`. Для остановки без удаления данных -используйте `down` без `-v`. Эксплуатационные сценарии и копирование описаны в -[руководстве эксплуатации](../docs/development/operations.md). +Default address: `http://localhost:3300`. Stop with down without -v to retain data. See [operations](../docs/development/operations.md) for backups/recovery. diff --git a/deploy/README.ru.md b/deploy/README.ru.md new file mode 100644 index 0000000..1b7e953 --- /dev/null +++ b/deploy/README.ru.md @@ -0,0 +1,195 @@ +# Установка Collaborative + + + +[English](README.md) · [Русский](README.ru.md) + + + +Production поставляется двумя образами из одного коммита: **Core** и **UI**. +PostgreSQL 17+ и S3 подключаются отдельно. SDK, Kit, контракты и встроенные плагины +собираются из исходников монорепозитория; предварительная публикация в npm не нужна. + +## Docker Compose + +Соберите оба образа из одного checkout с одинаковым идентификатором сборки: + +```sh +docker build --target core --build-arg DEPLOYMENT_VERSION=local -t collaborative-core:local . +docker build --target ui --build-arg DEPLOYMENT_VERSION=local -t collaborative-ui:local . +``` + +Скопируйте `deploy/env.example` в приватный файл вне репозитория. Задайте +`DATABASE_URL`, случайный `ASMBLYR_SETUP_TOKEN` (32+ символа), публичный origin в +`AUTH_UI_URL` и `SECRETS_LOCAL_KEY` согласно [настройкам интеграций](../docs/ru/features/integrations.md). +S3 можно подключить через системные настройки или env. Бакет должен существовать. + +```sh +export ASMBLYR_CORE_IMAGE=collaborative-core:local +export ASMBLYR_UI_IMAGE=collaborative-ui:local +export ASMBLYR_ENV_FILE=/private/collaborative.env +docker compose -f deploy/compose.yaml up -d --wait +``` + +Compose запускает миграции отдельным коротким запуском Core, затем Core и UI. +Публичный адрес по умолчанию `http://localhost:3000`; API доступен на `/api`. +`UI_PORT` и `BIND_ADDRESS` меняют публикацию порта. Внешний HTTPS reverse proxy +направьте на этот порт. `AUTH_UI_URL` должен совпадать с адресом в браузере. +Первого администратора создайте на `/setup`. + +В Compose публичный listener Core обрабатывает API и передаёт страницы в UI. +Next не проксирует API. UI получает только внутренний `CORE_URL` и общий +`SESSION_COOKIE_PREFIX`, без секретов PostgreSQL, S3 и провайдеров. +Если меняете префикс cookies, задайте `SESSION_COOKIE_PREFIX` в окружении Compose. +Для production закрепляйте **оба** образа одной версии по digest. + +## Kubernetes и Helm + +Чарт находится в [`deploy/helm/collaborative`](helm/collaborative). +Он создаёт два Deployment, два ClusterIP Service, опциональный Ingress и Job миграций. +Все ресурсы относятся к namespace релиза; чарт не создаёт БД, bucket, CRD, +кластерные роли, ingress-контроллер или cert-manager. + +До установки подготовьте namespace и Secret `collaborative-core` с параметрами Core +из `deploy/env.example`. Secret и внешняя БД должны существовать **до** Helm: +миграции выполняются hook Job перед созданием остальных ресурсов. +В приватном values-файле укажите, например: + +```yaml +publicUrl: https://admin.example.com +existingSecret: collaborative-core +images: + core: + repository: ghcr.io/asmblyr/collaborative-core + digest: sha256:REPLACE_WITH_VERIFIED_CORE_DIGEST + ui: + repository: ghcr.io/asmblyr/collaborative-ui + digest: sha256:REPLACE_WITH_VERIFIED_UI_DIGEST +ingress: + enabled: true + className: your-ingress-class + tlsSecretName: admin-tls +core: + replicas: 1 +ui: + replicas: 1 +``` + +```sh +helm upgrade --install collaborative deploy/helm/collaborative \ + --namespace collaborative --values /private/collaborative-values.yaml \ + --wait --timeout 10m +``` + +TLS Secret также принадлежит namespace релиза. `ingress.annotations` позволяет +передать настройки своего контроллера: таймаут для потоков ассистента должен +покрывать `OPENAI_API_TIMEOUT_MS`, буферизацию потоков следует отключить, +лимит тела запроса согласовать с лимитом файлов Core. Чарт использует стандартные +Prefix-маршруты, не требует regex, rewrite-target или snippets. +При внешнем Gateway задайте `ingress.enabled: false` и повторите маршруты: + +| Путь | Компонент | +| --------------------------------------------------- | --------------- | +| `/api`, `/sign/sso`, `/connections/google/callback` | Core, порт 3001 | +| `/oauth/interaction` | UI, порт 3000 | +| `/oauth` (включая `/oauth/complete/:uid`) | Core, порт 3001 | +| `/` | UI, порт 3000 | + +Самый длинный Prefix имеет приоритет. Cookie взаимодействия OAuth ограничена +`/oauth`; страница согласия и серверное завершение находятся в разных ветках. +Внутренний запрос UI к Core при рендеринге содержит сессию пользователя. +Публичные callbacks и HTTPS origin остаются на том же домене. + +`coreEnv` задаёт несекретные настройки Core. `TRUST_PROXY` — список адресов/CIDR +доверенного ingress через запятую; по умолчанию forwarded IP не доверяются. +Указывайте фактические адреса прокси и ограничивайте сетевой доступ к Core: +доверие всей сети без NetworkPolicy позволяет подделать адрес клиента. +Для приватных образов используйте `image.pullSecrets`. + +Для ключей OAuth-сервера и других файлов Core используйте отдельные заранее +созданные Secrets. Каждый ключ Secret станет файлом в `/run/secrets/`: + +```yaml +coreSecretFiles: + - name: oauth + secretName: collaborative-oauth +coreEnv: + OAUTH_ISSUER_URL: https://admin.example.com/oauth + OAUTH_KEYS_FILE: /run/secrets/oauth/keys.json +``` + +В этом примере Secret `collaborative-oauth` содержит ключ `keys.json`. +Создание файла описано в [руководстве эксплуатации](../docs/ru/development/operations.md). +Файлы подключаются только к Core и Job миграций, только для чтения. UI их не получает. +После изменения ключей или env выполните rollout Core; каждый экземпляр должен +использовать одинаковые настройки. В Docker Compose аналогичные файлы подключаются +через приватный override с `volumes` для Core и при необходимости миграций. + +Проверки Core: `/health` для жизни процесса, `/ready` для БД и миграций. +UI проверяется через `/healthz`, независимо от БД. Контейнеры работают без root, +без service-account token и с read-only root filesystem; временные файлы и +кэш UI хранятся в ограниченных `emptyDir`. Ресурсы настраиваются отдельно. + +## Реплики и обновления + +Core и UI по умолчанию имеют по одной реплике. Сессии, ограничения входа, +суточные/минутные лимиты ассистента, leases, запросы отмены и подготовленные формы +плагинов хранятся в PostgreSQL и доступны всем репликам. Проверка удалённой отмены +выполняется активным Core каждые 500 мс. Формы ограничены владельцем и живут 20 минут. +Процессные ограничения остаются дополнительной защитой ресурсов каждого Core. +Пользовательские плагины сами отвечают за внешние эффекты и общее состояние. + +Действующий поток ответа принадлежит одному соединению. При остановке Core он +прерывается; автоматического бесшовного продолжения генерации на другой реплике нет. +История сохраняется, незавершённый запрос можно повторить. Sticky sessions не требуются. + +Оба образа собираются с одним `DEPLOYMENT_VERSION` (в CI — commit SHA). Next использует +его для обнаружения несовпадения версии клиента и сервера; это не маршрутизация по +версиям и не гарантия доступности старых JS-файлов во время обновления. Для строгого +нулевого простоя интерфейса нужны сохранение старых ресурсов или переключение всего +релиза после готовности. Не смешивайте произвольные версии Core/UI. + +Перед обновлением делайте копию БД. Миграции должны быть совместимы со старым Core, +который может продолжать обслуживать запросы во время rollout. Helm ждёт Job миграций; +ошибка останавливает установку/обновление. **Helm rollback не откатывает PostgreSQL**. +Удаление релиза не удаляет внешнюю БД, S3 и заранее созданные Secrets. + +## CI и реестр + +GitHub Actions проверяет код, собирает два образа, проверяет их с одноразовой +PostgreSQL и валидирует Helm-чарт. После успешных проверок основной ветки или +релизного Git-тега workflow публикует: + +- `ghcr.io/asmblyr/collaborative-core`; +- `ghcr.io/asmblyr/collaborative-ui`; +- OCI-чарт `oci://ghcr.io/asmblyr/charts/collaborative`. + +Образы получают теги `sha-`, `edge` для main и версию для Git-тега `v*`. +Чарт main получает `0.1.0-edge.`; релизный чарт — версию Git-тега. +Версии и digest опубликованных артефактов указаны в +[GitHub Releases](https://github.com/Asmblyr/Collaborative/releases). +Для неопубликованных изменений собирайте образы локально и ставьте чарт из +репозитория. CI не разворачивает приложение на сервере. + +```sh +node scripts/test.mjs container-split collaborative-core:local collaborative-ui:local +node scripts/chart-check.mjs +``` + +Проверка контейнеров создаёт одноразовые PostgreSQL и Docker-сеть и удаляет их. + +## Совместимость и локальный стенд + +Существующий монолитный target `app` и `deploy/compose.monolith.yaml` сохранены +для перехода существующих установок. Новый CI публикует раздельные образы. +`deploy/local/compose.yaml` содержит дополнительный стенд разработки с PostgreSQL +и MinIO. Это отдельные внешние для приложения сервисы. MinIO имеет лицензию AGPL. +Скопируйте `deploy/local/env.example` в приватный файл, задайте секреты и выполните: + +```sh +docker compose --env-file /private/local.env -p asmblyr-local -f deploy/local/compose.yaml up -d --build --wait +``` + +Адрес по умолчанию `http://localhost:3300`. Для остановки без удаления данных +используйте `down` без `-v`. Эксплуатационные сценарии и копирование описаны в +[руководстве эксплуатации](../docs/ru/development/operations.md). diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2055803..b9ad9fe 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,105 +1,81 @@ import { defineConfig } from "vitepress"; +import { navigation } from "./navigation"; export default defineConfig({ - lang: "ru-RU", - title: "Asmblyr", - description: "Возможности, архитектура и контракты Asmblyr Collaborative", + lang: "en-US", + title: "Collaborative", + description: "Guides, architecture, and API contracts for Collaborative", srcExclude: ["public/**"], lastUpdated: true, + locales: { + root: { + label: "English", + lang: "en-US", + themeConfig: navigation("en"), + }, + ru: { + label: "Русский", + lang: "ru-RU", + description: "Руководства, архитектура и API-контракты Collaborative", + themeConfig: { + ...navigation("ru"), + outline: { label: "На этой странице" }, + docFooter: { prev: "Предыдущая страница", next: "Следующая страница" }, + sidebarMenuLabel: "Разделы", + returnToTopLabel: "Наверх", + skipToContentLabel: "К содержимому", + darkModeSwitchLabel: "Тема", + lightModeSwitchTitle: "Светлая тема", + darkModeSwitchTitle: "Тёмная тема", + langMenuLabel: "Выбрать язык", + lastUpdated: { text: "Обновлено" }, + notFound: { + title: "Страница не найдена", + quote: "Проверьте адрес или откройте главную страницу документации.", + linkLabel: "На главную", + linkText: "На главную", + }, + }, + }, + }, themeConfig: { - outline: { label: "На этой странице" }, - docFooter: { prev: "Предыдущая страница", next: "Следующая страница" }, - sidebarMenuLabel: "Разделы", - returnToTopLabel: "Наверх", - skipToContentLabel: "К содержимому", - darkModeSwitchLabel: "Тема", - lightModeSwitchTitle: "Светлая тема", - darkModeSwitchTitle: "Тёмная тема", + logo: { + light: "/assets/brand/collaborative-symbol.svg", + dark: "/assets/brand/collaborative-symbol-dark.svg", + alt: "Collaborative", + }, + socialLinks: [ + { icon: "github", link: "https://github.com/Asmblyr/Collaborative" }, + ], search: { provider: "local", options: { - translations: { - button: { - buttonText: "Поиск", - buttonAriaLabel: "Поиск по документации", + locales: { + ru: { + translations: { + button: { + buttonText: "Поиск", + buttonAriaLabel: "Поиск по документации", + }, + modal: { + displayDetails: "Показать подробности", + resetButtonTitle: "Очистить поиск", + backButtonTitle: "Закрыть поиск", + noResultsText: "Ничего не найдено", + footer: { + selectText: "выбрать", + selectKeyAriaLabel: "Enter", + navigateText: "перейти", + navigateUpKeyAriaLabel: "Стрелка вверх", + navigateDownKeyAriaLabel: "Стрелка вниз", + closeText: "закрыть", + closeKeyAriaLabel: "Escape", + }, + }, + }, }, }, }, }, - nav: [ - { text: "Продукт", link: "/guide/features" }, - { text: "API и SDK", link: "/reference/http" }, - { text: "Безопасность", link: "/security/overview" }, - ], - sidebar: [ - { - text: "Начало", - items: [ - { text: "Обзор", link: "/" }, - { text: "Локальный запуск", link: "/guide/getting-started" }, - { text: "Развёртывание", link: "/guide/deployment" }, - { text: "Карта возможностей", link: "/guide/features" }, - ], - }, - { - text: "Функциональность", - items: [ - { text: "Коллекции и записи", link: "/features/data" }, - { text: "Права и политики", link: "/features/access" }, - { text: "Настройки", link: "/features/settings" }, - { text: "Подключения и секреты", link: "/features/connections" }, - { text: "Мониторинг и Sentry", link: "/features/monitoring" }, - { text: "Переводы и язык", link: "/features/localization" }, - { text: "Пользователи и SSO", link: "/features/identity" }, - { text: "Сервисные интеграции", link: "/features/integrations" }, - { text: "Ассистент и MCP", link: "/features/assistant" }, - { text: "Обсуждения и уведомления", link: "/features/notifications" }, - { text: "Файлы", link: "/features/files" }, - { text: "Рабочие пространства и виды", link: "/features/workspaces" }, - { text: "Расширения", link: "/features/plugins" }, - ], - }, - { - text: "Разработка", - items: [ - { - text: "Локальная разработка", - link: "/development/local-development", - }, - { text: "Архитектура", link: "/development/architecture" }, - { text: "Первое расширение", link: "/development/first-extension" }, - { - text: "Архитектура ассистента", - link: "/development/assistant-architecture", - }, - { - text: "Проверки и документация", - link: "/development/documentation", - }, - { text: "Эксплуатация", link: "/development/operations" }, - { text: "Действия расширений", link: "/development/plugin-actions" }, - ], - }, - { - text: "Справочники", - items: [ - { text: "HTTP / OpenAPI", link: "/reference/http" }, - { text: "Матрица маршрутов", link: "/reference/routes" }, - { text: "SDK: руководство", link: "/reference/sdk-guide" }, - { text: "CLI: генерация типов", link: "/reference/cli-guide" }, - { text: "Пакеты и публикация", link: "/reference/packages" }, - { text: "SDK: типы и методы", link: "/reference/sdk/README" }, - { text: "Kit: руководство", link: "/reference/kit-guide" }, - { text: "Kit: типы и методы", link: "/reference/kit/README" }, - ], - }, - { - text: "Безопасность", - items: [ - { text: "Границы доступа", link: "/security/access-matrix" }, - { text: "Обзор", link: "/security/overview" }, - ], - }, - ], }, }); diff --git a/docs/.vitepress/navigation.ts b/docs/.vitepress/navigation.ts new file mode 100644 index 0000000..d2dbe7e --- /dev/null +++ b/docs/.vitepress/navigation.ts @@ -0,0 +1,128 @@ +import type { DefaultTheme } from "vitepress"; + +type Entry = [english: string, russian: string, path: string]; +const groups: [english: string, russian: string, entries: Entry[]][] = [ + [ + "Getting started", + "Начало", + [ + ["Overview", "Обзор", "/"], + ["Local setup", "Локальный запуск", "/guide/getting-started"], + ["Deployment", "Развёртывание", "/guide/deployment"], + ["Feature map", "Карта возможностей", "/guide/features"], + ], + ], + [ + "Features", + "Функциональность", + [ + ["Collections and records", "Коллекции и записи", "/features/data"], + ["Permissions and policies", "Права и политики", "/features/access"], + ["Settings", "Настройки", "/features/settings"], + [ + "Connections and secrets", + "Подключения и секреты", + "/features/connections", + ], + ["Monitoring and Sentry", "Мониторинг и Sentry", "/features/monitoring"], + [ + "Translations and language", + "Переводы и язык", + "/features/localization", + ], + ["Users and SSO", "Пользователи и SSO", "/features/identity"], + [ + "Service integrations", + "Сервисные интеграции", + "/features/integrations", + ], + ["Assistant and MCP", "Ассистент и MCP", "/features/assistant"], + ["Google Workspace", "Google Workspace", "/features/google-workspace"], + [ + "Discussions and notifications", + "Обсуждения и уведомления", + "/features/notifications", + ], + ["Live collaboration", "Совместная работа", "/features/realtime"], + ["Files", "Файлы", "/features/files"], + ["Workspaces and views", "Пространства и виды", "/features/workspaces"], + ["Plugins", "Расширения", "/features/plugins"], + ], + ], + [ + "Development", + "Разработка", + [ + [ + "Local development", + "Локальная разработка", + "/development/local-development", + ], + ["Architecture", "Архитектура", "/development/architecture"], + ["Live architecture", "Архитектура Live", "/architecture/realtime"], + [ + "Your first plugin", + "Первое расширение", + "/development/first-extension", + ], + [ + "Assistant architecture", + "Архитектура ассистента", + "/development/assistant-architecture", + ], + [ + "Documentation workflow", + "Сопровождение документации", + "/development/documentation", + ], + ["Operations", "Эксплуатация", "/development/operations"], + ["Plugin actions", "Действия расширений", "/development/plugin-actions"], + ], + ], + [ + "Reference", + "Справочники", + [ + ["HTTP / OpenAPI", "HTTP / OpenAPI", "/reference/http"], + ["Route matrix", "Матрица маршрутов", "/reference/routes"], + ["SDK guide", "SDK: руководство", "/reference/sdk-guide"], + [ + "CLI and type generation", + "CLI: генерация типов", + "/reference/cli-guide", + ], + ["Packages and publishing", "Пакеты и публикация", "/reference/packages"], + ["SDK types and methods", "SDK: типы и методы", "/reference/sdk/README"], + ["Kit guide", "Kit: руководство", "/reference/kit-guide"], + ["Kit types and methods", "Kit: типы и методы", "/reference/kit/README"], + ], + ], + [ + "Security", + "Безопасность", + [ + ["Access boundaries", "Границы доступа", "/security/access-matrix"], + ["Overview", "Обзор", "/security/overview"], + ], + ], +]; + +export function navigation(locale: "en" | "ru"): DefaultTheme.Config { + const prefix = locale === "ru" ? "/ru" : ""; + const label = locale === "ru" ? 1 : 0; + const entry = (item: Entry) => ({ + text: item[label], + link: prefix + item[2], + }); + return { + nav: [ + entry(["Product", "Продукт", "/guide/features"]), + entry(["API and SDK", "API и SDK", "/reference/http"]), + entry(["Security", "Безопасность", "/security/overview"]), + ], + sidebar: groups.map((group) => ({ + text: group[label], + items: group[2].map(entry), + })), + }; +} diff --git a/docs/architecture/realtime.md b/docs/architecture/realtime.md index aa1401e..67e46c4 100644 --- a/docs/architecture/realtime.md +++ b/docs/architecture/realtime.md @@ -1,39 +1,39 @@ -# Архитектура Collaborative Live + -SSE выбран для односторонних уведомлений: браузер обращается к Core через `/api`, -а получение и освобождение locks остаётся обычным HTTP-запросом. -Отдельный WebSocket transport не требуется для этого обмена. +# Collaborative Live architecture + +SSE carries one-way notifications: the browser reaches Core through `/api`, while +acquiring and releasing locks remains ordinary HTTP. This exchange does not +require a separate WebSocket transport. ```mermaid flowchart TD - UI[UI и SDK] -->|SSE с cookie или Bearer| Gateway[Core realtime routes] - Gateway --> Auth[Access и row permissions] + UI[UI and SDK] -->|SSE with cookie or Bearer| Gateway[Core realtime routes] + Gateway --> Auth[Access and row permissions] Gateway --> Presence[(asmblyr_presence)] Gateway --> Locks[(asmblyr_field_locks)] API[HTTP / Kit writes] --> Domain[Items services] Domain --> Events[(asmblyr_item_events)] - Events -->|pg_notify после commit| Bus[PostgresRealtimeBus] + Events -->|pg_notify after commit| Bus[PostgresRealtimeBus] Bus --> Gateway ``` -`recordItemEvent` записывает историю и публикует минимальный envelope внутри -той же транзакции. `LISTEN` использует отдельное соединение; переподключение -слушателя закрывает SSE-потоки, чтобы клиенты перечитали данные. В памяти Core -остаются только подписчики текущего процесса. `InMemoryRealtimeBus` служит для -изолированных тестов; PostgreSQL bus работает без дополнительного Redis. +`recordItemEvent` writes history and publishes a minimal envelope in the same +transaction. `LISTEN` uses a dedicated connection; reconnecting the listener +closes SSE streams so clients reload data. Core keeps only the current process's +subscribers in memory. `InMemoryRealtimeBus` supports isolated tests; the +PostgreSQL bus does not require Redis. -Подписка ограничена одним scope (`page`, `collection`, `record`) и одним -clientId. Gateway повторно загружает права перед выдачей события. Коллекция -получает только invalidation, запись — metadata события после проверки строки. -Число соединений ограничено существующим лимитом 32 окон на сессию. -SSE heartbeat не обращается к БД; продление presence и прав выполняется раз в -20 секунд и отправляет клиенту актуальный состав участников. Отставший сокет -закрывается при переполнении буфера. При завершении Core хук `preClose` закрывает -активные SSE-потоки до остановки HTTP-сервера. +A subscription is limited to one scope (`page`, `collection`, `record`) and one +clientId. The gateway reloads permissions before delivering an event. Collections +receive only invalidation; records receive event metadata after the row check. +Connections use the existing limit of 32 windows per session. SSE heartbeats do +not query the database. Presence and permissions are renewed every 20 seconds, +sending the current participants to the client. A slow socket is closed if its +buffer fills. On Core shutdown, `preClose` closes active SSE streams before the HTTP server stops. -Field locks лежат в Core-owned таблице с составным ключом -`(collection_id, record_id, field)`. `INSERT ... ON CONFLICT ... WHERE` -атомарно разрешает обновление своему editor или перехват истёкшей lease. -Фоновая очистка удаляет просроченные блокировки и публикует `field.unlocked`. -Миграция только добавляет таблицу; откат удаляет временные leases, а не данные -пользователя. +Field locks are stored in a Core-owned table with the composite key +`(collection_id, record_id, field)`. `INSERT ... ON CONFLICT ... WHERE` atomically +allows the owning editor to renew a lease or another editor to claim an expired +lease. Background cleanup removes expired locks and publishes `field.unlocked`. +The migration only adds a table; rollback removes temporary leases, not user data. diff --git a/docs/assets/brand/README.md b/docs/assets/brand/README.md index 5adf422..5b595dc 100644 --- a/docs/assets/brand/README.md +++ b/docs/assets/brand/README.md @@ -1,8 +1,12 @@ + + # Collaborative brand assets The public wordmark is **Collaborative**. The symbol combines four connected loops around a square opening, with a mint accent on the upper-right loop. + + ## Files - [`collaborative-symbol.svg`](./collaborative-symbol.svg): standalone symbol. @@ -12,7 +16,7 @@ around a square opening, with a mint accent on the upper-right loop. The SVG files are the editable source assets. They use vector paths, including outlined lettering, with no embedded raster images, external fonts, or scripts. -The wordmark uses Geist, distributed under [SIL OFL 1.1](../../../apps/ui/src/app/fonts/OFL.txt). +The wordmark uses Geist, distributed under [SIL OFL 1.1](https://github.com/Asmblyr/Collaborative/blob/main/apps/ui/src/app/fonts/OFL.txt). Use the same symbol geometry at every size. Keep clear space around it, preserve its proportions, and use the appropriate wordmark for the background. Graphite diff --git a/docs/development/architecture.md b/docs/development/architecture.md index 3f4542c..56fd1c2 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -1,90 +1,96 @@ -# Архитектура + -Asmblyr состоит из HTTP API и приложения админки. Основная поставка использует -отдельные образы Core и UI; совместимый монолитный образ также доступен. -PostgreSQL хранит структуру, данные и состояние авторизации; S3 хранит содержимое -файлов. +# Architecture + +Collaborative consists of an HTTP API and an admin application. The primary +distribution uses separate Core and UI images; a compatible monolithic image is +also available. PostgreSQL stores schemas, records, and authentication state; +S3 stores file content. ```mermaid flowchart LR - Browser[Браузер] -->|страницы| UI[Next.js UI] + Browser[Browser] -->|Pages| UI[Next.js UI] Browser -->|/api| Core[Fastify Core API] - UI -->|серверный рендеринг| Core - Client[Внешний клиент / SDK] --> Core + UI -->|Server rendering| Core + Client[External client / SDK] --> Core Core --> PG[(PostgreSQL)] - Core --> Objects[(S3-совместимое хранилище)] + Core --> Objects[(S3-compatible storage)] ``` -Браузер обращается к Core напрямую через `/api`; Core ведёт браузерную сессию. -Внешний клиент обращается к Core напрямую с собственным access token. -Файлы проходят через Core: метаданные находятся в PostgreSQL, содержимое — -в настроенном объектном хранилище. +The browser reaches Core directly through `/api`; Core owns browser sessions. +External clients access Core with their own access token. Files pass through +Core: PostgreSQL holds metadata and the configured object store holds content. + + -## Core и UI +## Core and UI -`apps/core` использует Fastify и Knex. Маршруты разбирают запрос, доменные сервисы -проверяют права и поведение, слой запросов работает с PostgreSQL. `/collections` -управляет структурой, `/items/:collection` — данными. Системные таблицы имеют префикс -`asmblyr_`; структура меняется версионными миграциями. +`apps/core` uses Fastify and Knex. Routes parse requests, domain services enforce +permissions and behavior, and the query layer accesses PostgreSQL. `/collections` +manages schemas; `/items/:collection` manages data. System tables use the +`asmblyr_` prefix and change through versioned migrations. -`apps/ui` использует Next.js, React и shadcn и отвечает за рендеринг страниц. -Браузер обращается к Core напрямую через `/api`; Next не содержит API-обработчиков. -Core управляет HttpOnly browser session, входом и callback провайдеров. При серверном -рендеринге Next читает cookie и обращается к Core от имени текущего пользователя. +`apps/ui` uses Next.js, React, and shadcn to render pages. The browser calls Core +through `/api`; Next has no API handlers. Core manages the HttpOnly browser +session, sign-in, and provider callbacks. During server rendering, Next reads +the cookie and calls Core as the current user. -Production использует отдельные образы и Deployment Core/UI, собранные из одного -коммита. Ingress направляет `/api`, `/sign/sso`, `/connections/google/callback` и -`/oauth` в Core; более длинный путь `/oauth/interaction` и остальные страницы — в UI. -Согласие завершается в Core на `/oauth/complete/:uid`, cookie протокола имеет путь -`/oauth`. Общий HTTPS origin сохраняет работу сессий и callbacks. +Production runs separate Core/UI images and Deployments built from the same commit. +Ingress routes `/api`, `/sign/sso`, `/connections/google/callback`, and `/oauth` +to Core; the longer `/oauth/interaction` path and other pages go to UI. Consent +completes at Core's `/oauth/complete/:uid`; protocol cookies use `/oauth`. +A shared HTTPS origin preserves sessions and callbacks. -В локальной разработке, Compose и совместимом монолитном target публичный listener -Core обрабатывает `/api` и передаёт страницы в Next. Порты монолита: публичный 3000, -Core 3001, Next 3002. В Helm этот listener выключен: ingress обращается к компонентам -напрямую. Миграции выполняются отдельным Job. PostgreSQL и S3 разворачивает оператор. +In local development, Compose, and the compatible monolithic target, Core's public +listener handles `/api` and forwards pages to Next. Monolithic ports are public +3000, Core 3001, and Next 3002. Helm disables that listener: ingress reaches each +component directly. Migrations run as a separate Job. The operator supplies PostgreSQL and S3. -Общие сессии, ограничения входа, запросы отмены ассистента и подготовленные формы -плагинов находятся в PostgreSQL. Контроллер активной генерации остаётся в её процессе; -перезапуск прерывает поток. Пользовательские плагины должны учитывать несколько реплик. +Shared sessions, sign-in limits, assistant cancellation requests, and prepared +plugin forms live in PostgreSQL. An active generation controller stays in its +process; restarting interrupts the stream. Custom plugins must account for multiple replicas. -## Контракты и расширения + -`packages/contracts` содержит общие типы. `packages/sdk` предоставляет HTTP-клиент, -`packages/cli` подключает SDK и генерирует типы, `packages/kit` содержит контексты, -декларации и сборщик расширений. Встроенные комментарии находятся в -`packages/plugin-comments`; инструменты Google — в `packages/plugin-google-workspace`. -Учебные примеры — в `examples/plugins`. -Установленные пакеты включаются явно в `asmblyr.plugins` корневого manifest. +## Contracts and plugins + +`packages/contracts` holds shared types. `packages/sdk` provides the HTTP client, +`packages/cli` connects projects and generates types, and `packages/kit` provides +plugin contexts, declarations, and the builder. Bundled discussions live in +`packages/plugin-comments`; Google tools in `packages/plugin-google-workspace`. +Tutorial examples are in `examples/plugins`. Installed packages are explicitly +listed in the root manifest's `asmblyr.plugins`. ```mermaid flowchart LR - Package[Пакет расширения / Kit] --> Routes[H3 file routes и handlers] - Routes --> Core[Core: HTTP и права] + Package[Plugin package / Kit] --> Routes[H3 file routes and handlers] + Routes --> Core[Core: HTTP and permissions] Routes --> Context[defineModelContext] - Context --> MCP[Внутренний MCP] - MCP --> Assistant[Ассистент] + Context --> MCP[Internal MCP] + MCP --> Assistant[Assistant] ``` -`defineModelContext` добавляется только к нужному handler: остальные маршруты -остаются HTTP-only. Ассистент и внутренний MCP используют тот же обработчик; -доступ к данным всё равно проверяет Core. [Пошаговый пример](./first-extension.md) -и [архитектура ассистента](./assistant-architecture.md) раскрывают этот путь. +Add `defineModelContext` only to the handlers you want to expose; other routes +remain HTTP-only. The assistant and internal MCP use the same handler, while +Core still checks data access. See the [tutorial](./first-extension.md) and +[assistant architecture](./assistant-architecture.md). + +Server handlers use H3 file routing. UI imports a separate browser entry. +Declared model handlers share checks across HTTP, the assistant, and internal MCP. +Plugin tables use `plugin__` and their own migrations. See +[Kit](../reference/kit-guide.md) and [plugin actions](./plugin-actions.md). -Серверные обработчики расширений используют H3 file routing. UI импортирует -отдельный браузерный entry. Объявленные model handlers используются HTTP, -ассистентом и внутренним MCP с общими проверками. Таблицы расширений имеют префикс -`plugin__` и собственные миграции. Подробности — в -[Kit](../reference/kit-guide.md) и [контракте действий](./plugin-actions.md). + -## Доступ и хранение +## Access and storage -Права проверяет Core: действия, поля, условия строк и grants настроек. UI metadata -не является границей доступа. История и транзакционные hooks проходят через общий -writer; прямой SQL и внешние эффекты не получают эти гарантии автоматически. +Core enforces action, field, row, and settings permissions. UI metadata is not +an access boundary. History and transactional hooks use the shared writer; +direct SQL and external effects do not automatically receive those guarantees. -Файлы имеют метаданные в PostgreSQL и содержимое в приватном S3 bucket. -Workspace группирует видимые коллекции, но не изолирует компании. -Произвольные плагины исполняются как доверенный код; песочницы нет. +File metadata lives in PostgreSQL and content in a private S3 bucket. +Workspaces group visible collections but do not isolate companies. +Arbitrary plugins run as trusted code; there is no sandbox. -Рекомендации по изменениям — в -[CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md). +See [CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md) +for change guidelines. diff --git a/docs/development/assistant-architecture.md b/docs/development/assistant-architecture.md index d4ae24e..2c283a8 100644 --- a/docs/development/assistant-architecture.md +++ b/docs/development/assistant-architecture.md @@ -1,48 +1,53 @@ -# Архитектура ассистента и model context + -Ассистент — опциональная функция Core. Ключ и маршрут OpenAI-совместимого -провайдера остаются на сервере; UI передаёт текущую страницу и показывает потоковый -ответ. Поведение и лимиты описаны на [странице функции](../features/assistant.md). +# Assistant architecture and model context + +The assistant is optional Core functionality. The OpenAI-compatible provider's +key and endpoint stay on the server; UI supplies the current page and displays +the streamed response. Behavior and limits are in the [feature guide](../features/assistant.md). ```mermaid flowchart LR - UI[UI: чат и контекст страницы] --> Core[Core: запрос и поток ответа] - Core --> Provider[Настроенный модельный провайдер] - Core --> MCP[Внутренний MCP: инструменты] - MCP --> Items[Сервисы items и permissions] - MCP --> Model[Model handlers расширений] + UI[UI: chat and page context] --> Core[Core: request and response stream] + Core --> Provider[Configured model provider] + Core --> MCP[Internal MCP: tools] + MCP --> Items[Items and permissions services] + MCP --> Model[Plugin model handlers] Model --> Items Items --> DB[(PostgreSQL)] ``` -## Инструменты и данные - -Встроенные инструменты исследуют разрешённую схему, читают/ищут записи и готовят -фильтры и формы. Часть контекста поступает из display metadata и системного -справочника терминов. Технические имена остаются контрактом API; словарь терминов -помогает сопоставить выражения пользователя с данными, но не меняет права. -Точные сценарии и ограничения перечислены в [функциональном описании](../features/assistant.md). - -Чат требует человеческого пользователя: superuser, хотя бы один data grant -read/create/update либо активное личное Google-подключение. -Каждый вызов инструмента проходит серверную проверку. `items` применяет права -действий, полей и строк; при MCP-вызове учитывается также `mcp.enabled` у каждой -затронутой коллекции, включая связанные пути и superuser. Публичного внешнего MCP -endpoint сейчас нет. Данные, возвращённые инструментом, могут попасть к -настроенному модельному провайдеру. - -## Действия расширений - -`server/api/**/*.post.ts` — обычный H3 HTTP handler. Разработчик явно оборачивает -подходящий обработчик в `defineModelContext(defineHandler(...), annotation)`. -`defineModelAnnotation` задаёт название, описание, `AccessGate`, при необходимости -`page` и `readOnly`. Сборщик Kit выводит runtime-схемы входа и выхода из типов -TypeScript. Один обработчик и его middleware вызываются по HTTP, из ассистента -и внутреннего MCP; ручного реестра в `plugin.ts` нет. - -`AccessGate.authenticated` допускает запрос, но не выдаёт доступ к коллекциям. -`useItems(event)` использует права вызывающего. Для `readOnly: true` запись через -items блокируется. Привилегированное `storage.own` model handler не получает. -Подробный контракт, валидация и ограничения — в -[действиях расширений](./plugin-actions.md); рабочий пример — -[calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator). + + +## Tools and data + +Built-in tools inspect permitted schemas, read/search records, and prepare filters +and forms. Display metadata and the system terminology dictionary supply part of +the context. Technical names remain the API contract; terminology helps match +user expressions to data without changing permissions. Supported scenarios and +limits are listed in the [feature guide](../features/assistant.md). + +Chat requires a human user who is a superuser, has at least one read/create/update +data grant, or has an active personal Google connection. Every tool call undergoes +server checks. `items` enforces action, field, and row permissions. MCP calls also +respect `mcp.enabled` on every affected collection, including related paths and +superuser requests. There is no external public MCP endpoint. Data returned by +tools may be sent to the configured model provider. + + + +## Plugin actions + +`server/api/**/*.post.ts` is an ordinary H3 HTTP handler. Developers explicitly +wrap suitable handlers in `defineModelContext(defineHandler(...), annotation)`. +`defineModelAnnotation` supplies a title, description, `AccessGate`, and optional +`page` and `readOnly`. The Kit builder derives input/output runtime schemas from +TypeScript types. HTTP, the assistant, and internal MCP call the same handler and +middleware; there is no manual registry in `plugin.ts`. + +`AccessGate.authenticated` admits a request but does not grant collection access. +`useItems(event)` uses the caller's permissions. `readOnly: true` blocks writes +through items. Model handlers do not receive privileged `storage.own`. +See [plugin actions](./plugin-actions.md) for validation and limitations, and +[calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator) +for a working example. diff --git a/docs/development/documentation.md b/docs/development/documentation.md index a1c3a37..4c5ab71 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -1,24 +1,45 @@ -# Сопровождение документации + -Пользовательское поведение описывается в `docs/guide` и `docs/features`, архитектура -и эксплуатация — в `docs/development`, границы безопасности — в `docs/security`. -Публичные страницы содержат актуальные контракты, примеры и ограничения. +# Documentation workflow -## Источники справочников +User behavior lives in `docs/guide` and `docs/features`, architecture and operations +in `docs/development`, and security boundaries in `docs/security`. Public pages +describe current contracts, examples, and limits. -| Содержание | Где редактировать | -| ----------------------- | ----------------------------------------------------------------- | -| HTTP-маршруты | Обработчики Core; AST scanner читает объявления | -| Требования доступа | `docs/reference/endpoints.json`, после проверки handler и service | -| Подробные OpenAPI-схемы | `scripts/docs/http-contracts.mjs` и файлы контрактов рядом | -| SDK/Kit | Публичные TypeScript-типы и руководства внутри пакетов | -| Руководства SDK/Kit | README, HOOKS, FIELDS, LIFECYCLE, CAPABILITIES, UI внутри пакетов | + -Генератор создаёт OpenAPI, матрицу маршрутов, копии package guides и справочники -TypeDoc. Сгенерированные файлы не редактируются вручную. Внутренние планы, -исследования, локальные отчёты и приватный архив не копируются в сайт. +## Languages -## Проверки +English is the default at the site root. Russian pages mirror the same paths under +`docs/ru` and are published at `/ru/`. The language menu keeps the current page. +The application has its own existing i18next catalogs; documentation localization +does not change the admin interface, API locale defaults, or application routes. + +Edit both language versions when behavior changes. Package guides use English +source files and `.ru.md` counterparts. The generator copies the appropriate +version into each locale. API identifiers, examples, permissions, and limits +must remain equivalent. Translations must not turn a proposal into implemented behavior. + + + +## Reference sources + +| Content | Edit here | +| ------------------------ | ------------------------------------------------------------------------------------- | +| HTTP routes | Core handlers; the AST scanner reads declarations | +| Access requirements | `docs/reference/endpoints.json`, after reviewing handler and service | +| Detailed OpenAPI schemas | `scripts/docs/http-contracts.mjs` and adjacent contract files | +| API text translations | Documentation translation catalogs under `scripts/docs/locales` | +| SDK/Kit | Public TypeScript types and package guides | +| SDK/Kit guides | Package README, HOOKS, FIELDS, LIFECYCLE, CAPABILITIES, UI, and `.ru.md` counterparts | + +The generator produces localized OpenAPI descriptions, route matrices, package +guide copies, and TypeDoc reference pages. Do not edit generated files manually. +Internal plans, research, local reports, and private archives are not copied into the site. + + + +## Checks ```sh pnpm docs:generate @@ -26,15 +47,15 @@ pnpm docs:check pnpm docs:build ``` -`docs:check` проверяет покрытие статических Core routes, актуальность каталога -и сгенерированных контрактов. Сборка сайта проверяет Markdown-ссылки. -Динамические маршруты расширений и protocol endpoints oidc-provider описываются -отдельно. Уровень `route-only` означает, что подробная схема операции ещё отсутствует. +`docs:check` validates static Core route coverage, catalog and generated-contract +freshness, and language coverage. Site builds validate Markdown links. Dynamic +plugin routes and oidc-provider protocol endpoints are documented separately. +`route-only` means a detailed operation schema is still missing. -При изменении endpoint проверяйте не только handler, но и предметную авторизацию, -обновляйте контракт и соответствующую функциональную страницу. Описание доступа -не исполняет авторизацию: её подтверждают отрицательные интеграционные тесты. +When changing an endpoint, review both the handler and domain authorization, +update its contract and feature page in both languages. Documentation does not +enforce access: negative integration tests verify it. -Интеграционные проверки запускайте через `scripts/test.mjs` с одноразовой БД. -Для изменений общих контрактов выполняйте `pnpm check` и `pnpm build`. -Отдельно указывайте проверки, для которых не было подготовлено live-окружение. +Run integration tests through `scripts/test.mjs` with a disposable database. +For shared-contract changes, run `pnpm check` and `pnpm build`. Explicitly identify +checks whose live environment was not available. diff --git a/docs/development/first-extension.md b/docs/development/first-extension.md index 76d11cc..e5d272c 100644 --- a/docs/development/first-extension.md +++ b/docs/development/first-extension.md @@ -1,25 +1,18 @@ -# Первое расширение + -Этот путь использует работающие примеры [overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview) -(страница и HTTP endpoint) и [calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator) -(действие для ассистента). Примеры не включены в обычную установку. -Начните с [локального запуска](../guide/getting-started.md). +# Your first plugin -## 1. Создайте пакет +This walkthrough uses the working [overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview) example (page and HTTP endpoint) and [calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator) (assistant action). Examples are not enabled in a normal installation. Start with [local setup](../guide/getting-started.md). -Для собственного workspace-пакета, например `@asmblyr-collaborative/plugin-hello`, используйте -`examples/plugins/hello`: -`pnpm-workspace.yaml` включает этот каталог. Удобная отправная точка — исходники -`examples/plugins/overview`, без копирования `dist` и `.asmblyr`. Переименуйте -`name` в `package.json` и `asmblyr.manifest.namespace`; для нового namespace -переименуйте первый каталог в `server/api/`. Namespace должен быть -уникальным и совпадать с первым сегментом HTTP-пути. Сохраните exports для -корневого entry, `./routes`, `./collections`, `./package.json` и `./ui`, script -`"build": "asmblyr-plugin build"`, TypeScript-конфигурацию и зависимости Kit/H3. -Версия манифеста сейчас `1`. [Пакет overview](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/package.json) -показывает полный рабочий manifest. + -Обязательный `plugin.ts` минимален: +## 1. Create a package + +For a workspace package such as @asmblyr-collaborative/plugin-hello, use examples/plugins/hello, included by pnpm-workspace.yaml. Start from examples/plugins/overview without copying dist or .asmblyr. + +Rename package.json name and asmblyr.manifest.namespace, then the first directory under server/api to the new namespace. It must be unique and match the first HTTP path segment. Keep exports for the root entry, ./routes, ./collections, ./package.json, and ./ui, the build script `asmblyr-plugin build`, TypeScript configuration, and Kit/H3 dependencies. Manifest version is 1. The [overview package](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/package.json) contains a complete manifest. + +Required plugin.ts is minimal: ```ts import { definePlugin } from "@asmblyr-collaborative/kit"; @@ -27,12 +20,13 @@ import { definePlugin } from "@asmblyr-collaborative/kit"; export default definePlugin({}); ``` -Маршруты, страницы и model handlers здесь вручную не регистрируются. +Do not manually register routes, pages, or model handlers here. + + -## 2. Добавьте серверный обработчик +## 2. Add a server handler -После переименования namespace файл `server/api/hello/session.get.ts` определяет -`GET /hello/session`. Сохраните тело обработчика overview и общий тип ответа: +After renaming the namespace, server/api/hello/session.get.ts defines GET /hello/session. Keep the overview handler and shared response type: ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -51,17 +45,13 @@ export default defineHandler((event): Overview => { }); ``` -Это адаптация [обработчика overview](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/server/api/overview/session.get.ts). -Core требует аутентификацию до вызова обработчика. Для записей используйте -`useAsmblyr(event).items` или `useItems(event)`: методы применяют права вызывающего. -`actor.displayName` требует заявленной capability `identity.profile` и -отдельного одобрения проекта. Одно лишь имя в manifest доступа не даёт. -Обработчики с записью проверяют предметные права и валидируют вход. +Adapted from the [overview handler](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/server/api/overview/session.get.ts). Core authenticates before invoking it. For records, use useAsmblyr(event).items or useItems(event), which enforce caller permissions. Actor.displayName requires declared identity.profile capability and separate project approval. A manifest declaration alone grants nothing. Write handlers must validate input and domain permissions. -## 3. Добавьте страницу + -`ui/index.ts` — отдельный браузерный entry; он не импортирует `plugin.ts` или -серверные файлы: +## 3. Add a page + +ui/index.ts is a separate browser entry. It must not import plugin.ts or server files. ```ts "use client"; @@ -70,24 +60,19 @@ import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; import { OverviewPage } from "./pages/overview-page.tsx"; export default defineUiPlugin({ - pages: [{ id: "home", title: "Обзор", component: OverviewPage }], + pages: [{ id: "home", title: "Overview", component: OverviewPage }], }); ``` -В компоненте `request("/session")` из `PluginPageProps` вызывает namespace endpoint -через BFF и текущую сессию. Рабочие компонент и hook есть в -[overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview/ui). -После подключения страница появляется по адресу -`/extensions/hello/home`. Отдельных прав на UI-страницу пока нет: -серверный endpoint сам должен проверять доступ к своим данным. Для вкладок записи -и редакторов полей смотрите [Kit UI](../reference/kit-ui.md) и -[поля](../reference/kit-fields.md). +The component's PluginPageProps request("/session") calls the namespace endpoint through the public API using the current session. Working components/hooks are in [overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview/ui). Once installed, the page appears at /extensions/hello/home. + +There are no separate UI-page permissions yet: server endpoints must check access to their own data. For record tabs and field editors, see [Kit UI](../reference/kit-ui.md) and [fields](../reference/kit-fields.md). -## 4. Подключите пакет и права + -Добавьте пакет в корневые `dependencies` или `devDependencies` через -`"@asmblyr-collaborative/plugin-hello": "workspace:*"`, выполните `pnpm install` и включите -его в корневом `package.json`: +## 4. Enable the package and capabilities + +Add `"@asmblyr-collaborative/plugin-hello": "workspace:*"` to root dependencies or devDependencies, run pnpm install, and enable it in root package.json: ```json { @@ -100,28 +85,23 @@ export default defineUiPlugin({ } ``` -Это **фрагмент**, не замена существующего блока: сохраните встроенные -`plugin-comments` и `plugin-google-workspace` с их разрешениями. Скопированный -manifest overview уже заявляет `identity.profile`; -в примере выше проект отдельно одобряет её для нового пакета. Отсутствующее -одобрение означает отсутствие -возможности. Серверные пакеты выполняются в процессе Core как доверенный код; -capabilities не создают песочницу. [Точный контракт](../reference/kit-capabilities.md). - -## 5. Опубликуйте действие для ассистента, если нужно - -Обычный H3 endpoint доступен по HTTP, но не появляется во внутреннем MCP. -Для model handler используйте `defineModelContext` вокруг того же -`defineHandler`, задайте `defineModelAnnotation({ title, description, -middleware: AccessGate.authenticated, readOnly: true })`. Модельный обработчик -должен быть статическим `.post.ts` маршрутом. Kit выведет схемы входа/выхода из -TypeScript; неподдерживаемые типы остановят сборку. Рабочий код с UI-формой — -[calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts). -Права на данные `AccessGate` не заменяет; внутри используйте `useItems(event)`. -Принцип вызова и ограничения описаны в [архитектуре ассистента](./assistant-architecture.md) -и [контракте действий](./plugin-actions.md). - -## 6. Проверьте и соберите +This is a fragment, not a replacement: preserve built-in plugin-comments and plugin-google-workspace entries and permissions. The copied overview manifest already declares identity.profile; the root configuration separately approves it. Missing approval means no capability. + +Server packages are trusted code in Core's process; capabilities do not create a sandbox. See the [capability contract](../reference/kit-capabilities.md). + + + +## 5. Optionally expose an assistant action + +Ordinary H3 endpoints work over HTTP but do not appear in internal MCP. Wrap the same defineHandler in defineModelContext<Input>, with `defineModelAnnotation({ title, description, middleware: AccessGate.authenticated, readOnly: true })`. + +A model handler must be a static .post.ts route. Kit derives input/output schemas from TypeScript and fails the build for unsupported types. See [calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts) for working code and UI form. + +AccessGate does not replace data authorization: use useItems(event). See [assistant architecture](./assistant-architecture.md) and [action contracts](./plugin-actions.md). + + + +## 6. Check and build ```sh pnpm --filter @asmblyr-collaborative/plugin-hello build @@ -129,13 +109,6 @@ pnpm --filter @asmblyr-collaborative/plugin-hello typecheck pnpm dev ``` -Имя фильтра замените реальным именем пакета. Перезапустите Core и UI после -изменения списка плагинов. Войдите в админку, откройте -`/extensions/hello/home` и проверьте запрос страницы. Для отдельного -примерного набора из репозитория доступны `pnpm build:examples` и -`node scripts/test.mjs core-plugins` с одноразовой БД. +Use your actual package name. Restart Core and UI after changing the plugin list. Sign in, open /extensions/hello/home, and verify the page request. Repository examples also support pnpm build:examples and node scripts/test.mjs core-plugins against a disposable database. -Перед выпуском запускайте `pnpm build`: production loader читает собранный -`dist/routes.json`, а UI — отдельный `./ui` entry. `dist` и `.asmblyr` не включайте -в Git. Собственные коллекции, миграции, hooks и настройки разбираются в -[Kit lifecycle](../reference/kit-lifecycle.md) и [Kit hooks](../reference/kit-hooks.md). +Run pnpm build before publishing. Production discovery reads dist/routes.json; UI uses the separate ./ui entry. Keep dist and .asmblyr out of Git. Collections, migrations, hooks, and settings are covered in [Kit lifecycle](../reference/kit-lifecycle.md) and [Kit hooks](../reference/kit-hooks.md). diff --git a/docs/development/local-development.md b/docs/development/local-development.md index f1aed70..4ac18f2 100644 --- a/docs/development/local-development.md +++ b/docs/development/local-development.md @@ -1,38 +1,45 @@ -# Локальная разработка - -## Структура репозитория - -| Каталог | Ответственность | -| ---------------------------------- | ----------------------------------------------------------- | -| `apps/core` | Fastify API, доменные сервисы, Knex, миграции и авторизация | -| `apps/ui` | Next.js админка и серверный BFF для сессии | -| `packages/contracts` | Общие публичные типы | -| `packages/sdk` | Типизированный HTTP-клиент | -| `packages/cli` | Подключение SDK и генерация типов | -| `packages/kit` | Серверный и браузерный контракт расширений, сборщик | -| `packages/plugin-comments` | Установленный по умолчанию плагин комментариев | -| `packages/plugin-google-workspace` | Инструменты личных Google-подключений | -| `examples/plugins` | Не включённые по умолчанию примеры | -| `deploy`, `docs`, `scripts` | Развёртывание, документация и проверки | - -[Схема компонентов](./architecture.md) показывает путь запросов и данных. - -## Запуск и изменения - -Требуются Node.js 22+, pnpm 11.13.1 и Docker Compose. Выполните -[первый запуск](../guide/getting-started.md), затем работайте через `pnpm dev`. -Эта команда собирает пакеты и запускает Core с UI. Маршруты расширений в локальных -workspace-пакетах отслеживает Core; изменение списка установленных пакетов требует -перезапуска Core и UI. Для production UI и пакеты собираются заново. - -Структуру Core меняйте новой версионной миграцией в `apps/core/migrations`. -Пользовательские коллекции живут в схеме `public`; имена `asmblyr_` и `plugin_` -зарезервированы. Миграции существующей установки выполняются через `pnpm db:migrate`. -Доступ к данным, правам и файлам проверяйте в сервисах Core, а не только в UI. -[Политики и границы доступа](../features/access.md) и -[матрица доступа](../security/access-matrix.md) описывают текущие правила. - -## Проверки + + +# Local development + + + +## Repository layout + +| Directory | Responsibility | +| ---------------------------------- | -------------------------------------------------------------------- | +| `apps/core` | Fastify API, domain services, Knex, migrations, authentication | +| `apps/ui` | Next.js admin pages and server rendering; Core owns sessions and API | +| `packages/contracts` | Shared public types | +| `packages/sdk` | Typed HTTP client | +| `packages/cli` | SDK connection and type generation | +| `packages/kit` | Server/browser plugin contracts and builder | +| `packages/plugin-comments` | Discussion plugin installed by default | +| `packages/plugin-google-workspace` | Tools for personal Google connections | +| `examples/plugins` | Examples disabled by default | +| `deploy`, `docs`, `scripts` | Deployment, documentation, checks | + +The [architecture](./architecture.md) shows request and data paths. + + + +## Running and changing the application + +Use Node.js 22+, pnpm 11.13.1, and Docker Compose. Complete +[first-time setup](../guide/getting-started.md), then work with `pnpm dev`. +This builds packages and starts Core and UI. Core watches plugin routes in local +workspace packages. Changing the installed package list requires restarting both +Core and UI. Rebuild UI and packages for production. + +Change Core schemas with a new versioned migration in `apps/core/migrations`. +Custom collections live in `public`; `asmblyr_` and `plugin_` names are reserved. +Run existing-installation migrations through `pnpm db:migrate`. Check data, +permission, and file access in Core services, not just UI. Current rules are in +[policies](../features/access.md) and the [access matrix](../security/access-matrix.md). + + + +## Checks ```sh pnpm format:check @@ -42,13 +49,12 @@ pnpm docs:build pnpm build ``` -Для интеграционных тестов используйте **только** `node scripts/test.mjs` и -одноразовую БД: скрипту нужен локальный PostgreSQL и `TEST_DATABASE_ADMIN_URL` -с правом `CREATEDB`. `pnpm check` объединяет публикационную проверку, формат, -типы, lint, тесты, документацию; полный набор может быть долгим. -Инструкции по обновлению сгенерированных справочников — в -[сопровождении документации](./documentation.md). +Run integration tests **only** through `node scripts/test.mjs` and its disposable +database. The runner needs local PostgreSQL and `TEST_DATABASE_ADMIN_URL` with +`CREATEDB`. `pnpm check` combines publication checks, formatting, types, lint, +tests, and documentation; the full suite can take time. Generated reference +updates are explained in the [documentation workflow](./documentation.md). -Перед pull request прочитайте [CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md). -Рабочие health/ready проверки, журналы и резервные копии описаны в -[эксплуатации](./operations.md). +Before opening a pull request, read +[CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md). +See [operations](./operations.md) for health/readiness checks, logs, and backups. diff --git a/docs/development/operations.md b/docs/development/operations.md index 384bb99..9016f1d 100644 --- a/docs/development/operations.md +++ b/docs/development/operations.md @@ -1,30 +1,36 @@ -# Эксплуатация и восстановление + -Одна команда использует отдельную установку, PostgreSQL и приватный bucket. -SMTP необязателен: администратор может вручную передать приглашение или ссылку -восстановления. Workspaces не разделяют данные компаний. +# Operations and recovery -## Сборка и установка +Each team uses a separate installation, PostgreSQL database, and private bucket. +SMTP is optional: an administrator can share invitation or recovery links manually. +Workspaces do not separate companies' data. -Dockerfile собирает единый образ Core/UI с закреплённым Node image digest, pnpm -и lockfile; приложения работают от пользователя `node`. UI доступен на порту -`3000`, Core API — на `3001`. PostgreSQL и S3 подключаются отдельно. -GitHub workflow проверяет типы, lint, тесты, документацию, production build и -запуск контейнера с одноразовым PostgreSQL перед публикацией образа в GHCR. + -[Шаблоны установки](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.md) -включают локальную установку с S3 и вариант для внешних PostgreSQL/bucket. -Миграции выполняются перед Core. Для публичной установки нужны HTTPS reverse proxy, -точный внешний `AUTH_UI_URL`, приватные БД и bucket. OAuth signing keys сохраняйте -между выпусками и резервируйте отдельно. +## Build and deployment -Перед обновлением создайте копию данных и проверьте миграции на восстановленной -установке. Откат образа допустим только с совместимой схемой; разрушительный -rollback миграций не заменяет восстановление. +The Dockerfile builds separate Core/UI images and a compatible monolithic target, +using a pinned Node image digest, pnpm, and the lockfile. Applications run as `node`. +The primary deployment exposes a shared UI/API origin; PostgreSQL and S3 are external. +The GitHub workflow checks types, lint, tests, documentation, production builds, +and container startup with disposable PostgreSQL before publishing to GHCR. -## Копии локальной Docker-установки +[Deployment templates](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.md) +include a local setup with S3 and a setup for external PostgreSQL/buckets. +Migrations run before Core. Public installations require an HTTPS reverse proxy, +the exact external `AUTH_UI_URL`, and private database/bucket access. Preserve +OAuth signing keys across releases and back them up separately. -Следующие инструменты обслуживают только `deploy/local/compose.yaml`: +Before upgrading, back up data and test migrations on a restored installation. +Image rollback requires a compatible schema; destructive migration rollback is +not a substitute for recovery. + + + +## Backups of the local Docker installation + +These tools support only `deploy/local/compose.yaml`: ```sh node scripts/operations/backup.mjs /private/asmblyr.env asmblyr-local /private/backups/new-copy @@ -32,56 +38,62 @@ node scripts/operations/restore.mjs /private/restore.env asmblyr-restore-check / node scripts/operations/status.mjs /private/restore.env asmblyr-restore-check ``` -Backup останавливает UI/Core и S3, сохраняет `pg_dump -Fc` и offline object volume, -записывает SHA-256 и ID образов, затем поднимает сервисы даже при ошибке копирования. -Каталог копии должен быть новым; других авторов БД/S3 в этот момент быть не должно. +Backup stops UI/Core and S3, saves `pg_dump -Fc` and the offline object volume, +records SHA-256 hashes and image IDs, then restarts services even if copying fails. +Use a new backup directory and ensure no other process writes to the database or S3. -Restore проверяет hashes и версии образов, допускает только новый проект -`asmblyr-restore-NAME` без существующих volumes. Он выполняет `pg_restore`, -восстанавливает object volume и запускает миграции. Используйте отдельный UI port. -После восстановления проверьте вход, число записей, доступ и содержимое файлов. -Env и signing keys сохраняются отдельно от копии БД/объектов. +Restore verifies hashes and image versions and accepts only a new +`asmblyr-restore-NAME` project without existing volumes. It runs `pg_restore`, +restores the object volume, and applies migrations. Use a separate UI port. +After recovery, verify sign-in, record counts, permissions, and file content. +Back up environment files and signing keys separately from database/object data. -Для внешних PostgreSQL/S3 оператор организует согласованную копию при остановленных -авторах и восстановление в новые ресурсы. Скрипты локального compose не копируют -внешние облачные bucket и не создают инфраструктуру. Копии содержат пользовательские -данные и credential hashes: храните их приватно и шифруйте средствами backup-системы. +For external PostgreSQL/S3, the operator takes a consistent backup with writers +stopped and restores into new resources. Local Compose scripts do not copy cloud +buckets or provision infrastructure. Backups contain user data and credential +hashes: keep them private and encrypt them with your backup system. -## Восстановить администратора + -После сборки Core оператор с доступом к БД может выполнить: +## Recover an administrator + +After building Core, an operator with database access can run: ```sh node scripts/operations/admin-recover.mjs admin@example.test /private/new-credential.json ``` -`DATABASE_URL` передаётся через окружение процесса. Команда создаёт приватный файл -с новым случайным паролем, не печатает его и отказывается заменить существующий -файл. Предыдущие способы входа, сессии и OAuth consents отзываются. -Обычных пользователей восстанавливает superuser через ссылку в админке. +Pass `DATABASE_URL` through the process environment. The command writes a private +file containing a new random password, does not print it, and refuses to overwrite +an existing file. Previous sign-in methods, sessions, and OAuth consents are revoked. +A superuser recovers ordinary users through a link in the admin interface. + + -## Ключи OAuth +## OAuth keys -Ключи создаются отдельно от сборки: +Generate keys separately from the build: ```sh node scripts/operations/create-oauth-keys.mjs .local-data/oauth-keys.json ``` -Укажите `OAUTH_ISSUER_URL` и `OAUTH_KEYS_FILE` в окружении Core. Путь к ключам -должен быть доступен из рабочего каталога Core; для локального запуска из -`apps/core` это `../../.local-data/oauth-keys.json`. В контейнере подключайте -приватный файл отдельно и указывайте его путь внутри контейнера. Команда -отказывается заменить существующий файл; не пересоздавайте ключи при обновлении. +Set `OAUTH_ISSUER_URL` and `OAUTH_KEYS_FILE` in Core's environment. The key path +must resolve from Core's working directory: for local runs from `apps/core`, use +`../../.local-data/oauth-keys.json`. Mount the private file separately in containers +and specify its container path. The command refuses to overwrite an existing +file; do not regenerate keys during upgrades. + + -## Диагностика +## Diagnostics -Опциональный [Sentry и локальные p95/p99](../features/monitoring.md) подключаются -в системных настройках. Мониторинг сам по себе не создаёт правила алертов, -retention или dashboards в вашем Sentry: их настраивает оператор. +Optional [Sentry and local p95/p99 metrics](../features/monitoring.md) are configured +in system settings. Monitoring does not create Sentry alert rules, retention, +or dashboards; the operator configures those. -Воспроизводимый замер API на синтетической схеме запускается только явно. -Из корня репозитория в PowerShell: +The reproducible API benchmark on a synthetic schema runs only when explicitly +enabled. From the repository root in PowerShell: ```powershell $env:ASMBLYR_PERFORMANCE_AUDIT = "1" @@ -89,22 +101,23 @@ node scripts/test.mjs core-performance Remove-Item Env:\ASMBLYR_PERFORMANCE_AUDIT ``` -Runner создаёт и удаляет отдельную временную локальную БД. Нужен доступ к локальному -PostgreSQL с правом CREATE DATABASE; удалённая рабочая БД не допускается. Тест -создаёт 40 коллекций/20 000 записей, замеряет список, поиск и глубокую страницу -при 1/8 параллельных запросах, выводит p50/p95/p99, SQL/request и планы поиска. -Это Fastify inject без браузера и внешней сети, для superuser и простой схемы; -он не доказывает производственный SLO или скорость сложных прав/связей. - -Для сравнения накладных расходов задайте `ASMBLYR_PERFORMANCE_MONITORING=disabled` -или `enabled`. Enabled использует настоящий Sentry SDK с офлайн-транспортом, -API performance и SQL spans; сеть/доставка в Sentry не измеряются. -`ASMBLYR_PERFORMANCE_SAMPLE_RATE` задаёт долю трассировок от 0 до 1 (по умолчанию 1 -в тесте). `ASMBLYR_PERFORMANCE_REPORT` сохраняет JSON в указанный приватный путь. -После проверки удалите тестовые переменные из окружения оболочки. - -`/health` проверяет процесс, `/ready` — БД и обязательные миграции. -Логи не должны содержать тела запросов, токены и конфигурационные секреты. -Retentions задаются окружением Core; история бизнес-изменений по умолчанию -не удаляется автоматически. Настройте мониторинг, внешние копии, проверку -восстановления и доставку алертов для своей установки. +The runner creates and removes a temporary local database. Local PostgreSQL with +CREATE DATABASE permission is required; a remote working database is rejected. +The test creates 40 collections and 20,000 records, measures listing, search, +and deep pagination with 1/8 concurrent requests, and reports p50/p95/p99, +SQL/request, and search plans. It uses Fastify inject without a browser or external +network, a superuser, and a simple schema. It does not establish a production SLO +or performance with complex permissions/relationships. + +Compare overhead with `ASMBLYR_PERFORMANCE_MONITORING=disabled` or `enabled`. +Enabled mode uses the real Sentry SDK with an offline transport, API performance, +and SQL spans; network/Sentry delivery is not measured. +`ASMBLYR_PERFORMANCE_SAMPLE_RATE` sets trace sampling from 0 to 1 (default 1 in +the test). `ASMBLYR_PERFORMANCE_REPORT` writes JSON to the specified private path. +Remove test variables from the shell environment afterwards. + +`/health` checks the process; `/ready` checks the database and required migrations. +Logs must not contain request bodies, tokens, or configuration secrets. Core's +environment controls retention; business change history is not automatically +deleted by default. Configure monitoring, external backups, recovery checks, +and alert delivery for your installation. diff --git a/docs/development/plugin-actions.md b/docs/development/plugin-actions.md index 5fd82dc..c1cb075 100644 --- a/docs/development/plugin-actions.md +++ b/docs/development/plugin-actions.md @@ -1,11 +1,14 @@ -# Действия плагинов: HTTP, MCP и ассистент + -Действующий контракт: file routing H3 v2, типизированная аннотация и один обработчик. -Публичного MCP endpoint пока нет. Инструкции по разработке расширений — в [Kit](../reference/kit-guide.md). +# Plugin actions: HTTP, MCP, and the assistant -## Объявление +The current contract uses H3 v2 file routing, typed annotations, and one handler. There is no public MCP endpoint yet. See [Kit](../reference/kit-guide.md) for plugin development. -`server/api/calculator/calculate.post.ts`: + + +## Declaration + +server/api/calculator/calculate.post.ts: ```ts import { @@ -17,8 +20,8 @@ import { import type { CalculationInput } from "../../../shared/calculation.ts"; const annotate = defineModelAnnotation({ - title: "Расчёт стоимости", - description: "Рассчитать стоимость подписки по параметрам пользователя.", + title: "Calculate price", + description: "Calculate subscription cost from the user's parameters.", middleware: AccessGate.authenticated, readOnly: true, page: "home", @@ -26,7 +29,7 @@ const annotate = defineModelAnnotation({ export default defineModelContext( defineHandler(async (event) => { - // Core проверил JSON до входа; здесь обычный H3 и предметная логика. + // Core validated JSON before entry; use ordinary H3 and domain logic here. const input = (await event.req.json()) as CalculationInput; return { total: input.users * input.monthlyPrice * input.months }; }), @@ -34,50 +37,32 @@ export default defineModelContext( ); ``` -Полный расчёт с копейками и округлением — в -[calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts). -Пример выше иллюстрирует объявление, не денежную арифметику. - -- Путь и метод определяет файл. Model handler требует статический `.post.ts` - внутри namespace. ID получается из пути после namespace: `calculate`; - вложенные сегменты соединяются дефисами. Коллизия ID останавливает запуск. -- `plugin.ts` остаётся `definePlugin({})`. Ручной регистрации и отдельного - `server/actions` нет. Общий `/extensions/:namespace/actions/:action` не используется. -- Обёртка `defineModelContext` явно публикует инструмент; обычные H3 endpoints - в MCP не видны. Аннотация сохраняется в нативной metadata H3. -- `middleware` сейчас принимает `AccessGate.authenticated` или `AccessGate.superuser`. - Дополнительную предметную middleware можно указать во внутреннем `defineHandler`. -- `page` — необязательный ID страницы, принимающей подготовленный результат. - `readOnly` по умолчанию false. Для вычислений указывайте true. -- Старые `defineAction`/`defineActionContract` сохранены как deprecated для совместимости. - Новый код использует этот API. `bindModelDefinition` предназначен загрузчику Core; - разработчик плагина его не вызывает. - -## Генерация и валидация - -Существующий сборщик плагинов анализирует `` и возвращаемый тип внутреннего -обработчика **до стирания типов TypeScript**. Результат async разворачивается из -Promise. При желании результат можно явно описать интерфейсом. - -Поддержаны строки, числа, boolean, литералы, null, объединения внутри полей, -массивы (включая readonly) и вложенные объекты с известными ключами. Корневые вход -и выход — объекты. Все поля обязательны; отсутствие значения обозначается null. -Лишние ключи запрещены, в том числе во вложенных объектах. - -`any`, `unknown`, необязательные свойства, рекурсивные типы, index signatures, -классы, функции, кортежи и не-JSON типы останавливают сборку. Нельзя молча заменить -непонятный тип на разрешение любых данных. Ограничение сложности — 20 уровней и -1000 узлов. У плагина нужен tsconfig; прямой вызов обёртки должен быть default export -или инициализатором экспортированной по умолчанию локальной переменной. Импорт -`defineModelContext` с alias поддержан; произвольные фабрики вокруг него пока нет. - -Описания и ограничения свойств задаются в JSDoc: +See [calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts) for minor-unit arithmetic and rounding. The snippet demonstrates declaration, not monetary arithmetic. + +- File paths determine method and URL. Model handlers require a static .post.ts route inside their namespace. IDs derive from the remaining path, joining nested segments with hyphens. Collisions stop startup. +- plugin.ts remains definePlugin({}). There is no manual registry, separate server/actions directory, or generic /extensions/:namespace/actions/:action route. +- defineModelContext explicitly publishes a tool. Plain H3 endpoints are invisible to MCP. Annotations use native H3 metadata. +- Middleware supports AccessGate.authenticated or AccessGate.superuser. Additional domain middleware belongs inside defineHandler. +- Page optionally names the page accepting a prepared result. ReadOnly defaults false; set true for calculations. +- Legacy defineAction/defineActionContract remain deprecated for compatibility. New code uses this API. bindModelDefinition is for Core's loader, not plugin authors. + + + +## Generation and validation + +The plugin builder analyzes Input and the inner handler's return type before TypeScript erases them. Async results are unwrapped from Promise. An explicit output interface is optional. + +Supported types: strings, numbers, booleans, literals, null, field unions, arrays (including readonly), and nested objects with known keys. Root input/output must be objects. Every property is required; represent absence with null. Extra keys are forbidden, including in nested objects. + +Any, unknown, optional properties, recursion, index signatures, classes, functions, tuples, and non-JSON types fail the build rather than silently accepting arbitrary data. Complexity limits are 20 levels and 1000 nodes. Plugins need a tsconfig. The direct wrapper call must be the default export or initializer of a default-exported local variable. Aliased defineModelContext imports work; arbitrary wrapper factories do not. + +JSDoc supplies property descriptions and constraints: ```ts export interface CalculationInput { /** - * Количество пользователей. - * @title Пользователи + * Number of users. + * @title Users * @integer * @minimum 1 * @maximum 100000 @@ -86,112 +71,73 @@ export interface CalculationInput { } ``` -Обычный комментарий становится description. Поддержаны `@title`, `@description`, -`@integer`, `@minimum`, `@maximum`, `@multipleOf`, `@minLength`, `@maxLength`, -`@minItems`, `@maxItems`. Числовые ограничения применяются к простому типу свойства; -несовместимый тип, неизвестный тег или некорректные границы дают ошибку. +Normal comments become descriptions. Supported tags: @title, @description, @integer, @minimum, @maximum, @multipleOf, @minLength, @maxLength, @minItems, @maxItems. Numeric constraints apply to simple property types. Incompatible types, unknown tags, and invalid bounds fail generation. + +Schemas go to .asmblyr/models/<route-path>.json and the production route index. .asmblyr is ignored. Type/schema failures leave previous dist output intact. The source loader regenerates schemas before loading handlers, so shared-type changes apply after a development Core restart. Missing schemas on annotated handlers prevent loading. -Схемы пишутся в `.asmblyr/models/<путь маршрута>.json` и production route index. -`.asmblyr` исключён из Git. Сборка не заменяет предыдущий dist при ошибке типов -или генерации. Source loader пересоздаёт схемы перед загрузкой обработчиков, -поэтому изменение общего TS-типа подхватывается при dev-перезапуске Core. -Отсутствующая схема у аннотированного обработчика останавливает загрузку. +Core validates input before and output after the handler, using Zod from generated JSON Schema. A generic alone does not validate at runtime outside Core. -Core проверяет вход до handler и выход после него. Runtime использует Zod из -сгенерированной JSON Schema. Generic сам по себе не выполняет валидацию без Core. +Forms import JSON and use useAction<Input, Output>(model, props, options) from @asmblyr-collaborative/kit/ui/use-action. modelField from @asmblyr-collaborative/kit/model returns field label/description. Enable resolveJsonModule:true in tsconfig. Browser code must not import server handlers or root Kit. Run pnpm build:plugins for initial generation; pnpm dev already does this before UI startup. -Форма импортирует JSON и использует `useAction(model, props, options)` -из `@asmblyr-collaborative/kit/ui/use-action`. `modelField` из `@asmblyr-collaborative/kit/model` возвращает -подпись/описание поля. В tsconfig нужен `resolveJsonModule: true`. Браузер не -импортирует серверный handler или корневой Kit. Для начальной генерации — -`pnpm build:plugins`; `pnpm dev` уже выполняет её перед запуском UI. + -## Права и данные +## Permissions and data -`AccessGate.authenticated` разрешает вход аутентифицированному пользователю или -сервисному аккаунту. Это не разрешение на коллекции. `AccessGate.superuser` -дополнительно требует superuser; такой инструмент скрыт от остальных при discovery -и повторно проверяется на каждом вызове. +AccessGate.authenticated allows authenticated humans or services, not collection access. AccessGate.superuser adds superuser checks; discovery hides those tools from others and calls recheck access. ```ts const items = useItems(event); await items.update("articles", id, { title }); ``` -`useItems(event)` — общий API данных для обычных endpoints и model handlers. -Он автоматически использует личность и права текущего запроса: коллекцию, действие, -поля, проверки связей, валидацию и историю Core. Нельзя передать другого actor или -повысить права. HTTP и MCP используют тот же handler и сервис `/items`. -При отказе HTTP возвращает 403, MCP — ошибку `PERMISSION_DENIED` без содержимого -недоступных записей. Сессия и права заново проверяются перед каждым MCP-вызовом. - -В MCP есть дополнительное пересечение с публикацией коллекций: `mcp.enabled=false` -закрывает чтение и запись, включая фильтры, поиск, подписи и изменение связей. -Это действует и для superuser. HTTP-права от настройки публикации не меняются. -`readOnly: true` блокирует create/update/delete/commit через items в обоих каналах. -Удаление через MCP также блокируется, если настроенные cascade/setNull/setDefault -могут затронуть отключённую коллекцию, включая цепочку каскадов. Проверка по схеме -консервативна: она не читает скрытые строки и действует даже при пустой дочерней таблице. - -`useActionContext(event)` даёт actor, superuser, signal и items. Полный контекст -`useAsmblyr`, привилегированный storage плагина, Knex, токены и cookies не передаются. -Условия прав на строки применяются через общий items API. Пользовательский фильтр запроса не является политикой доступа; плагин не может подменить actor или расширить его grants. - -Пакеты выполняются как доверенный серверный код, без песочницы. `readOnly` ограничивает -выданный items, но не может запретить сторонний сетевой клиент, импортированный самим -плагином. MCP hints отражают readOnly; для произвольного кода openWorldHint=true. - -## Вызов и транзакции - -- Core: `POST /calculator/calculate`. -- UI: `POST /api/calculator/calculate` напрямую в Core с браузерной сессией. -- MCP: `plugin_calculator__calculate`, JSON text и structured content. -- Ответ HTTP: `{ data: { namespace, actionId, input, output } }`. - -Handler получает нормализованный POST с проверенным JSON. Исходные auth headers, -cookies и query не передаются. H3 middleware выполняется в обоих каналах. -Response, redirect, streaming, изменение заголовков или статуса не поддерживаются; -возвращается JSON-объект. Обычные endpoints без обёртки сохраняют полный HTTP API. - -Каждая запись items и её история транзакционны. Весь произвольный handler одной -транзакцией не является. Для связанного сохранения есть items.commit. Ошибка после -завершённой записи (в том числе в выходной схеме) не отменяет её. После таймаута -нельзя автоматически повторять действие с записью: результат сначала проверяется. - -Вызов ограничен 10 секундами, результат — 48 КБ. Отмена прекращает ожидание, -запрещает последующие вызовы items и создание prepared-формы; уже начатую запись -она гарантированно не откатывает. Доверенный handler должен соблюдать signal. - -## Подготовленная форма и ответ ассистента - -Успешное действие с `page` создаёт приватный снимок входа и выхода на 20 минут: -до 32 на пользователя, 256 на установку. Снимки хранятся в PostgreSQL, доступны -всем репликам Core и сохраняются при перезапуске до истечения срока. Значений формы в URL нет. - -`GET /extensions/:namespace/drafts/:id` проверяет сессию, владельца, namespace -и доступ к действию. Чужой или истёкший снимок даёт одинаковый 404. При изменении -прав на данные снимок не перечитывается из коллекций: он является результатом уже -выполненного действия владельца, а не новой выдачей данных. - -Ассистент объясняет результат в сообщении и вызывает `present_plugin_result` с -ID успешного результата текущего обращения. Появляется кнопка **«Открыть страницу»**; -автоматической навигации нет. Возвращается `requiresUserClick: true`. Поддельный ID -не принимается. Наличие prepared-формы само по себе не доказывает изменение данных. - -`useAction` передаёт host dirty/busy, проверяет ID и выходную схему, защищает форму -при переходе из ассистента или замене снимка. Общая защита всех переходов через -меню пока не реализована. Частичные формы и чтение текущих ручных черновиков не -поддержаны. Детальные разрешения на отдельные действия и публичный MCP — позднее. - -## Проверка изменений - -- File routing и схемы совпадают в source и built. Нет параллельного реестра. -- HTTP/MCP используют один handler, H3 middleware и валидацию входа/выхода. -- Plain endpoints не видны MCP; superuser и отозванные права проверяются при вызове. -- Items защищает действия/поля, MCP-публикацию и связанные коллекции; readOnly блокирует запись. -- Форма использует генерируемую схему без серверных импортов. -- Сохраняются владелец/TTL снимка, защита dirty-формы и кнопка без автоперехода. -- Документация и calculator соответствуют текущему API. +useItems(event) is shared by ordinary endpoints and model handlers. It uses the current caller and enforces collection/action/field access, relationships, validation, and Core history. Actors cannot be replaced or elevated. HTTP/MCP share the handler and items service. Denial returns HTTP 403 or MCP PERMISSION_DENIED without inaccessible record contents. Every MCP call rechecks session and permissions. + +MCP additionally intersects collection publication: mcp.enabled=false blocks reading/writing, including filters, search, labels, and relation changes, even for superusers. HTTP grants are unaffected. ReadOnly:true blocks items create/update/delete/commit in both channels. + +MCP deletion also rejects cascades/setNull/setDefault that could touch disabled collections, including cascade chains. This conservative schema check does not inspect hidden rows and applies even to empty child tables. + +useActionContext(event) exposes actor, superuser, signal, and items. It does not expose full useAsmblyr context, privileged plugin storage, Knex, tokens, or cookies. Shared items enforces row rules. Query filters are not access policies; plugins cannot replace actors or expand grants. + +Packages are trusted server code, without a sandbox. ReadOnly restricts supplied items, not an external client imported by a plugin. MCP hints reflect readOnly; arbitrary code has openWorldHint=true. + + + +## Invocation and transactions + +- Core: POST /calculator/calculate. +- UI: POST /api/calculator/calculate directly in Core with a browser session. +- MCP: plugin_calculator\_\_calculate, JSON text and structured content. +- HTTP response: `{ data: { namespace, actionId, input, output } }`. + +Handlers receive a normalized POST with validated JSON, excluding original auth headers, cookies, and query. H3 middleware runs in both channels. Return a JSON object: Response, redirect, streaming, header/status mutation are unsupported. Unwrapped endpoints retain the full HTTP API. + +Each items write and its history are transactional; the arbitrary handler as a whole is not. Use items.commit for linked writes. Failure after a completed write, including output-schema failure, does not roll it back. Never automatically retry a timed-out write before checking its outcome. + +Calls are limited to ten seconds and results to 48 KB. Cancellation stops waiting and blocks later items calls/prepared-form creation but cannot guarantee rollback of an already started write. Trusted handlers must observe signal. + + + +## Prepared forms and assistant results + +A successful action with page creates a private input/output snapshot lasting twenty minutes: at most 32 per user and 256 per installation. PostgreSQL storage makes snapshots available across replicas and restarts until expiry. Form values never appear in URLs. + +GET /extensions/:namespace/drafts/:id checks session, owner, namespace, and action access. Foreign/expired snapshots both return 404. Data-permission changes do not reread a snapshot from collections: it is the owner's already completed action result, not a new data query. + +The assistant explains the result and calls present_plugin_result with a successful result ID from the current response. An Open page button appears; there is no automatic navigation. The result includes requiresUserClick:true. Forged IDs are rejected. A prepared form alone does not prove data changed. + +useAction reports dirty/busy state to the host, validates IDs/output schema, and protects the form during assistant navigation or snapshot replacement. A universal guard for all menu navigation is not yet implemented. Partial forms and reading current manual drafts are unsupported. Fine-grained action permissions and public MCP remain future work. + + + +## Change review + +- Source and built discovery agree on routes/schemas, without a parallel registry. +- HTTP/MCP share a handler, H3 middleware, and input/output validation. +- Plain endpoints stay hidden from MCP; calls recheck superuser and revoked access. +- Items enforces action/field access, MCP publication, related collections, and read-only behavior. +- Forms use generated schemas without server imports. +- Owner/TTL checks, dirty-form protection, and user-click navigation remain intact. +- Documentation and calculator match the current API. ```sh pnpm build:plugins @@ -199,9 +145,10 @@ pnpm build:examples node scripts/test.mjs core-plugins ``` -Runner создаёт и удаляет отдельную временную PostgreSQL. Интеграционные тесты -нельзя запускать напрямую на базе проекта. Изменения UI также проверяются в браузере. +The runner creates/removes disposable PostgreSQL databases. Never run integration tests directly on the project database. Verify UI changes in a browser too. + + -## Личные подключения +## Personal connections -Аннотация `connection: "google"` требует активного собственного подключения и одобренной capability `connections.google`. `useActionContext(event).connections.google` даёт ограниченный broker без credentials. `proposeWrite` подготавливает owner-bound предложение; подтверждение реализовано в Core и отсутствует в MCP. Google-плагин остается H3 file routes со схемами из типов; отдельной регистрации инструментов нет. См. [Google Workspace](../features/google-workspace.md). +Annotation connection:"google" requires an active personal connection and approved connections.google capability. useActionContext(event).connections.google exposes a restricted broker without credentials. `proposeWrite` prepares an owner-bound proposal; Core owns confirmation and never exposes it in MCP. The Google plugin retains H3 file routes and type-generated schemas, without separate tool registration. See [Google Workspace](../features/google-workspace.md). diff --git a/docs/features/access.md b/docs/features/access.md index 0ff5eb5..abc5f35 100644 --- a/docs/features/access.md +++ b/docs/features/access.md @@ -1,44 +1,44 @@ -# Права и политики + -## Модель +# Permissions and policies -Permission — самостоятельная настройка доступа. Policy объединяет любое количество permissions; одно permission можно связать с несколькими policies. Пользователь получает несколько policies, их разрешения объединяются. Отдельных deny-правил нет. + -У permission одна цель: коллекция **либо** раздел настроек. Для коллекции задаются action `create/read/update/delete`, поля и необязательный `rowFilter`. Без условия разрешение действует для всех записей. Для раздела — `section`, `action: read | update`, `fields: ["*"]`. `update` включает просмотр; `read` не разрешает изменения. +## Model -Прямое изменение общего permission влияет на все использующие его политики. Редактор конфигурации политики сохраняет её собственный набор атомарно, не меняя чужую политику через случайное редактирование общего экземпляра. +A permission is an independent access rule. A policy groups any number of permissions, and one permission may belong to several policies. A user can receive several policies; their grants are combined. There are no deny rules. -## Проверка +Each permission targets either a collection or a settings section. Collection grants specify `create/read/update/delete`, fields, and an optional `rowFilter`. Without a condition, they apply to every row. Settings grants specify `section`, `action: read | update`, and `fields: ["*"]`. Update includes viewing; read does not allow changes. -Core загружает активного principal и актуальные grants из БД. Отзыв политики не требует перевыпуска пользовательского токена. Действие, поля и связанные записи проверяются в сервисах, которыми пользуются HTTP, Kit и внутренний MCP. +Editing a shared permission directly affects all policies using it. The policy configuration editor saves its own set atomically without accidentally modifying another policy through a shared permission. -Первичный ключ остаётся идентификатором читаемой записи. Запись без read может успешно вернуть `data: null`. System routes не являются обычными items: для них собственные проверки допуска. + -## Приложения в политиках +## Enforcement -Приложение с включённым управлением через политики появляется во вкладке -«Приложения» редактора политики. «Разрешить вход» допускает пользователя к SSO; -галочки ниже назначают права внешнего сервиса из его настроенного каталога. -Несколько политик объединяются, deny-правил нет. Отсутствие назначения запрещает -вход даже superuser; superuser Collaborative не означает администратора внешнего сервиса. +Core loads the active principal and current database grants. Revoking a policy does not require replacing a user's token. Services shared by HTTP, Kit, and internal MCP enforce actions, fields, and related-record access. -`GET /policies/applications` возвращает каталог для редактора и требует человеческого -доступа `policies/read` или `update`. Полная конфигурация `POST /policies` и -`PATCH /policies/:id` принимает необязательное поле -`applications: [{ appId, scopes }]`. Пустой `scopes` разрешает только вход, -пустой `applications` снимает все назначения приложений этой политики. -Пропуск `applications` при обновлении сохраняет назначения. Права приложения, -права данных и пользователи сохраняются одной транзакцией. `GET /policies/:id` -возвращает этот же список. Неизвестные приложения и разрешения отклоняются. +The primary key remains the identifier of a readable record. A successful write without read permission can return `data: null`. System routes have their own access checks and are not ordinary items. -Состав меняет только человек-superuser. Ограниченное назначение готовых политик -действует и для их прав на приложения: менеджеру нужен явный разрешённый набор, -он не меняет себя или superuser. OAuth-права не дают доступ к Core API. -Подробности токенов и сроков отзыва — в [интеграциях](./integrations.md#доступ-через-политики). + -## Условия записей +## Applications in policies -В редакторе политики нажмите действие коллекции: «Нет доступа», «Все записи» или «По условию». Условие состоит из поля, оператора и значения. Поиск поля учитывает его название и техническое имя; оператор зависит от типа. Значением может быть литерал или совместимый параметр из поиска. Параметры и их описание раскрываются по мере необходимости. Группы поддерживают И/ИЛИ; выбор полей свёрнут отдельно. «Применить» возвращает правило в черновик; доступ изменяется после общего сохранения политики. +An application with policy-managed access appears in the policy editor's Applications tab. Allow sign-in admits the user to SSO; the checkboxes below assign external-service permissions from its configured catalog. Grants from multiple policies combine without deny rules. No assignment means no sign-in, including for a Collaborative superuser: that role does not imply external-service administration. + +`GET /policies/applications` supplies the editor catalog and requires a human with policies/read or update. Full configuration through `POST /policies` and `PATCH /policies/:id` accepts optional `applications: [{ appId, scopes }]`. Empty `scopes` allows sign-in only; empty `applications` clears the policy's application assignments. Omitting applications on update preserves them. + +Application grants, data grants, and users save in one transaction. `GET /policies/:id` returns the same list. Unknown applications and permissions are rejected. + +Only a human superuser may edit a policy's contents. Delegating an existing policy also delegates its application rights: managers need an explicit allowed set and cannot change themselves or superusers. OAuth grants do not grant Core API access. See [integrations](./integrations.md#policy-managed-access) for token and revocation behavior. + + + +## Row conditions + +Click a collection action in the policy editor and select No access, All records, or Conditional. Each condition has a field, operator, and value. Field search matches labels and technical names; operators depend on type. Values can be literals or compatible context parameters with discoverable descriptions. Groups support AND/OR. Field selection is a separate collapsed section. + +Apply updates the draft; access changes only after saving the whole policy. ```json { @@ -63,50 +63,58 @@ Core загружает активного principal и актуальные gra } ``` -`rowFilter: null` явно снимает условие. При PATCH permission отсутствие `rowFilter` сохраняет старое условие. Настройки `section` условий не поддерживают. Общий permission разделяется между политиками; редактор конфигурации учитывает условие при выборе экземпляра и сохраняет независимость других политик. +`rowFilter: null` explicitly removes a condition. Omitting it when patching a permission preserves the old condition. Settings sections do not support conditions. The configuration editor considers the condition when selecting a shared permission, keeping other policies independent. + +Core/Contracts owns the parameter catalog: `user.id`, `user.email`, `service.id`, `actor.id`, and `request.now`. Values come from the authenticated principal and server time; clients cannot supply context. Incompatible types are rejected on save. A missing principal parameter invalidates the **entire rule**, including OR branches. Corrupt or outdated conditions also fail closed. + +Conditions support direct scalar fields, including ID, M:1 foreign keys, and timestamps, with the ordinary operators for each type. `exists/notExists`, dotted paths, JSON, repeaters, and file arrays are unsupported. Groups must be nonempty: maximum depth 3, 20 conditions/30 nodes, and 8192 JSON characters. Literals are strings; value lists are arrays of strings. + +Read rules restrict SQL before pagination and counting. Fields are projected per row using the matching rules: “title for everyone” plus “body for my records” never reveals someone else's body. Filters, sorting, and aggregates on conditionally readable fields apply only to rows where that field is readable. Search checks each field separately. Labels, relationship lists/candidates, files, Kit, and MCP share these boundaries. + +Create checks the resulting record with server defaults. Only user-supplied fields require field grants; defaults need no separate grant. Update checks both the original and resulting row against the same matching branches for changed fields. Leaving the allowed scope, such as changing the author under an “edit my records” rule, rolls back the transaction. Bulk writes and nested drafts also roll back entirely on denial. Delete checks the original row. Write responses return readable fields only, or `data: null`. -Каталог параметров принадлежит Core/Contracts: `user.id`, `user.email`, `service.id`, `actor.id`, `request.now`. Значения берутся из аутентифицированного principal и времени сервера; клиент не может передать контекст. Несовместимый тип отклоняется при сохранении. Если параметр отсутствует для участника запроса, недействительно **всё правило**, в том числе его ветки ИЛИ. Повреждённое или устаревшее условие также не открывает доступ. + -Поддерживаются прямые скалярные поля, включая ID, внешний ключ M:1 и timestamps. Операторы совпадают с обычным фильтром по этому типу; `exists/notExists`, поля через точку, JSON, repeaters и массивы файлов в этой версии не поддерживаются. Группы непустые, глубина до 3, до 20 условий/30 узлов, JSON до 8192 символов. Литералы — строки; списки значений — массивы строк. +### Initial limitations -Чтение ограничивает SQL до пагинации и count. Поля каждой записи проецируются по сработавшим правилам: разрешение «title всем» вместе с «body своим» не открывает чужой body. Фильтр, сортировка и агрегаты по условно доступному полю работают только по строкам, в которых разрешено это поле. Поиск проверяет каждое искомое поле отдельно. Те же границы применяются к labels, связанным спискам/кандидатам, файлам, Kit и MCP. +- Related search and filters across relationships are disabled when the source, target, or junction collection has conditional read rules. Direct foreign keys and relationship lists still work with row checks. This prevents EXISTS-based leaks until path-level conditions are supported. +- History stores diffs, not full historical snapshots. Conditional grants therefore do not expose history: only fields from unconditional read grants appear. Without those grants, history is empty, including for deleted records. +- Superusers bypass row conditions. Trusted plugin `storage.own` remains a separate restricted capability; ordinary `items` checks the caller's access. -Создание проверяет итоговую запись с серверными defaults. Проверяются только поля, переданные пользователем; defaults не требуют самостоятельного field grant. Изменение проверяет исходную и итоговую запись по тем же сработавшим веткам для изменяемых полей. Выход из области разрешения, например смена автора при праве «редактировать свои», откатывает всю транзакцию. Bulk и вложенный draft также откатываются целиком при отказе. Удаление проверяет исходную запись. Результат записи возвращает только читаемые поля либо `data: null`. + -### Границы первой версии +## Administrative privileges -- Related search и фильтры по полям через связь отключены, если исходная, целевая или промежуточная коллекция использует условное чтение. Прямой внешний ключ и списки отношений продолжают работать с проверкой строк. Это предотвращает утечку через EXISTS до поддержки условий на пути связи. -- История хранит diff, а не полное состояние каждого старого объекта. Поэтому условные grants не открывают историю: видны только поля из безусловных read grants. При отсутствии таких grants история пуста, включая удалённые записи. Поддержка истории по условиям требует полных исторических снимков. -- Superuser обходит условия. Доверенный `storage.own` остаётся отдельной ограниченной capability плагина; обычный `items` проверяет права вызывающего. +- Only a human superuser can change policy contents or permissions, or create/delete policies. +- `policies/update` allows assignment and removal of existing policies from the manager's explicit allowed set. Managers cannot change their own or superusers' assignments. +- `services/update` allows service management only when all current and requested policies are in that set. Keys and federations enforce the same boundary. +- `users/update` allows invitations and viewing access; it does not itself allow changing policies. +- `superuser` bypasses ordinary data/settings grants, not validation or structural safeguards. -## Сильные административные права +Settings grants do not automatically provide collection reads or DDL. Service tokens cannot use settings, even if their policy contains section grants. -- Состав политик, permissions, создание и удаление политик меняет только человек-superuser. -- `policies/update` позволяет назначать и снимать готовые политики из явного разрешённого набора пользователя. Менеджер не меняет собственные назначения и назначения superuser. -- `services/update` позволяет управлять сервисами, если все их текущие и запрошенные политики входят в разрешённый набор. Ключи и федерации проверяют ту же границу. -- `users/update` позволяет приглашать пользователей и видеть их доступ; само по себе не разрешает изменение политик. -- `superuser` обходит обычные data/settings grants, но не проверки корректности и защиты структуры. + -Права настроек не дают чтение записей коллекций и DDL автоматически. Сервисные токены не могут использовать настройки, даже если связанная политика содержит section grants. +## Unsupported behavior -## Пока отсутствует +There are no deny rules. Table filters, workspaces, hidden collections, and MCP enablement are distinct features and do not replace permissions. -Запрет через deny. Фильтр таблицы, workspace, скрытая коллекция и MCP enable — разные механизмы; они не заменяют permissions. + -## Ограниченное назначение политик +## Bounded policy delegation -Администратор открывает пользователя → панель доступа → «Разрешено назначать», выбирает готовые политики и сохраняет набор. Это отдельная настройка конкретного пользователя, не permission внутри политики. Назначение другой политики не переносит чужое право делегирования. По умолчанию набор пуст; собственные права пользователя не являются разрешением выдавать их другим. +An administrator opens a user's access panel, selects existing policies under Allowed to assign, and saves the set. This is a per-user setting, not a permission inside a policy. Assigning a policy does not inherit another user's delegation rights. The set is empty by default; having a policy yourself does not allow you to give it to others. -Нужны оба допуска: `policies/update` для назначений людям либо `services/update` для сервисов, и ID политики в разрешённом наборе. `read` разрешает просмотр. Назначение самому себе, изменение состава политики и редактирование разрешённого набора доступны только superuser. +Both checks must pass: policies/update for human assignments or services/update for services, and the policy ID in the allowed set. Read grants allow viewing. Self-assignment, policy-content changes, and editing allowed sets require a superuser. -`PUT /users/:id/delegation` принимает только `{ policyIds }` и доступен superuser. `PUT /policies/:id/users` принимает только `{ userIds }` и атомарно заменяет назначения одной готовой политики. Индивидуальные PUT/DELETE назначений проверяют те же ограничения. Чужие политики целевого пользователя не заменяются. Существующие собственные назначения и назначения superuser в bulk-запросе можно сохранить без изменения. +`PUT /users/:id/delegation` accepts only `{ policyIds }` and requires a superuser. `PUT /policies/:id/users` accepts only `{ userIds }` and atomically replaces assignments of one existing policy. Individual PUT/DELETE assignments enforce the same restrictions without replacing the target user's other policies. Bulk requests may preserve existing self/superuser assignments unchanged. -Сервис с хотя бы одной политикой вне набора доступен менеджеру только для просмотра. Нельзя выпустить или отозвать ключ, изменить федерацию, отключить аккаунт или удалить его закрытые политики через PUT. Сервис без политик не имеет прав на данные; менеджер может настроить его в пределах своего набора. Повторное приглашение не выдаётся менеджеру для аккаунта с закрытыми политиками либо с собственным правом делегирования. +A service with any policy outside the manager's set is read-only to that manager. They cannot issue/revoke keys, change federation, disable the account, or remove protected policies through PUT. A service without policies has no data access and may be configured within the manager's allowed set. Managers cannot reissue invitations for accounts with protected policies or their own delegation rights. -Список `GET /users` содержит `hasDelegation`: признак непустого личного разрешённого набора. UI скрывает для менеджера кнопку повторного приглашения у таких пользователей, собственного аккаунта и аккаунтов с политиками вне набора. Сам набор редактируется только в панели доступа администратором; сервер перепроверяет ограничения независимо от кнопок. +`GET /users` includes `hasDelegation`, indicating a nonempty personal allowed set. The UI hides re-invitation for those accounts, the manager's own account, and accounts with policies outside the set. Only administrators edit the set in the access panel; the server independently rechecks every restriction. -Проверки читают актуальное состояние из БД; отзыв набора действует со следующим запросом без смены токена. Разрешение делегировать не даёт права читать данные по выбранным политикам. Сервисные токены не управляют политиками или делегированием. +Checks use current database state, so revocation applies on the next request without token replacement. Delegation does not grant data access through the selected policies. Service tokens cannot manage policies or delegation. -Миграция `20261003040000_policy_delegations.cjs` создаёт пустой набор для существующих менеджеров; прежние grants и назначения сохраняются. Непривилегированные менеджеры теряют неограниченное изменение политик: администратор должен явно настроить разрешённый набор. Down отказывается удалять непустые настройки делегирования. +Migration `20261003040000_policy_delegations.cjs` creates empty sets for existing managers and preserves their grants and assignments. Non-superuser managers lose unrestricted policy editing; administrators must explicitly configure their allowed set. Down refuses to remove nonempty delegation settings. -Отзыв набора ограничивает будущие административные операции; уже выданные ключи и назначенные получателям политики автоматически не отзываются. Их нужно отзывать отдельно. Изменение администратором состава разрешённой политики меняет её права у всех получателей. +Revocation restricts future administrative actions. Existing keys and assigned policies are not automatically revoked and must be withdrawn separately. Administrator changes to an allowed policy affect all its recipients. diff --git a/docs/features/assistant.md b/docs/features/assistant.md index 6b0163a..d73559f 100644 --- a/docs/features/assistant.md +++ b/docs/features/assistant.md @@ -1,267 +1,117 @@ -# Ассистент и внутренний MCP - -Путь запроса, инструментов и модельных обработчиков показан в -[архитектуре ассистента](../development/assistant-architecture.md). - -Ассистент опционален: Core подключается к OpenAI-совместимому провайдеру, ключ хранится на сервере. UI передаёт контекст текущей страницы; ответы передаются потоком. Переход на предложенную страницу выполняет пользователь кнопкой. - -Инструменты могут исследовать доступную схему, искать/читать данные, формировать фильтры и выборки, подготавливать формы и вызывать явно объявленные model-handlers расширений. Названия для человека берутся из display metadata; технические имена используются для API. - -Для обычного поиска ассистенту задан короткий сценарий: нужные записи и кнопка -полной выборки. Подсчёты, рейтинги и дополнительные справочники запрашиваются -для соответствующих вопросов; подробный анализ можно продолжить следующим -сообщением. Это инструкция модели, а не гарантия длительности ответа провайдера. - -## Границы - -Чат требует человека: superuser, хотя бы один data grant read/create/update либо активное личное Google-подключение. Право `assistant` позволяет управлять настройками, но само не даёт права чата/данных. - -Внутренний MCP — общий контракт инструментов для ассистента. Публичного внешнего MCP endpoint пока нет. Каждая операция выполняется с правами пользователя, дополнительно учитывая включение коллекции в MCP, в том числе для superuser. Нельзя выдавать модели сырой SQL или привилегированный storage. - -Для плагина разработчик оборачивает H3 handler в `defineModelContext` и задаёт аннотацию. HTTP и MCP используют тот же handler и проверку. Входной AccessGate не заменяет права на затронутые данные. `readOnly` должен запрещать запись в доступных инструментах. - -## Настройки и журнал - -В разделе настроек доступны инструкции, модель и телеметрия: пользователь, время, запрошенная/фактическая модель, вызовы инструментов, входные/выходные токены, длительность и результат. Если провайдер не вернул usage, неизвестные значения не превращаются в ноль. - -Сводка ответа содержит `toolTrace`: до 32 вызовов с порядковым номером, -именем объявленного инструмента, длительностью, результатом и фиксированным -кодом ошибки. Аргументы, содержимое результатов и тексты исключений в эту -диагностику не попадают. Старые ответы могут не содержать `toolTrace`. -Счётчики токенов суммируются по обращениям к модели внутри одного ответа; -это не размер одного контекста. - -Общий таймаут одного ответа, включая сжатие истории и продолжения с -инструментами, по умолчанию составляет 180 секунд. `OPENAI_API_TIMEOUT_MS` -задаёт значение от 1000 до 300000 мс; явное значение сохраняется. Число -обращений к модели остаётся ограниченным восемью, отмена пользователем -прерывает текущую генерацию и инструменты. -Прокси админки допускает до 310 секунд, чтобы не оборвать поток раньше -максимального таймаута Core. - -Ошибки инструментов возвращают безопасные `code` и `hint`, например -`SCHEMA_REQUIRED` для чтения без описания коллекции и `INVALID_FILTER` для -неверного фильтра. Идентичный неудачный вызов внутри ответа блокируется -без повторного выполнения; модель переходит к итоговому сообщению. -Повтор с исправленными аргументами допускается, пока остаются шаги. -Для фильтров `reason` различает неверную структуру JSON, путь, оператор, -тип значения, квантор и превышение ограничений; `hint` объясняет исправление -без раскрытия отклонённых значений. Фильтр передаётся JSON-строкой группы -`logic`/`children`. Пустая строка очищает условия запроса, настоящий JSON -`null` наследует контекст; строка `"null"` отклоняется. Поиск другого клиента -должен использовать его условие вместо условия открытой таблицы. Это -инструкция модели; Core по-прежнему проверяет все права и фильтры. -Смысловые условия вроде «активные» должны сохраняться в поиске, подсчёте -и карточке через настроенный термин или явно заданный критерий. Если термин -не определяет однозначный смысл, ассистент должен уточнить критерий. -В аргументе поиска `q` строка `"null"` отклоняется как ошибочное обозначение -пустого значения. Для наследования условий нужен JSON `null`, для очистки — -пустая строка; поиск буквального слова возможен через `q="NULL"` или значение -текстового фильтра. Это ограничение внутренних инструментов, HTTP/SDK-поиск -не меняется. Унаследованный поиск из таблицы сохраняется целиком. -Режим `order` (`relevance`/`field`) наследуется вместе с поиском и сохраняется -в подборке: при её открытии таблица повторяет проверенный порядок результатов. -Явная сортировка по полю переключает обычный порядок, `order=relevance` -использует это поле только для равных совпадений. - -Если завершённый ответ сопровождается только `present_selection` или -`present_plugin_result`, Core проверяет и создаёт карточки, затем показывает -этот ответ без ещё одного обращения к модели. Ошибка карточки, пустой текст -или дополнительные инструменты требуют обычного продолжения. Это не -изменяет правила подтверждения записей или навигации пользователем. - -`describe_collection` описывает M2O, O2M и M2M. Для доступных связей ко многим -`relation.relatedRead` указывает целевую коллекцию, поле фильтра, оператор, -квантор и исходный ключ. Ассистент читает связанные записи отдельным запросом; -поля alias нельзя включать в `fields`. Подсказки учитывают права чтения полей, -включение коллекций в MCP и ограничения фильтрации связей при row rules. - -Системный справочник терминов связывает пользовательские слова со смыслом данных; конкретные привязки к коллекции пока настраивает superuser. - -## Сессии и история - -Ход работы отображается отдельно от ответа: время выполнения, небольшие серые -статусы действий и публичные пояснения ассистента. После завершения этот блок -сворачивается; внутри остаются шаги и статистика. Итоговый ответ содержит только -финальное сообщение модели. Таблицы, карточки выборок и важные оговорки остаются -в результате. Внутренние рассуждения, аргументы инструментов и их сырые результаты -в блок хода работы не передаются. - -У текстового ответа появляется компактная кнопка копирования, когда генерация -закончилась или была остановлена. Она копирует исходный текст ответа с Markdown, -включая таблицы и код; ход работы, статистика и отдельные карточки действий в буфер -не добавляются. После копирования появляется подтверждение. Если браузер не дал -доступ к буферу обмена, интерфейс предлагает выделить и скопировать текст вручную. -Иконки в шапке снабжены подсказками; новая сессия начинает отдельный разговор, -сохраняя переписку и черновик предыдущей сессии. - -В узкой панели широкие таблицы прокручиваются по горизонтали внутри ответа; -числа и идентификаторы не разбиваются для подгонки колонок. Область таблицы -доступна с клавиатуры. Блоки кода также имеют отдельную горизонтальную прокрутку. -При уменьшении высоты панели поле ввода и отступы становятся компактнее, -сохраняя место для переписки; длинный черновик прокручивается внутри поля. - -API возвращает отдельный `activity`: до 64 записей `status`/`note`, до 8000 -символов в записи и 64 000 символов в сумме. Новые сообщения сохраняют его -отдельно от `content`; при восстановлении истории блок доступен для раскрытия. -Перед продолжением и сжатием диалога используются только завершённые сообщения, -без `activity`. Старые сохранённые ответы остаются неизменными. - -NDJSON дополнен событием `activity`. `text-delta` с `provisional=true` относится -к текущему незавершённому шагу и показывается в ходе работы; такой текст ещё -не является итоговым ответом. При завершении модельного шага Core либо сохраняет -его как публичное пояснение, либо передаёт финальный текст с `reset=true`. -`reset` заменяет текущий текст ответа. Остановка сохраняет полученные публичные -пояснения отдельно, а уже подтверждённую часть итогового ответа — в `content`. - -Пустой диалог показывает приветственную карточку с двумя действиями: поиск и -объяснение структуры, а на странице коллекции — фильтр и объяснение полей. -При отключённом контексте предлагаются общие вопросы о проектировании данных; -инструменты базы и Google остаются доступны, если отдельно включён доступ к данным. -Нажатие подставляет редактируемый вопрос в поле ввода; отправка остаётся за -пользователем. Карточка также открывает личные подключения Google Drive и Sheets -и исчезает после начала переписки. - -В карточке сохранённой записи доступна кнопка «Спросить об этой записи». -Ассистент открывается внутри редактора, включая вложенные карточки и импортированные -materialized view. Сворачивание ассистента не закрывает карточку и сохраняет текст -ввода. Контекст показывает коллекцию и ID записи; после закрытия карточки снова -используется контекст страницы. - -Над вводом показан контекст следующего вопроса. Если он отличается от последнего -отправленного вопроса, появляется компактная подсказка о смене истории для ответа. -Переходы между контекстами отмечены разделителями в ленте и сохраняются при открытии -истории сессии. Значок справки объясняет изоляцию истории, её сжатие и режим без -контекста страницы. Поиск и фильтр внутри той же таблицы не создают отдельную историю. -Переход во время генерации не меняет уже отправленный запрос: подсказка показывает -его прежний контекст, а новое место применяется к следующему вопросу. - -В снимок передаётся только идентификатор сохранённой записи. Core проверяет -доступность коллекции в MCP и право чтения конкретной строки до обращения к модели. -Значения полей ассистент запрашивает обычными инструментами с актуальными правами -пользователя. Несохранённые значения формы и новые записи в черновике не передаются. -Условия открытой таблицы не переносятся в контекст записи, кнопка применения фильтра -таблицы в карточке недоступна. - -API-контекст записи: `{ page: "items", workspaceId: null, collection: "articles", record: { id: "1" } }`. -`record` принимает только строковый `id` до 255 символов; Core дополнительно проверяет -тип первичного ключа. `record` и `table` взаимно исключают друг друга. Для выключенной -в MCP коллекции снимок не содержит запись, её данные и структуру прочитать нельзя. -Существующий контекст таблицы остаётся совместимым. - -Контекст страницы и доступ к данным управляются отдельно. Переключатель над вводом -включает только сведения о текущей странице, коллекции, записи и условиях таблицы. -В окне «Подключения ассистента» доступен переключатель «Доступ к данным и -подключениям»: он разрешает инструменты базы, личного Google и расширений для -следующих вопросов. Выключение не удаляет подключённые аккаунты и не останавливает -уже отправленный запрос. Проверки прав пользователя, публикации коллекций в MCP -и подтверждения записи в Google сохраняются. - -Новый необязательный параметр `/assistant/messages`: -`dataAccess: { enabled: true, workspaceId: null }`. С `context: null` инструменты -работают без сведений о текущей странице: модель должна явно найти/указать -коллекцию, условия поиска и фильтра не наследуются, предложение фильтра открытой -таблицы недоступно. `enabled: false` отключает все инструменты, независимо от -контекста. При переданном контексте `workspaceId` должен совпадать с ним; это -привязка разговора, а не дополнительные права и не ограничитель каталога коллекций. -Без `dataAccess` прежние клиенты сохраняют старое поведение: отсутствие контекста -отключает инструменты. Некорректный параметр отклоняется, а не приводит к включению. - -Рабочая история и резюме разделены по режиму доступа и пространству даже без -контекста страницы. Выключение доступа не скрывает уже видимую переписку, но для -нового ответа не используются сообщения и резюме режима с доступом к данным. -Возврат к прежнему режиму продолжает его историю. Повтор вопроса сохраняет снимок -страницы исходного запроса. Доступ при повторе не расширяется: если пользователь -его выключил после сбоя, инструменты остаются отключёнными и при повторе. - -Переписка виджета хранится в PostgreSQL и принадлежит текущему пользователю. -Кнопка «История сессий» открывает предыдущие диалоги; после перезагрузки -восстанавливается последняя сессия. «Новая сессия» создаёт отдельный пустой -контекст, сохраняя прежнюю переписку в истории. Есть удаление сессии и загрузка -более ранних сообщений. История других пользователей недоступна, в том числе -superuser. - -Неотправленный текст хранится отдельно для каждой сессии в памяти открытой -админки. При создании нового разговора поле ввода пустое, а черновик прежней -сессии остаётся доступен через историю с отметкой «Черновик» и короткую подсказку -«Вернуться к черновику» после создания нового разговора. Возврат к сессии -восстанавливает её ввод; загрузка более ранних сообщений его не меняет. -Ошибка создания или открытия сессии не очищает текущий текст. Текст, введённый -до появления первой сессии, привязывается к ней. Удаление сессии удаляет её -черновик после успешного ответа сервера. - -Черновики не отправляются в API или модели и не сохраняются в браузерное -хранилище. Сворачивание ассистента их сохраняет, перезагрузка страницы или -выход из открытой админки — очищает. Сохранение переписки в PostgreSQL -не включает неотправленный ввод. - -Хранятся тексты сообщений, публичный ход работы, привязка к контексту страницы, результат -завершения/остановки и метрики ответа. Исходный контекст страницы, результаты -инструментов, служебные токены подключений и внутренние рассуждения модели в -историю не копируются. Текст переписки сохраняется таким, каким его отправили -пользователь и ассистент. -Интерактивные карточки фильтров, форм и подтверждений записи доступны в текущем -ответе; при восстановлении истории они не активируются повторно. Для нового -действия нужно снова обратиться к ассистенту. - -Перед обращением к модели Core собирает завершённые пары сообщений текущего -контекста. При превышении 24 000 символов, 25 сообщений или 50 000 байт -сериализованной истории старая часть сжимается той же моделью без инструментов. -Резюме ограничено 4000 символами; последние сообщения и текущий вопрос -сохраняются в рабочем контексте. Полная переписка при этом остаётся в базе. -Резюме ведётся отдельно для контекста каждой страницы и каждой записи и не переносится в новую -сессию. Видимая переписка остаётся общей, но модель получает завершённые сообщения -только текущего контекста. Его содержимое является данными разговора и не даёт разрешений на действия. -Запросы сжатия входят в телеметрию и общий лимит восьми обращений к модели. - -POST /assistant/messages поддерживает сохранённый режим с conversationId, -messageId, content и необязательными settings/context. Повтор завершённого -запроса с тем же messageId возвращает сохранённый текст без повторного обращения -к модели. Изменение тела с тем же ID запрещено. Прежний формат messages остаётся -совместимым и не сохраняет диалог. API сессий: GET/POST /assistant/conversations, -GET/DELETE /assistant/conversations/:id; пагинация передаёт before. - -Одновременно в сессии выполняется один запрос. При отключении клиента -генерация останавливается; полученный к этому моменту текст сохраняется при -обработке остановки. После аварийного завершения процесса незавершённый ответ -отмечается прерванным по истечении одиннадцати минут. Сжатие и ответ делят -общий настроенный таймаут запроса. Генерации в фоне и возобновления -потока после перезагрузки нет. Удаление пользователя удаляет его личные сессии; -HISTORY_RETENTION_DAYS также очищает неактивные сессии по времени последнего -сообщения, если срок хранения явно включён. - -Лимиты общие для реплик через PostgreSQL: по умолчанию 100 запросов на пользователя в сутки UTC и два одновременных запроса. Настраиваются `ASSISTANT_DAILY_REQUESTS` и `ASSISTANT_CONCURRENT_REQUESTS`. В одном запросе максимум восемь обращений к модели, 100 000 байт сериализованного контекста перед каждым обращением и настроенный предел выходных токенов. Последнее доступное обращение резервируется для итогового ответа без инструментов, учитывая уже выполненное сжатие истории. При нехватке шагов модель должна объяснить, какие сведения ещё не получены. Если провайдер всё же возвращает вызов инструмента на итоговом шаге, Core его не выполняет. Это ограничение количества/объёма, не денежный бюджет провайдера. История сохраняется по умолчанию; `HISTORY_RETENTION_DAYS` явно включает очистку по возрасту. - -При подтверждённом отключении ассистента его закрытый виджет скрыт. Если панель -уже открыта, она сохраняет диалог и ввод и сообщает об отключении. Сетевая ошибка, -таймаут или некорректный ответ проверки статуса не скрывают ассистента: в панели -появляется сообщение и кнопка «Повторить». Повтор проверяет подключение и не -отправляет сообщение модели. До успешной проверки отправка новых вопросов -недоступна, но текст можно редактировать. Последняя известная конфигурация и -пользовательские параметры сохраняются при техническом сбое. - -401 и 403 показывают отдельные сообщения об истёкшей сессии или отсутствии прав. -При первоначальном подтверждённом отсутствии доступа закрытый виджет не -отображается. Ручная проверка и события фокуса/смены настроек отменяют предыдущую -проверку; устаревший ответ не заменяет новый. Проверка ограничена восемью секундами. - -Реальные обсуждения записей остаются -в плагине комментариев, а [уведомления](./notifications.md) доступны отдельным -колокольчиком в шапке админки и работают независимо от AI. - -Данные, попавшие в контекст/результат инструмента, отправляются настроенному провайдеру. Выбор провайдера, объём передачи и договорные требования — эксплуатационное решение. Текст записи и результат плагина могут содержать prompt injection: модельный ответ не считается границей авторизации. + + +# Assistant and internal MCP + +See [assistant architecture](../development/assistant-architecture.md) for the request, tool, and model-handler pipeline. + +The assistant is optional. Core connects to an OpenAI-compatible provider and keeps the key on the server. The UI sends current-page context and streams responses. Users choose whether to navigate to suggested pages. + +Tools inspect accessible schema, search/read data, build filters and selections, prepare forms, and call explicitly declared plugin model handlers. Human labels come from display metadata; API calls use technical names. + +Ordinary searches are instructed to return relevant records and a full-selection button. Counts, rankings, and extra dictionaries are requested when the question needs them; deeper analysis can follow in another message. This is a model instruction, not a provider-latency guarantee. + + + +## Boundaries + +Chat requires a human who is a superuser, has at least one read/create/update data grant, or has an active personal Google connection. The assistant settings permission grants configuration access, not chat or data access. + +Internal MCP is the assistant's shared tool contract. There is no public external MCP endpoint yet. Every operation uses the user's permissions and collection MCP enablement, including for superusers. Never expose raw SQL or privileged storage to the model. + +Plugin developers wrap H3 handlers in `defineModelContext` with an annotation. HTTP and MCP use the same handler and checks. Entry AccessGate does not replace permissions on affected data. Read-only mode must prevent writes in available tools. + + + +## Settings and telemetry + +Settings include instructions, model, and telemetry: user, time, requested/actual model, tool calls, input/output tokens, duration, and outcome. Missing provider usage remains unknown rather than becoming zero. + +Response summaries include `toolTrace`: up to 32 calls with sequence, declared tool name, duration, result, and fixed error code. Arguments, result contents, and exception text are excluded. Older responses may lack this field. Token counts sum model calls within a response; they are not the size of one context. + +The default total response timeout is 180 seconds, including history compaction and tool continuations. `OPENAI_API_TIMEOUT_MS` accepts 1000–300000 ms and preserves explicit values. Each response allows at most eight model calls. User cancellation interrupts generation and tools. The public proxy permits 310 seconds so it does not cut off Core's maximum timeout. + +Tool errors provide safe `code` and `hint`, such as SCHEMA_REQUIRED before describing a collection or INVALID_FILTER for malformed filters. An identical failed call is blocked from executing again within the response, prompting a final answer. Corrected arguments can be retried while steps remain. + +Filter error `reason` distinguishes JSON structure, path, operator, value type, quantifier, and limit errors. Hints explain corrections without revealing rejected values. Filters are JSON strings with logic/children groups. Empty string clears conditions; actual JSON null inherits context; the string "null" is invalid. + +Searching for another client should replace the open table's client condition. This is a model instruction; Core still enforces all filters and permissions. Meaningful conditions such as “active” must persist in search, counts, and cards through a configured term or explicit criterion. Ambiguous terms require clarification. + +Tool search `q="null"` is rejected as a mistaken empty value. Use JSON null to inherit or an empty string to clear. Literal text can be searched with `q="NULL"` or a text-filter value. This restriction is internal to assistant tools; HTTP/SDK search is unchanged. Inherited table search is preserved in full. + +Search `order` (relevance/field) is inherited and retained in selection cards, so opening them reproduces the validated order. Explicit field sorting selects field order; relevance order uses the field only for ties. + +When a completed response contains only present_selection or present_plugin_result calls, Core validates/creates cards and displays its text without another model call. Invalid cards, empty text, or additional tools require normal continuation. Write confirmation and user-controlled navigation are unchanged. + +describe_collection describes M2O, O2M, and M2M. For accessible to-many relations, relation.relatedRead gives the target collection, filter field, operator, quantifier, and source key. Related records are read separately; alias fields cannot appear in fields. Hints respect field reads, MCP enablement, and relationship-filter limitations under row rules. + +The shared glossary maps user terminology to data meaning. Collection-specific mappings currently require a superuser. + + + +## Sessions and history + +Work progress is separate from the answer: elapsed time, compact gray action statuses, and public assistant notes. After completion it collapses, retaining steps and statistics. The final answer contains only the model's final message, including tables, selection cards, and important caveats. Internal reasoning, tool arguments, and raw results are not exposed in progress. + +A compact Copy button appears after completion or stopping. It copies original Markdown, including tables and code, excluding progress, metrics, and separate action cards. Success is acknowledged; denied clipboard access prompts manual selection/copying. Header icons have tooltips. New session starts a separate conversation while preserving the previous conversation and draft. + +Wide tables scroll horizontally inside narrow responses without splitting numbers or identifiers. The table area is keyboard accessible, and code blocks scroll independently. Short panels use a compact composer and spacing, preserving room for messages; long drafts scroll inside the input. + +The API returns separate `activity`: up to 64 status/note entries, 8000 characters each and 64000 total. New messages save activity separately from content and restore it as expandable history. Continuation and compaction use completed messages only, excluding activity. Older stored answers are unchanged. + +NDJSON includes activity events. A text-delta with provisional=true belongs to the unfinished step and appears in progress, not yet the final answer. On step completion, Core keeps it as a public note or sends final text with reset=true, replacing current answer text. Stopping preserves public notes separately and confirmed answer text in content. + +An empty conversation shows a welcome card with search and structure actions, or filter/field explanations on collection pages. Without page context, it suggests general data-design questions. Database and Google tools remain available if data access is separately enabled. Clicking a suggestion fills an editable prompt; the user still sends it. The card also opens personal Google Drive/Sheets connections and disappears once conversation begins. + +Saved records offer Ask about this record. The assistant opens inside the editor, including nested cards and imported materialized views. Collapsing it keeps the record card and draft open. Context shows collection and record ID; closing the card restores page context. + +The composer shows context for the next question. A change from the previous question displays a compact history-context hint. Context transitions have separators in the conversation and persist in restored history. Help explains isolation, compaction, and working without page context. Search/filter changes within one table do not create a separate history. Navigation during generation leaves the submitted context unchanged; the new location applies to the next question. + +A record snapshot sends only its saved identifier. Before contacting the model, Core checks MCP enablement and read access to that row. Tools fetch field values with current user permissions. Unsaved form values and new draft records are excluded. Table conditions are not inherited by record context, and Apply table filter is unavailable inside a record card. + +Record context is `{ page: "items", workspaceId: null, collection: "articles", record: { id: "1" } }`. Record accepts only a string ID up to 255 characters, additionally validated against primary-key type. Record and table are mutually exclusive. A collection disabled in MCP omits record context and denies its data/schema tools. Existing table context remains compatible. + +Page context and data access have separate controls. The composer switch enables page, collection, record, and table-condition information. Data and connections in Assistant connections enables database, personal Google, and plugin tools for future questions. Disabling it neither deletes accounts nor stops an already submitted request. User grants, MCP enablement, and Google write confirmation still apply. + +The optional /assistant/messages parameter `dataAccess: { enabled: true, workspaceId: null }` enables tools with context:null, without page information. The model must explicitly find/select a collection; search/filter conditions are not inherited and suggesting a filter for the open table is unavailable. Enabled:false disables every tool regardless of context. With context present, workspaceId must match. This binds conversation context; it adds neither permissions nor a collection-catalog restriction. + +Clients omitting dataAccess preserve previous behavior: no context means no tools. Invalid parameters are rejected rather than enabling access. + +Working history and summaries are partitioned by access mode and workspace even without page context. Turning access off keeps visible messages but excludes the data-enabled mode's messages/summaries from new responses. Returning to that mode resumes its history. Retrying keeps the original page snapshot and never broadens access: if the user disabled tools after failure, retry keeps them disabled. + +Widget conversations are stored in PostgreSQL and owned by the current user. Session history opens previous conversations; reload restores the latest session. New session creates an empty context and retains earlier history. Sessions can be deleted and older messages loaded. Other users' history is inaccessible, including to superusers. + +Unsent text stays separately per session in the open admin's memory. New conversations start empty; previous drafts remain in history with a Draft marker and a Return to draft hint. Reopening restores input, and loading older messages does not change it. Failed session creation/opening preserves the current text. Input written before the first session is attached to it. Successful session deletion also removes its draft. + +Drafts are never sent to the API/model or browser storage. Collapsing preserves them; reloading or leaving the admin clears them. PostgreSQL history excludes unsent input. + +Stored history includes message text, public progress, page-context association, completion/stop outcome, and metrics. It excludes raw page snapshots, tool results, connection tokens, and internal reasoning. Text stays as sent by the user/assistant. Interactive filter, form, and write-confirmation cards work only in the current response and are not reactivated from history. Ask again for a new action. + +Before model calls, Core collects completed message pairs for the current context. Exceeding 24000 characters, 25 messages, or 50000 serialized bytes triggers compaction of older history using the same model without tools. Summaries are capped at 4000 characters, preserving recent messages and the current question. Full conversation history remains in the database. + +Each page/record context has its own summary, which is not transferred to a new session. The visible transcript is shared, but the model receives completed messages only from the current context. Conversation content grants no authorization. Compaction counts toward telemetry and the eight-call limit. + +POST /assistant/messages supports persisted conversationId, messageId, content, and optional settings/context. Repeating a completed messageId returns saved text without another model call; changing its body is forbidden. The legacy messages format remains compatible but does not persist conversation. Session API: GET/POST /assistant/conversations and GET/DELETE /assistant/conversations/:id, with before pagination. + +Only one request runs per session. Client disconnect stops generation; received text is saved when handling cancellation. After a process crash, incomplete responses become interrupted after eleven minutes. Compaction and answering share one configured timeout. Background generation and stream resumption after reload are unsupported. + +Deleting a user deletes personal conversations. When explicitly configured, HISTORY_RETENTION_DAYS removes inactive sessions by last-message time. + +PostgreSQL shares limits across replicas: defaults are 100 requests per user per UTC day and two concurrent requests, controlled by ASSISTANT_DAILY_REQUESTS and ASSISTANT_CONCURRENT_REQUESTS. Each request permits eight model calls, 100000 serialized context bytes before each call, and the configured output-token limit. + +The last available call is reserved for a final answer without tools, counting compaction. If steps run out, the model should explain missing information. Core never executes a tool returned on that final step. These are volume/count limits, not a monetary provider budget. History is retained by default; age cleanup requires HISTORY_RETENTION_DAYS. + +Confirmed assistant disablement hides the closed widget. An already open panel preserves conversation/input and shows that it is disabled. Network errors, timeouts, or invalid status responses keep the panel visible with Retry. Retry checks the connection without sending a model message. New questions stay disabled until a successful check, while input remains editable. Technical failures preserve the last known configuration and user settings. + +401 and 403 have distinct expired-session/access-denied messages. Initially confirmed lack of access hides the closed widget. Manual checks and focus/settings changes cancel prior checks; stale responses cannot replace newer results. Status checks time out after eight seconds. + +Record discussions remain in the Comments plugin. [Notifications](./notifications.md) use a separate header bell and work independently of AI. + +Context and tool-result data go to the configured provider. Provider choice, transfer scope, and contractual requirements are operator decisions. Record text and plugin results may contain prompt injection; model output is never an authorization boundary. ## Google Workspace -[Личное подключение Google Drive и Sheets](./google-workspace.md) добавляет чтение по запросу и предложения записи с подтверждением. Активное подключение также разрешает чат человеку без data grants, сохраняя ограничения данных Core. +[Personal Google Drive and Sheets connections](./google-workspace.md) add on-demand reads and confirmed write proposals. An active connection also permits chat without data grants while preserving Core access limits. -Карточки Google-записи различают выполнение, отклонение, отказ до записи и -неизвестный результат. Итог можно открыть повторно и обновить отдельным чтением -статуса; подтверждение не повторяется. При наличии ссылка ведёт в файл или таблицу -Google. Просмотр ограничен сроком предложения и владельцем подключения; подробнее — -в [описании Google Workspace](./google-workspace.md). +Write cards distinguish completion, rejection, pre-write failure, and unknown outcome. Results can be reopened and refreshed through a read-only status request without repeating confirmation. Links open the Google file/spreadsheet when available. Viewing remains owner- and lifetime-bound; see [Google Workspace](./google-workspace.md). -Отмена ответа доступна через любую реплику Core: запрос сохраняется в PostgreSQL, -активный процесс проверяет его каждые 500 мс. При перезапуске процесса поток -прерывается; бесшовное продолжение генерации не поддерживается. Подготовленные формы -плагинов сохраняются в PostgreSQL на 20 минут и проверяют владельца при открытии. +Cancellation works through any Core replica: PostgreSQL stores the request, and the active process checks every 500 ms. Process restart interrupts the stream; seamless generation continuation is unsupported. Prepared plugin forms are stored in PostgreSQL for twenty minutes and check ownership when opened. diff --git a/docs/features/connections.md b/docs/features/connections.md index 844c75f..f88c6fe 100644 --- a/docs/features/connections.md +++ b/docs/features/connections.md @@ -1,104 +1,54 @@ -# Подключения и защита секретов - -Суперпользователь управляет подключениями на `/admin/settings/integrations`. -Список открывает правую панель с безопасными параметрами, состоянием ключей, -проверкой соединения и включением/отключением. Изменения подхватываются ядром -без перезапуска, включая другие экземпляры, подключённые к той же базе. - -[Sentry и мониторинг](./monitoring.md) настраиваются отдельно на -`/admin/settings/monitoring`. DSN используют те же write-only secrets, revision, -локальное шифрование/Yandex KMS и приоритет env. - -## Источники настроек - -Непустые параметры подключения в окружении Core имеют приоритет над настройками -админки. Вся соответствующая группа становится read-only: API также отклоняет -изменение с 409. Пустые примеры переменных не блокируют группу. Явное -`ASSISTANT_ENABLED=false` или `FILES_STORAGE=disabled` отключает подключение и -блокирует редактирование. Незавершённая env-конфигурация не дополняется -сохранёнными в админке ключами. -Изменение env-файла требует обычного перезапуска Core; изменения через админку -подхватываются автоматически. - -API возвращает адрес API, модель, бакет, регион, источник и признаки наличия -ключей. URL с логином, паролем, query или fragment не допускаются. Секреты -write-only: пропуск сохраняет прежний ключ, `null` удаляет, пустая строка запрещена. -Параметры и секреты сохраняются при отключении подключения. - -В read-only настройках S3 бакет, регион, endpoint и ID сервисного аккаунта можно -выделить или скопировать кнопкой рядом с полем. Ключи доступа не раскрываются и -не копируются; провайдер и включение подключения остаются недоступны для изменения. - -`GET /settings/integrations`, `PUT /settings/integrations/:section` и -`POST /settings/integrations/:section/test` требуют активной человеческой сессии -superuser. `section`: `storage`, `assistant`, `encryption`. Изменение передаёт -текущую `revision`: устаревший или конкурентный запрос получает 409. -Делегированные `files/update` и `assistant/update` не дают доступа к ключам -подключений. Лимит проверки соединения — 12 запросов в минуту по правилам -credential rate limit. - -## Файлы и ассистент - -S3 поддерживает бакет, регион, HTTPS endpoint и отдельные access/secret/session -keys. Yandex Object Storage также поддерживает федеративный обмен OIDC → IAM -без постоянного S3-ключа: в админке задаются бакет и ID сервисного аккаунта, -на сервере — `YC_OIDC_TOKEN_FILE` с обновляемым projected token. Ранее -настроенные `FILES_YC_*` продолжают работать. Создание bucket, федерации, -сервисного аккаунта и IAM-политик выполняет оператор установки. - -Проверка S3 выполняет HeadBucket и не загружает файлы. При наличии файлов -ядро запрещает смену местоположения хранилища; перенос файлов — отдельная -операция оператора. Отключение сохраняет метаданные, но загрузка и скачивание -байтов требуют включённого подключения. - -AI-провайдер поддерживает OpenAI-compatible Responses и Chat Completions, -base URL, модель, лимит ответа, таймаут и поддерживаемые настройки reasoning / -thinking. API-ключ хранится зашифрованным. Проверка запрашивает `GET /models` -без генерации: она проверяет доступность этого endpoint, но не гарантирует -доступ к конкретной модели. Некоторые совместимые провайдеры не реализуют -список моделей. Инструкции и телеметрия остаются на странице AI-ассистента. - -## Шифрование - -Все обратимые секреты, сохранённые этими настройками админки, хранятся в -PostgreSQL как версионированный ciphertext. Пароли и session/access tokens -продолжают храниться в виде хешей; OAuth signing keys и env-секреты остаются -конфигурацией оператора. -Секреты OAuth-приложений и OAuth protocol payload сохраняют существующее -шифрование ключом из `OAUTH_KEYS_FILE`; новый провайдер относится к подключениям -S3/AI и не меняет действующие OAuth-сеансы. - -| Режим | Настройки админки | Bootstrap на сервере | -| ---------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| Локальный | Выбор локального провайдера | `SECRETS_LOCAL_KEY`: 32 случайных байта, base64url | -| Yandex KMS | ID симметричного ключа и сервисного аккаунта | `YC_OIDC_TOKEN_FILE`: projected OIDC token, федерация и права `kms.keys.encrypterDecrypter` на ключ | - -Локальный режим использует AES-256-GCM со случайным nonce и AAD, привязывающим -секрет к установке и полю. KMS использует native REST encrypt/decrypt с AAD -и краткоживущим федеративным токеном; ключ KMS не передаётся приложению. -Размер одного секрета ограничен 8000 байт UTF-8. Lockbox в этом механизме -не участвует: KMS защищает ключом ciphertext в базе. - -Смена провайдера или KMS key ID проверяет новый ключ, расшифровывает секреты -старым и шифрует новым в одной транзакции. Ошибка сохраняет прежнее состояние; -автоматического переключения на локальный ключ нет. Для защиты от потери -данных сначала смените провайдера в админке, затем при необходимости закрепите -его env-переменными `SECRETS_PROVIDER`, `SECRETS_YC_KMS_KEY_ID`, -`SECRETS_YC_SERVICE_ACCOUNT_ID`. Изменение env поверх несовместимого сохранённого -провайдера блокирует чтение секретов до восстановления согласованной настройки. - -Bootstrap-ключ не задаётся через админку. Его нельзя заменять случайным новым -значением поверх действующей установки: прежние ciphertext требуют прежнего -ключа. Резервная копия базы должна сопровождаться отдельной защищённой копией -локального ключа либо сохранённым доступом к прежним версиям ключа KMS. -После переноса в KMS старый локальный ключ может потребоваться для восстановления -старых резервных копий. - -Сохранённые ciphertext и секреты не включаются в журналы изменений или ответы -API. Событие аудита содержит только раздел, провайдер и число ключей. -Настройки можно просмотреть даже при недоступности KMS; работа подключений -требует успешного получения их секретов. Уже созданные клиенты могут держать -расшифрованные ключи в памяти до смены конфигурации или остановки процесса. - -См. [Yandex Workload Identity Federation](https://yandex.cloud/ru/docs/iam/concepts/workload-identity) -и [KMS symmetric encryption](https://yandex.cloud/ru/docs/kms/concepts/symmetric-encryption). + + +# Connections and secret protection + +Superusers manage connections at `/admin/settings/integrations`. Each list entry opens a side panel with safe parameters, key status, connection testing, and an enable switch. Core picks up changes without restart, including other instances using the same database. + +[Sentry monitoring](./monitoring.md) has a separate page at `/admin/settings/monitoring`. DSNs use the same write-only secrets, revisions, local encryption/Yandex KMS, and environment priority. + + + +## Configuration sources + +Nonempty Core environment parameters take precedence over admin settings. The entire corresponding group becomes read-only; the API rejects changes with 409. Empty example variables do not lock a group. Explicit `ASSISTANT_ENABLED=false` or `FILES_STORAGE=disabled` disables and locks that connection. Incomplete environment configuration is not supplemented with saved admin keys. + +Environment changes require a Core restart. Admin changes apply automatically. + +The API returns safe values such as API URL, model, bucket, region, source, and key-presence flags. URLs containing credentials, query strings, or fragments are rejected. Secrets are write-only: omission preserves the old value, `null` removes it, and an empty string is invalid. Disabling a connection retains its parameters and secrets. + +Read-only S3 settings allow selecting/copying the bucket, region, endpoint, and service-account ID. Access keys cannot be revealed or copied; provider and enable switches remain locked. + +`GET /settings/integrations`, `PUT /settings/integrations/:section`, and `POST /settings/integrations/:section/test` require an active human superuser. Sections are `storage`, `assistant`, and `encryption`. Updates supply the current `revision`; stale or concurrent requests return 409. Delegated files/update or assistant/update grants do not expose connection keys. Connection tests allow 12 requests per minute under the credential rate-limit rules. + + + +## Files and assistant + +S3 supports bucket, region, HTTPS endpoint, and separate access/secret/session keys. Yandex Object Storage also supports OIDC → IAM federation without a permanent S3 key: configure bucket and service-account ID in the admin, and `YC_OIDC_TOKEN_FILE` with a refreshed projected token on the server. Existing `FILES_YC_*` settings continue to work. Operators create the bucket, federation, service account, and IAM policies. + +The S3 test runs HeadBucket without uploading files. Core prevents changing storage location while files exist; moving files is a separate operator task. Disabling storage preserves metadata, but uploading/downloading bytes requires an enabled connection. + +The AI provider supports OpenAI-compatible Responses and Chat Completions, base URL, model, output limit, timeout, and supported reasoning/thinking settings. Its API key is encrypted. Testing calls `GET /models` without generating content. This checks endpoint availability, not access to a particular model; some compatible providers do not implement model listing. Instructions and telemetry remain on the Assistant page. + + + +## Encryption + +Reversible secrets saved by these admin settings are stored in PostgreSQL as versioned ciphertext. Passwords and session/access tokens remain hashes. OAuth signing keys and environment secrets remain operator configuration. + +OAuth application secrets and protocol payloads retain their existing encryption using `OAUTH_KEYS_FILE`. The connection-secret provider does not change active OAuth sessions. + +| Mode | Admin configuration | Server bootstrap | +| ---------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| Local | Select the local provider | `SECRETS_LOCAL_KEY`: 32 random bytes, base64url | +| Yandex KMS | Symmetric key and service-account IDs | `YC_OIDC_TOKEN_FILE`: projected OIDC token, federation, and `kms.keys.encrypterDecrypter` on the key | + +Local encryption uses AES-256-GCM with a random nonce and AAD binding each secret to its installation and field. KMS uses native REST encrypt/decrypt with AAD and a short-lived federated token; the KMS key is never sent to the application. Each secret is limited to 8000 UTF-8 bytes. Lockbox is not involved: KMS protects ciphertext stored in the database. + +Changing provider or KMS key ID validates the new key, decrypts with the old one, and re-encrypts in one transaction. Failure preserves the previous state; there is no automatic fallback to the local key. Change provider in the admin first, then optionally lock it with `SECRETS_PROVIDER`, `SECRETS_YC_KMS_KEY_ID`, and `SECRETS_YC_SERVICE_ACCOUNT_ID`. An incompatible environment override blocks secret reads until configuration is consistent again. + +The bootstrap key is not set through the admin. Do not replace it with a random new value on an existing installation: old ciphertext requires the old key. Database backups need a separate protected copy of the local key or continued access to the relevant KMS key versions. After moving to KMS, the old local key may still be needed to restore older backups. + +Neither secrets nor ciphertext appear in audit logs or API responses. Audit events contain only section, provider, and key count. Settings remain viewable while KMS is unavailable, but connections need successful secret retrieval. Existing clients may retain decrypted keys in memory until configuration changes or the process stops. + +See [Yandex Workload Identity Federation](https://yandex.cloud/en/docs/iam/concepts/workload-identity) and [KMS symmetric encryption](https://yandex.cloud/en/docs/kms/concepts/symmetric-encryption). diff --git a/docs/features/data.md b/docs/features/data.md index e3e61ec..47ce00a 100644 --- a/docs/features/data.md +++ b/docs/features/data.md @@ -1,57 +1,42 @@ -# Коллекции, записи и связи + -## Структура +# Collections, records, and relationships -Главная страница `/` показывает доступные коллекции выбранного рабочего пространства -и открывает их записи по `/items/`. Управление структурой, полями и связями -находится отдельно в `/admin/collections` и доступно суперпользователю. Настройки -команды и системы остаются в `/admin/settings`. Маршруты Core API `/collections` -и `/items` от этого разделения не меняются. + -Коллекция описывает пользовательскую таблицу либо явно подключённое materialized view в `public`. Core-таблицы используют `asmblyr_`, таблицы расширений — `plugin__`; эти префиксы зарезервированы. Их структуру нельзя произвольно менять через API. +## Structure -Создание коллекции задаёт техническое имя, режим одной записи, первичный ключ (serial, bigserial, UUID либо ручная строка), системные даты. Отображаемое название, шаблон подписи записи, скрытие из навигации, MCP-описание и участие в MCP настраиваются отдельно. Скрытие не отзывает доступ по API. +The home page / lists accessible collections in the selected workspace and opens records at /items/<collection>. Superusers manage structure, fields, and relationships separately at /admin/collections. Team/system settings remain at /admin/settings. Core API /collections and /items routes are unchanged by this separation. -В редакторе поля выбирается обычный тип либо связь; настройки разделены по назначению. После создания нельзя менять техническое имя/тип обычного поля. `nullable` отвечает за допустимость NULL в БД; `required` — за непустое значение в API. Это разные ограничения. +A collection describes a user table or explicitly imported materialized view in public. Core reserves asmblyr* tables; plugins reserve plugin*<namespace>\_<name>. Their structure cannot be arbitrarily changed through the API. -Системное состояние по умолчанию использует опубликовано/черновик/архивировано, новая запись — опубликована. Стандартное представление скрывает черновики/архив; это поведение отображения, а не ограничение доступа. Значения состояния настраиваются. +Collection creation sets its technical name, singleton mode, primary key (serial, bigserial, UUID, or manually assigned string), and system timestamps. Display label, record-label template, navigation visibility, MCP description, and MCP enablement are separate settings. Hiding a collection does not revoke API access. -## Работа с данными +Field editors distinguish ordinary types and relationships, with settings grouped by purpose. Ordinary field names/types cannot change after creation. Nullable controls database NULL; required controls nonempty API values. These are different constraints. -### Дополнительные поля системных коллекций +Default system status uses published/draft/archived, with new records published. The default view hides drafts/archives as presentation behavior, not authorization. Status values are configurable. -В `/admin/collections` раздел «Показать системные коллекции» по умолчанию свёрнут. -Он содержит пользователей, файлы, политики доступа, рабочие пространства и сервисные -аккаунты. Сессии, токены, секреты и служебные таблицы не показываются. Встроенные поля -помечены замком: их имя, тип, ограничения и отображение нельзя менять этим редактором. -Сами системные коллекции нельзя переименовать или удалить. + -Суперпользователь может добавлять собственные физические колонки: обычные типы, -даты, JSON, теги, списки вариантов, файлы и связи «многие к одному» с обычными -пользовательскими коллекциями. Настройки открываются в стандартной правой панели. -Вычисляемые/условные правила и настройка поиска для этих полей -пока не поддерживаются. Все дополнительные колонки остаются nullable и необязательными, -чтобы регистрация пользователя и другие штатные операции не требовали новых значений. -Можно задать default; при добавлении он применяется к существующим строкам, -а его последующее изменение влияет только на новые записи. +## Working with data -Кнопка «Дополнительные данные» открывает выбор существующей записи и форму её -пользовательских полей. У пользователей эти поля также доступны суперпользователю -в карточке профиля в `/admin/settings/users`, с отдельной кнопкой сохранения. -Создание и удаление системных записей остаётся в штатных -разделах. Удаление собственного поля требует ввода его имени и удаляет значения; -зависимые SQL-представления блокируют удаление, `CASCADE` не используется. + -Например, создайте коллекцию `departments`, затем в системной коллекции «Пользователи» -добавьте поле «Многие к одному» с именем `department_id` и выберите `departments`. -В дополнительных полях пользователя появится обычный селектор записей с поиском. -В БД создаются внешний ключ и индекс; удаление департамента очищает ссылку -(`ON DELETE SET NULL`), сохраняя пользователя. Удаление самой коллекции департаментов -блокируется, пока существует поле связи. Целевая коллекция и тип ключа после создания -не меняются. Системные/плагинные таблицы, singleton и materialized view не могут быть -целями этих связей; обратные O2M/M2M-поля и default для связи не поддерживаются. +### Custom fields on system collections -Чтобы создать такую связь через существующий POST configuration, передайте: +The Show system collections section at /admin/collections starts collapsed. It contains users, files, policies, workspaces, and service accounts, excluding sessions, tokens, secrets, and internal tables. Locked built-in fields cannot have names, types, constraints, or presentation changed here. System collections cannot be renamed or deleted. + +Superusers may add physical custom columns: ordinary types, dates, JSON, tags, choices, files, and many-to-one relations to ordinary user collections. Standard side panels edit their settings. Computed/conditional rules and search configuration are not yet supported. All custom columns remain nullable and optional so registration and built-in operations need no new values. + +A default can be set; adding a column applies its default to existing rows, while later default changes affect only new records. + +Additional data opens an existing-record selector and custom-field form. Superusers also edit users' extra fields in /admin/settings/users, with a separate save button. Native sections retain creation/deletion of system records. Deleting a custom field requires typing its name and removes its values; dependent SQL views block deletion, without CASCADE. + +For example, create departments, then add a many-to-one department_id field to system Users targeting departments. User extra data gains a searchable record selector. Core creates a foreign key and index. Deleting a department clears the reference with ON DELETE SET NULL and preserves the user. Dropping departments is blocked while the relation exists. + +The target and key type cannot change after creation. System/plugin tables, singletons, and materialized views cannot be targets. Reverse O2M/M2M fields and relation defaults are unsupported. + +Existing POST configuration accepts: ```json { @@ -60,38 +45,25 @@ "type": "relation", "targetCollection": "departments" }, - "presentation": { "label": "Департамент" } + "presentation": { "label": "Department" } } ``` -Core API: `GET /system-collections`, `POST|PUT -/system-collections/:name/fields/:field/configuration`, `DELETE -/system-collections/:name/fields/:field`, `GET /system-collections/:name/records` -и `GET|PATCH /system-collections/:name/records/:id`. Имена `users`, `files`, `policies`, -`workspaces`, `service_accounts` являются отдельным системным каталогом. PATCH принимает -`{ "values": { "custom_field": "value" } }` и изменяет только зарегистрированные -пользовательские колонки. Встроенные значения и секреты не возвращаются; селектор -содержит ID и подпись (для пользователя — email). Все эти маршруты доступны только -активному человеческому суперпользователю, включая заполнение значений. Системные -таблицы не становятся обычными `/items`-коллекциями и не включаются в генерацию SDK. -Самостоятельное редактирование этих полей обычными пользователями не включено; -отдельная коллекция расширения профиля с обычными политиками продолжает работать. - -Владение колонкой фиксируется атомарно с DDL в `asmblyr_system_fields`. -Существующая колонка никогда не присваивается пользовательскому расширению. -При будущих миграциях Core нужно проверять конфликт имён и разрешать его явно, -с сохранением пользовательских данных. Откат реестра запрещён, пока в нём есть поля. -Изображения и галереи используют обычную файловую библиотеку; привязанные файлы -защищены от удаления. Интеграционные проверки: `node scripts/test.mjs core-system-collections`. - -### Связь обычной коллекции с пользователем - -В редакторе обычной коллекции выберите «Многие к одному», затем -«Пользователи · системная». Например, поле `manager_id` связывает департамент -с руководителем. Форма, фильтр и ячейки таблицы используют имя и почту пользователя; -само значение в записи и SDK — UUID. Системные таблицы не появляются в обычном каталоге. - -Создание через `POST /collections/:name/relations` доступно суперпользователю: +Core API: GET /system-collections, POST|PUT /system-collections/:name/fields/:field/configuration, DELETE /system-collections/:name/fields/:field, GET /system-collections/:name/records, and GET|PATCH /system-collections/:name/records/:id. + +Names users, files, policies, workspaces, and service_accounts belong to a separate system catalog. PATCH accepts `{ "values": { "custom_field": "value" } }` and changes registered custom columns only. Built-in values and secrets are not returned; selectors expose ID and label (email for users). + +All these routes, including value editing, require an active human superuser. System tables do not become ordinary /items collections and are excluded from SDK generation. Ordinary users cannot self-edit these fields. The separate profile-extension collection with ordinary policies remains available. + +Column ownership is recorded atomically with DDL in asmblyr_system_fields. Existing columns are never adopted as custom extensions. Future Core migrations must detect naming conflicts and resolve them explicitly while preserving data. Registry rollback is blocked while fields exist. Images/galleries use the normal library, and attached files cannot be deleted. Tests: node scripts/test.mjs core-system-collections. + + + +### Linking an ordinary collection to a user + +In an ordinary collection's relationship editor, choose Many to one → Users · system. For example, manager_id links a department to its manager. Forms, filters, and table cells show user name/email; records and SDK retain the UUID. System tables stay out of the ordinary catalog. + +Superusers create the relation through POST /collections/:name/relations: ```json { @@ -103,194 +75,93 @@ Core API: `GET /system-collections`, `POST|PUT } ``` -`@users` — специальная цель связи, отличная от пользовательской коллекции `users`. -Core создаёт UUID-столбец, индекс и внешний ключ на системного пользователя. -Поддерживаются `restrict` и `setNull`; каскадное удаление записей, обратные поля, -значения по умолчанию, вычисления и зависимые фильтры этой связи не поддерживаются. -Фильтрация записей по UUID руководителя поддерживается; поиск внутри связанного -профиля и переходы вроде `manager_id.email` не предоставляются. - -Для поиска пользователей, получения подписей и назначения пользователя требуется -человеческая сессия с чтением раздела «Пользователи» (или суперпользователь). -Права записи исходной коллекции также обязательны. Сервисные ключи не получают -доступ к каталогу пользователей. Очистить необязательную связь можно с правом -изменения самого поля. Назначить можно только активного пользователя. -Сохранённую связь можно оставить без изменения, даже если пользователь отключён -или у редактора больше нет доступа к каталогу пользователей. - -`GET /users/references` возвращает только `id` и `label`, принимает `q`, `page`, -`limit` (1–100) и `ids` (до 100 UUID через запятую). Поиск показывает активных; -запрос известных `ids` также позволяет подписать уже сохранённые ссылки на -отключённых пользователей. Без права чтения подпись недоступна, UI показывает UUID. -`/items/asmblyr_users` остаётся закрытым. Миграция не добавляет пользовательские -поля автоматически; откат блокируется, пока существуют связи с пользователями. +@users is a special target, distinct from a user-created users collection. Core creates a UUID column, index, and FK to the system user. Restrict and setNull are supported; cascading record deletion, reverse fields, defaults, computations, and dependent filters are not. Filtering by manager UUID works; searching the linked profile or paths such as manager_id.email is not exposed. + +Searching, resolving labels, and assigning a user require a human session with Users read access or a superuser, plus write access to the source collection. Service keys cannot access the directory. Clearing an optional relation requires update on the field. Only active users can be assigned. Existing references may stay unchanged after the user is disabled or the editor loses directory access. + +GET /users/references returns only id/label and accepts q, page, limit (1–100), and ids (up to 100 comma-separated UUIDs). Search shows active users; known-ID lookup can label stored references to disabled users. Without read permission, labels are unavailable and the UI shows UUIDs. + +/items/asmblyr_users remains closed. Migration creates no custom fields automatically; rollback is blocked while user relations exist. ### Materialized views -На `/admin/collections` суперпользователь может подключить готовое PostgreSQL -materialized view через кнопку-иконку «Подключить представление». В правой панели -выбирается источник из `public`, затем название, ключ строки, папка, подпись записи -и подписи полей. Подключение добавляется в выбранное рабочее пространство. -Сохранение регистрирует только метаданные Asmblyr; отмена ничего не создаёт. -После подключения обычные настройки полей и конструктор карточки позволяют -изменить форматы, переводы, порядок и расположение полей. - -Без отдельной раскладки карточка представления автоматически располагает короткие -поля в две колонки (в одну в узкой панели), а заголовок и длинное содержимое — на -полную ширину. Числа показываются с разделителями разрядов без потери точности -decimal/bigint, булевы значения — компактными метками. Указанные подписи, переводы, -форматы, скрытие и раскладка сохраняют приоритет. Автоматические подписи только -заменяют подчёркивания пробелами; перевод пользовательских полей и валюта не -угадываются. Это оформление при чтении: SQL и сохранённые метаданные не изменяются, -поэтому оно работает и для ранее подключённых представлений. - -Первый вариант требует заполненное представление с поддерживаемыми скалярными -полями и одним устойчивым ключом: UUID, положительный integer/bigint или непустая -строка не длиннее 255 символов. На ключе должен существовать действующий полный -одноколоночный UNIQUE индекс; составные, частичные и expression-индексы не подходят. -Имена источника и полей должны соответствовать обычным API-именам коллекций. -Несовместимые и уже подключённые объекты отображаются с причиной и недоступны -для выбора. Зарезервированные Core/plugin объекты не предлагаются. - -Представления доступны только для чтения: таблица, карточка, фильтры, сортировка, -поиск, SDK и MCP используют обычный read API. Для других пользователей нужны -отдельные read grants на поля и выходные строки. Права на исходные коллекции -не переносятся автоматически. В схеме SDK `sourceKind` равен `materialized-view`, -create/update/delete выключены, а генератор типов запрещает операции записи. - -В списке записей подключённого materialized view показана метка «Только просмотр». -Она отличает представление от редактируемой коллекции; настройка отображения, -поиск и фильтры остаются доступны в рамках прав пользователя. - -Создание, редактирование и удаление строк, bulk/commit с вложенными записями, -физические поля, индексы, CRUD-состояния и FK-связи запрещены сервером, включая -суперпользователя. Изменяемые метаданные отображения не изменяют SQL-объект. -«Отключить представление» удаляет подключение и его права/метаданные из каталога, -сохраняя само materialized view и данные в PostgreSQL. - -SQL-определение, создание и `REFRESH MATERIALIZED VIEW` управляются внешними -миграциями или процессом проекта. Перезагрузка таблицы выполняет только чтение. -Внешний процесс должен сохранять выбранный ключ уникальным, непустым и устойчивым -между пересчётами. Изменение колонок или потеря уникального индекса возвращают -`409 MATERIALIZED_VIEW_CHANGED`; источник нужно подключить заново. -Незаполненный источник возвращает `503 MATERIALIZED_VIEW_NOT_POPULATED`. -Время последнего внешнего пересчёта не показывается. Составные ключи, обычные views -и логические связи с представлениями не поддерживаются этим вариантом. - -### Типы и простые редакторы - -Поле `date` хранит календарную дату без времени и часового пояса. API, SDK и Kit -передают её строкой `YYYY-MM-DD`; форма показывает календарный ввод и не пересчитывает -сутки по часовому поясу устройства. Поддерживается диапазон `0001-01-01`–`9999-12-31`; -несуществующие даты и строки со временем отклоняются. `datetime` сохраняет прежний -контракт момента времени с часовым поясом. - -Обычный `bigint` — знаковое 64-битное целое, отдельное от `bigserial` первичного ключа. -API принимает и возвращает каноническую десятичную строку, например -`"9007199254740993"`. Диапазон — от `"-9223372036854775808"` до -`"9223372036854775807"`. JS-числа, дроби, экспоненциальная запись, ведущие нули и `-0` -отклоняются. Фильтрация и сортировка выполняются по числовой колонке PostgreSQL. - -В уже управляемой коллекции `character varying`/`varchar` распознаётся как `text`. -Это не меняет SQL-тип и ограничение длины колонки и не подключает произвольные -существующие таблицы автоматически. - -Для одного варианта используется штатный `select`: у `text` значения вариантов -строковые, у `integer` — числовые, включая `0` и отрицательные числа. Пример настройки: -`{ interface: "select", options: [{ value: 0, label: "Обычный" }] }`. -Сервер проверяет тип, допустимые значения и default. Удалённые варианты остаются -видимыми в старых записях; новые записи и изменения этого поля принимают только -текущий список. `multiselect` для JSON сохраняет строковые варианты. - -«Теги» в выборе нового поля создаёт JSON-колонку с `interface: "tags"`. Этот же -редактор можно выбрать для существующего JSON-поля. Ввод показывает компактные -плашки: Enter или выход из поля добавляет тег, крестик удаляет его. Просмотр -карточки показывает все теги, таблица — первые три и `+N` с остальными в подсказке. - -API и редактор принимают массив строк: до 100 тегов, каждый до 120 символов после -обрезки пробелов по краям. Пустые теги, повторы после обрезки и управляющие символы -отклоняются; регистр и порядок сохраняются. Обязательное поле требует хотя бы -одного тега; необязательное допускает `[]`, а nullable-поле — также `null`. -Default проходит ту же проверку. Настройка редактора не переписывает старые записи: -несовместимое значение остаётся видимым как исходный JSON и проверяется при явном -изменении этого поля. `/schema` экспортирует теги как `strings` без закрытого enum, -генератор SDK — как `string[]` (с `| null` для nullable-поля). После смены редактора -обновите схему SDK через `asm schema pull` и `asm generate`. - -Для radio-сценариев достаточно этого же списка. Обычный JSON редактируется в -текстовом поле с проверкой JSON перед сохранением и на сервере. Текстовое поле с -интерфейсом `url` принимает абсолютный HTTP(S)-адрес без учётных данных; компактная -кнопка рядом открывает корректную ссылку в новой вкладке. При просмотре ссылка -также доступна для открытия. - -### Редактирование и таблица - -Нажатие строки открывает боковой редактор; запись имеет собственный URL `/items//`. Без права update редактор работает на чтение. Создание использует такой же диалог. Выделение строк показывает плавающую панель действий. - -Таблица поддерживает страницы, сортировку, перестановку/скрытие и ресайз колонок, поиск, -фильтры и виды. При вводе поиска включается порядок по релевантности: точное -совпадение, начало названия с целым словом, целое слово внутри названия, затем -подстрока. Точный первичный ключ имеет наивысший приоритет. При равном совпадении -основные поисковые поля выше дополнительных; автоматический приоритет берётся -из шаблона/поля названия записи, иначе из первого текстового поля. Приоритет -настраивается в редакторе поля или связи. Поиск остаётся буквальным и без учёта -регистра; исправления опечаток и морфологии нет. - -Ширина меняется перетаскиванием правого края заголовка. Двойной щелчок возвращает -стандартную ширину. На разделителе работают стрелки влево/вправо (шаг 16 px), -Home/End (80/1200 px) и Delete/Backspace (сброс); Escape отменяет текущий жест. -Ширины сохраняются в личных настройках после отпускания указателя, восстанавливаются -после перезагрузки и входят в определение именованного вида. Скрытие и перестановка -сохраняют ширины; удаление поля или потеря права чтения исключают его настройки. -Это настройка отображения: право редактировать записи не требуется. - -`GET/PATCH /users/me/table-preferences/:collection` и `columns` в определениях -`/table-views/:collection` поддерживают необязательное `widths`, например -`{"order":["id","title"],"hidden":[],"widths":{"title":400}}`. -Значения — целые CSS-пиксели от 80 до 1200, ключи — доступные поля коллекции. -Пустая карта возвращает все ширины к стандартным; отсутствие ключа поля — его -стандартная ширина. Старые определения без `widths` остаются допустимыми. - -Участие поля в поиске и индекс — отдельные решения. Поиск по настроенной связи -ограничен одним уровнем, учитывает название связанной записи и не размножает -строки источника. Скрытые правами и чувствительные поля не участвуют в совпадении -или ранжировании. Связи с условными правами строк пока исключены из поиска. -Релевантность вычисляется в PostgreSQL до пагинации; равные результаты упорядочены -по столбцу и первичному ключу. Клик по заголовку включает обычную сортировку, -переключатель возвращает релевантность. Режим сохраняется в личных видах. -Глобальный поиск ранжирует записи внутри каждой коллекции; общий межколлекционный -рейтинг не вычисляется. Наличие индекса не гарантирует приемлемый план на любом -размере данных: проверяйте EXPLAIN на своей выборке. - -В HTTP/SDK параметр `order` принимает `relevance` или `field`. Запрос с `q` без -явных `sort`/`direction` использует релевантность; явный столбец сохраняет обычный -порядок. `order=relevance` вместе с `sort` задаёт столбец для равных результатов. -Без поисковой строки эффективный режим всегда `field`, он возвращается в `page.order`. - -Таблица связанных записей без явно заданных колонок показывает ключ, поле подписи -и до двух доступных M2O-полей для контекста (например, площадку категории). -Ссылка на коллекцию текущей карточки не дублируется. Закрытые и чувствительные -поля не добавляются в автоматический контекст; выбранный пользователем набор -колонок имеет приоритет. Пустое название остаётся пустым, вместо него не подставляется название -другого поля. Подписи M2O загружаются с обычными правами на связанные записи. - -Фильтры объединяют условия AND/OR, включая допустимые связанные поля. Нельзя использовать закрытое поле как канал фильтрации/сортировки. Максимум HTTP-страницы — 100; total передаётся строкой. - -## Связи и сохранение - -Есть M:1, обратные 1:M и M:N с промежуточной коллекцией. В форме можно выбирать существующие записи и создавать новые; права каждой затронутой коллекции/поля проверяются отдельно. Перенос между родителями требует соответствующего изменения связи. - -`POST /items/:collection/commit` сохраняет корневую запись и связанные изменения одной транзакцией. Ошибка откатывает также историю и транзакционные hooks. Draft ограничен 100 изменениями и глубиной 5. Последовательные обычные HTTP-запросы одной транзакцией не становятся. - -## Правила поля и зависимые формы - -Во вкладке «Правила» бокового редактора доступны скрытие в форме, режим чтения, -условная обязательность и копирование значения из M2O-связи. `rules.hidden` -меняет только форму; доступ через API по-прежнему задаётся политикой. -`rules.readonly` отклоняет явную запись поля через штатный writer, включая -superuser. Default при создании применяется без ручной записи readonly-поля. - -Пример `presentation` для поля `note`: +Superusers import existing PostgreSQL materialized views using the Connect view icon at /admin/collections. The side panel selects a public source, then label, row key, folder, record label, and field labels. The view joins the selected workspace. Saving registers only Collaborative metadata; cancellation creates nothing. Ordinary field settings and the card builder then control formats, translations, order, and placement. + +Without a custom layout, short fields use two columns (one in a narrow panel), while titles and long content span the width. Numbers use separators without losing decimal/bigint precision; booleans use compact badges. Explicit labels, translations, formats, hidden settings, and layout take precedence. Automatic labels only replace underscores with spaces; translations and currency are never guessed. This read-time presentation changes neither SQL nor metadata and works for previously imported views. + +Import requires a populated view with supported scalar fields and one stable key: UUID, positive integer/bigint, or a nonempty string up to 255 characters. A valid, full, single-column UNIQUE index must cover that key. Composite, partial, and expression indexes do not qualify. Source/field names must follow normal API naming rules. Incompatible or already imported objects show a reason and cannot be selected; reserved Core/plugin objects are excluded. + +Views are read-only. Tables, cards, filters, sorting, search, SDK, and MCP use ordinary read APIs. Other users need separate grants on output fields/rows; source-collection permissions do not transfer. SDK schema marks sourceKind as materialized-view and disables create/update/delete; generated types prohibit writes. + +The record list shows a Read only badge, while permitted presentation settings, search, and filters remain available. + +The server rejects row creation/update/deletion, bulk/nested commit, physical fields, indexes, CRUD status fields, and FK relationships, even for superusers. Editable presentation metadata never changes SQL. Disconnect view removes the catalog connection and its grants/metadata while preserving the PostgreSQL object and data. + +External migrations/processes own SQL definition, creation, and REFRESH MATERIALIZED VIEW. Reloading the UI only reads data. External refresh must preserve a unique, nonempty, stable key. Column changes or loss of the unique index return 409 MATERIALIZED_VIEW_CHANGED and require reconnecting the source. Unpopulated views return 503 MATERIALIZED_VIEW_NOT_POPULATED. Last external refresh time is not displayed. Composite keys, ordinary views, and logical relationships to views are unsupported. + + + +### Types and simple editors + +Date stores a calendar day without time or time zone. API, SDK, and Kit use YYYY-MM-DD; the calendar editor never shifts days for device time zone. Range: 0001-01-01 through 9999-12-31. Invalid dates and timestamps are rejected. Datetime keeps its time-zone-aware instant contract. + +Ordinary bigint is a signed 64-bit integer, distinct from a bigserial primary key. API input/output is a canonical decimal string, such as "9007199254740993", ranging from "-9223372036854775808" to "9223372036854775807". JavaScript numbers, fractions, exponents, leading zeros, and -0 are rejected. PostgreSQL performs numeric filtering/sorting. + +Character varying/varchar in an already managed collection maps to text without changing SQL type or length limits. This does not automatically import arbitrary tables. + +Select uses string options for text and numeric options for integer, including zero/negative numbers. Example: `{ interface: "select", options: [{ value: 0, label: "Standard" }] }`. Core validates types, allowed values, and defaults. Removed choices remain visible in old records; new records and explicit field edits accept only current choices. JSON multiselect stores string choices. + +Tags creates a JSON column with interface:tags, also selectable for existing JSON fields. Enter or blur adds a tag; the cross removes it. Cards show all tags; tables show three plus +N with a tooltip. + +API/editor accept up to 100 strings, each up to 120 characters after trimming. Empty tags, trimmed duplicates, and control characters are rejected; case/order are preserved. Required fields need at least one tag; optional allows []; nullable also allows null. Defaults use the same validation. + +Changing the interface does not rewrite old data. Incompatible values remain visible as raw JSON and are validated when explicitly edited. /schema exports strings without a closed enum; SDK generation uses string[] with null for nullable fields. Refresh after interface changes with asm schema pull and asm generate. + +Use the same option list for radio-style scenarios. Ordinary JSON uses a text editor with client/server validation. Text with interface:url accepts absolute HTTP(S) URLs without credentials; a compact button opens valid links in a new tab, including read-only views. + + + +### Editor and table + +Click a row to open a side editor at /items/<collection>/<id>. Without update access it is read-only. Creation uses the same dialog. Selecting rows reveals a floating action bar. + +Tables support pagination, sorting, column order/visibility/resize, search, filters, and views. Search defaults to relevance: exact match, whole-word prefix, whole word within a label, then substring. Exact primary keys rank highest. Primary search fields win equal matches over secondary fields. Automatic priority uses record-label/template fields, falling back to the first text field; field/relation editors can override it. Matching is literal and case-insensitive, without typo correction or morphology. + +Resize by dragging a header's right edge; double-click resets. Keyboard separators support Left/Right (16 px), Home/End (80/1200 px), Delete/Backspace (reset), and Escape (cancel drag). Widths save to personal preferences after pointer release, survive reload, and join named-view definitions. Hiding/reordering preserves widths; removing fields or read access removes their settings. Record-edit permission is unnecessary. + +GET/PATCH /users/me/table-preferences/:collection and table-view columns accept optional widths, for example: + +```json +{ "order": ["id", "title"], "hidden": [], "widths": { "title": 400 } } +``` + +Widths are integer CSS pixels from 80–1200 for accessible fields. Empty maps reset all widths; missing field keys use defaults. Old definitions without widths remain valid. + +Search participation and indexing are separate. Configured relationship search traverses one level, uses related labels, and does not duplicate source rows. Hidden/sensitive fields contribute neither matches nor ranking. Relations with conditional row grants are excluded for now. + +PostgreSQL ranks before pagination; ties use the selected column and primary key. Header clicks select field ordering; the relevance switch restores ranking. Personal views retain the mode. Global search ranks within each collection, not across collections. An index does not guarantee a suitable plan at every scale: inspect EXPLAIN on your workload. + +HTTP/SDK order accepts relevance or field. Search with q and no explicit sort/direction uses relevance; explicit sorting preserves field order. Order=relevance with sort uses that field for ties. Without search text, effective order is always field and appears in page.order. + +Related-record tables without explicit columns show key, label field, and up to two accessible M2O context fields, such as a category's marketplace. A backlink to the current card's collection is omitted. Hidden/sensitive fields are excluded; user-selected columns take precedence. Empty labels stay empty rather than borrowing another field's title. M2O labels use normal related-record permissions. + +Filters combine AND/OR, including permitted related fields. Closed fields cannot become filtering/sorting channels. HTTP pages are limited to 100; total is a string. + + + +## Relationships and saving + +M:1, inverse 1:M, and M:N with a junction collection are supported. Forms select existing records or create new ones. Every affected collection/field is checked separately. Moving between parents requires the corresponding relation update. + +POST /items/:collection/commit saves the root and related changes in one transaction. Failure also rolls back history and transactional hooks. Drafts permit 100 changes and depth 5. Sequential ordinary HTTP requests do not share a transaction. + + + +## Field rules and dependent forms + +The side editor's Rules tab controls hidden, read-only, conditional-required, and M2O-copy behavior. Rules.hidden changes forms only; policies still control API access. Rules.readonly rejects explicit writes through the normal writer, including superusers. Create defaults apply without manually writing the read-only field. + +Example presentation for note: ```json { @@ -303,22 +174,13 @@ superuser. Default при создании применяется без руч } ``` -Условие использует другие скалярные поля этой коллекции: `eq`, `ne`, `empty`, -`notEmpty`, объединение `all`/`any`, до 12 правил. `false` и `0` не пусты. -Core проверяет итоговую запись с defaults и прежними значениями при PATCH. -Обязательное значение — не NULL и не пустая/пробельная строка. Необходимые -скрытые поля раскрываются при проверке формы; серверная ошибка сохраняет черновик. +Conditions use other scalar fields with eq, ne, empty, notEmpty and all/any, up to 12 rules. False/zero are not empty. Core checks the final record, including defaults and previous PATCH values. Required values cannot be NULL or empty/whitespace strings. Form validation reveals required hidden fields; server errors preserve the draft. -`rules.computed: { relation: "project", field: "client" }` копирует `project.client` -при сохранении. Копируются скалярные поля. Типы полей должны совпадать, а для FK — ещё и целевая коллекция. -Копирование ограничено одной прямой M2O; цепочки, циклы и произвольные выражения -отклоняются. Явная запись computed-поля запрещена. Пользователю нужны права -чтения источника и записи изменяемого результата. Отсутствующий источник связи -даёт NULL с обычной проверкой nullable/required. Это вычисление при сохранении, -не динамическая колонка: изменение исходной записи само по себе не обновляет -все зависимые записи. Чувствительный источник разрешён только для чувствительного результата. +`rules.computed: { relation: "project", field: "client" }` copies project.client on save. Source/target scalar types must match, including target collection for foreign keys. Only one direct M2O is supported; chains, cycles, and arbitrary expressions are rejected. Explicit computed-field writes are forbidden. Users need source-read and result-write access. Missing relation sources yield NULL with normal nullable/required checks. -M2O-поле может задать `relationFilter`, например выбор города по региону формы: +This computes on save, not dynamically: editing a source does not update every dependent record. Sensitive sources require sensitive results. + +A relationFilter can restrict M2O candidates, such as cities by the form's region: ```json { @@ -340,61 +202,50 @@ M2O-поле может задать `relationFilter`, например выбо } ``` -Список кандидатов использует обычные фильтры и права строк/полей. До заполнения -зависимости выбор закрыт. Изменение родителя в форме сбрасывает зависимые ссылки, -включая следующий уровень. Core проверяет новое значение при создании и при -изменении самой связи либо её зависимости. Несоответствующая связь отклоняется; -сервер сам не выбирает и не очищает её. Старый архивный выбор разрешено сохранить -при редактировании другого поля. Литералы скалярные или массив скаляров, зависимости -— скалярные поля формы. AND/OR ограничены 30 узлами и глубиной 3. Связанный путь -фильтра — один переход; условия прав строк при таком переходе по-прежнему -отклоняются безопасно. Условие с недоступной зависимостью не раскрывается в каталоге. +Candidates use ordinary filters and row/field permissions. Selection stays disabled until dependencies are filled. Changing a parent clears dependent form references, including the next level. Core validates on create and when the relation or its dependency changes. Invalid links are rejected; the server never silently selects/clears them. An old archived choice may remain when editing an unrelated field. + +Literals are scalars or scalar arrays; dependencies are scalar form fields. AND/OR supports 30 nodes and depth 3. Related filter paths permit one hop; row conditions along that hop still fail safely. Conditions with inaccessible dependencies are omitted from the catalog. + +These checks apply to HTTP, SDK, Kit, bulk, and nested commit. One invalid row rolls back the transaction. Direct SQL bypasses the writer. + +Record-label templates support two M2O hops, such as {{project.client.name}} · {{title}}. The catalog suggests valid fields. Each hop checks read access, including row rules; unavailable values fall back to the normal label/key. Resolution uses batched queries. To-many paths and template expressions are unsupported. + +Inverse 1:M/M:N panels can be placed in a form section/tab for existing records without duplication below the form. On new records, placed panels show a hint until first save. + + + +## Overwrite protection -Эти проверки общие для HTTP, SDK, Kit, массового изменения и вложенного commit. -Ошибочный ряд откатывает транзакцию целиком. Прямой SQL не проходит через writer. +The side editor remembers original values and sends expectedValues for changed fields only. Core compares them under a row lock in the same transaction, including nested records, foreign keys, and junction attributes. It compares actual values rather than timestamps, detecting changes from ordinary PATCH, plugins, or direct SQL at the next protected save. -Шаблоны подписей поддерживают до двух M2O-переходов, например -{{project.client.name}} · {{title}}. Каталог предлагает допустимые поля. -Чтение проверяется на каждом переходе, включая условные права строки; при -недоступном поле/записи используется обычная подпись или ключ. Подстановка -использует пакетные запросы. To-many пути и вычисления внутри шаблона не поддержаны. -Панели обратных 1:M и M:N связей можно разместить в секции или вкладке конструктора -формы существующей записи; повторной панели внизу не будет. В новой записи -размещённая панель показывает подсказку до первого сохранения. +Edits to different fields can save independently. If another save already wrote the identical desired value, the repeat succeeds. A mismatch returns 409 ITEM_CHANGED and rolls back the entire commit. The UI retains the draft, fetches accessible current values, and offers Your value / Current value. Users continue editing and save explicitly; the next write checks its baseline again. Read failures or access changes keep the draft open. -## Защита от перезаписи +SDK and Kit opt in through items.commit expectedValues for each existing modified record. Omission remains compatible. Ordinary update, bulk changes, and SQL do not require baselines. This is changed-field comparison, not a version counter or whole-card lock. Returning a field to its original value is not a conflict. JSON, rich text, and arrays compare as whole values. Relationships use explicit attach/detach rather than whole-list snapshots. Drafts are in-memory and do not survive reload. -Боковой редактор запоминает исходные значения и передаёт `expectedValues` только для изменяемых полей. Core проверяет их под блокировкой строки в той же транзакции. Для вложенных записей, FK и атрибутов промежуточной связи применяется такая же проверка. Системные даты не нужны: сравниваются фактические значения, поэтому изменение через обычный PATCH, плагин или прямой SQL тоже обнаруживается при следующем защищённом сохранении. +Conflict errors do not expose current values. Every compared field requires read and write access, including conditional grants. -Изменения разных полей сохраняются независимо. Если другое сохранение уже записало точно такое же значение, повтор считается успешным. Несовпадение одного изменяемого поля возвращает `409 ITEM_CHANGED` и откатывает весь commit. UI оставляет черновик в панели, загружает доступные актуальные значения и предлагает выбрать «Ваше значение / Актуальное». После выбора пользователь продолжает редактирование и сохраняет явно; повторная запись снова проверяет исходные значения. При ошибке чтения или изменении доступа черновик остаётся в открытом окне. + -SDK и Kit могут включить эту проверку через `items.commit` с `expectedValues` для каждой существующей изменяемой записи. Пропуск параметра совместим с прежними клиентами. Обычные `update`, массовые изменения и прямой SQL сами по себе не требуют исходных значений. Это проверка содержимого изменяемых полей, не счётчик версий или блокировка всей карточки. Возврат поля к исходному значению не считается конфликтом. JSON, rich text и массивы сравниваются целиком; состав связей меняется явными attach/detach, без проверки снимка всего списка. Черновик хранится в памяти панели и не переживает перезагрузку страницы. +## Who is here -Проверка не раскрывает актуальные значения в ошибке и требует права чтения и записи каждого сравниваемого поля, включая условные grants. +Existing record cards show compact avatars for participants viewing that record. Hover/focus shows names; multiple windows of one person share one avatar. Three avatars plus an overflow count are visible. Unsaved new records have no presence. -## Кто сейчас здесь +The legacy presence UI polls every five seconds. Profile name/avatar are used, with Participant as fallback. Core stores 30-second PostgreSQL leases and removes them on leaving; expiry covers offline closes. Revoked/expired sessions and disabled users are excluded on the next list read. Presence failures hide avatars without blocking editing. See [real-time collaboration](./realtime.md) for SSE presence and field-lock behavior. -В существующей карточке записи показываются компактные аватарки участников, открывших эту же запись. Наведение или фокус раскрывает имя; один пользователь с несколькими окнами получает одну аватарку. Видны три аватарки и счётчик остальных. Новой несохранённой записи присутствие не назначается. +Every update checks current row-read access. Other record participants see names, avatars, and window counts, not email, search/filter URLs, sessions, or record values. Presence does not imply active editing. Plugin collections use ordinary record presence. -UI обновляет список раз в 5 секунд. Имя и аватар берутся из профиля; без заданного имени отображается «Участник». Core хранит отметки в PostgreSQL на 30 секунд и удаляет их при уходе; закрытие окна без сети покрывает срок действия. Сессии, которые отозваны или истекли, и отключённые пользователи исключаются сразу при следующем чтении списка. Ошибка присутствия скрывает аватарки и не мешает редактированию. + -Каждое обновление проверяет актуальные права чтения конкретной записи. Имена, аватары и число окон видны другим участникам этой записи; email, URL с поиском/фильтрами, сессии и значения записи не передаются. Присутствие не означает, что человек редактирует поле. Плагинные коллекции используют обычное присутствие записей. +## History and deletion -## История и удаление +Item events record actor, time, and changed values. -История item events хранит actor, время и изменения значений. +Text/email with presentation.sensitive:true is masked in read-only forms/tables, edited as a password, and excluded from record labels. History stores [REDACTED] in before/after for create/update/delete, bulk, and nested commits. Enabling this setting irreversibly scrubs older events for the field in the same transaction; disabling it does not restore them. Sensitive defaults are forbidden. -Текст/email с `presentation.sensitive: true` маскируется в форме просмотра и -таблице, редактируется как password и не используется в подписи записи. -Вместо значения в `before`/`after` истории сохраняется `[REDACTED]`, включая -create/update/delete, bulk и nested commit. Включение настройки в той же -транзакции безвозвратно очищает прежние события этого поля; выключение не -восстанавливает их. Default чувствительного поля запрещён. Это не шифрование -и не write-only API: разрешённый read по-прежнему возвращает живое значение, -его ограничивают права поля. Копии БД, прямой SQL и внешние логи проверяет оператор. +This is neither encryption nor a write-only API: authorized reads still return live values, controlled by field permissions. Operators must handle database copies, direct SQL, and external logs. -Чтение истории проецируется на разрешённые поля; события, затрагивающие только закрытые поля, не раскрываются. История схемы и безопасный rollback поля с восстановлением данных — отдельная нерешённая задача. +History reads project permitted fields and omit events touching only closed fields. Schema history and safe field rollback with data restoration remain unimplemented. -Прямой SQL и каскады, исполняемые самой БД, не проходят через обычный writer и не обеспечивают такую же историю/hooks. Удаление коллекции/поля требует superuser и проверки влияния; backup остаётся обязанностью эксплуатации. +Direct SQL and database cascades bypass the ordinary writer and do not guarantee equivalent history/hooks. Deleting collections/fields requires a superuser and impact checks. Operators remain responsible for backups. -Источники: `apps/core/src/collections`, `apps/core/src/items`; контракты — `packages/contracts/src/items.d.ts`, SDK — [справочник](../reference/sdk-guide.md). +Sources: apps/core/src/collections and apps/core/src/items; contracts: packages/contracts/src/items.d.ts; [SDK reference](../reference/sdk-guide.md). diff --git a/docs/features/files.md b/docs/features/files.md index fdd03d9..5b6109a 100644 --- a/docs/features/files.md +++ b/docs/features/files.md @@ -1,44 +1,46 @@ -# Файлы и объектное хранилище + -Файл — отдельная сущность с метаданными в PostgreSQL и содержимым в настроенном S3/YC backend. Поля коллекций хранят ссылки на файлы; пользовательский доступ к содержимому проверяет Core. +# Files and object storage -| Операция | Текущий допуск | -| -------------------------------------- | ------------------------------------------------------------ | -| Список библиотеки, история файла | Человек + files/read (либо update); superuser | -| Загрузка, изменение, удаление | Человек + files/update; superuser | -| Метаданные/скачивание связанного файла | files/read, superuser либо read доступ к полю с этой ссылкой | -| Resolve списка ссылок | Только разрешённые для principal ссылки | +Files store metadata in PostgreSQL and content in the configured S3 or Yandex Cloud backend. Collection fields reference files; Core checks access to their content. -Скачивание по умолчанию — attachment/octet-stream; preview выставляет ограничительную CSP и nosniff. Ограничение загрузки — 25 MiB; загрузка буферизуется в памяти. Секреты хранилища не передаются клиенту. +| Operation | Required access | +| ------------------------------------- | ----------------------------------------------------------------------- | +| Library and file history | Human with files/read or files/update; superuser | +| Upload, edit, delete | Human with files/update; superuser | +| Metadata or download through a record | files/read, superuser, or read access to the field referencing the file | +| Resolve file references | Only references accessible to the principal | -Поддерживается Yandex IAM token provider, включая настройку федеративного обмена. Это инфраструктурная конфигурация Core; пользовательская федерация GitLab CI для входа в Asmblyr — другой механизм. +Downloads default to attachment/octet-stream. Previews use a restrictive CSP and `nosniff`. Uploads are buffered in memory and limited to 25 MiB. Storage credentials never reach the client. -Параметры хранилища задаются в [подключениях админки](./connections.md) либо в окружении Core по `apps/core/.env.example`. Env-конфигурация блокирует редактирование группы в админке. Bucket и IAM создаёт оператор своей установки. + -Для выбора новой файловой ссылки в поле нужен files/read и право записи этого поля. Уже сохранённую ссылку можно читать через разрешённую запись без доступа ко всей библиотеке. При отзыве files/read библиотека и выбор новых ссылок закрываются; интерфейс загрузки/редактирования отключён для files/read без update. +## Configuration -Библиотека общая для одной установки команды: files/read открывает все её файлы, а не только собственные загрузки. Отдельных прав на каждый объект пока нет. Workspaces не разделяют файлы на tenants. Локальные инструменты установки сохраняют согласованные PostgreSQL и S3 копии при остановленных авторах; [backup/restore](../development/operations.md) описан отдельно. +Configure storage under Connections in the admin UI or through environment variables. Environment configuration locks the corresponding settings group in the UI. The operator provides the bucket and IAM permissions. -## Публичная ссылка на один файл +Yandex IAM authentication, including a federated token exchange, is an infrastructure connection. It is separate from GitLab login federation. -Новые и существующие файлы имеют `visibility: "private"`. Человек с `files/update` -(либо superuser) может изменить видимость одного готового файла на `public` через -боковой редактор или `PATCH /files/:id`. После сохранения появляется кнопка -копирования ссылки. Неправильное значение видимости отклоняется. +A new file reference requires files/read and write access to its record field. A readable record can expose an existing reference without granting library access. Revoking files/read closes the library and picker; read-only access cannot upload or edit files. -`GET /public/files/:id/content` в Core и `/api/public/files/:id/content` в UI -возвращают байты опубликованного файла без сессии. `?preview=1` разрешает только -уже поддержанные растровые превью. Публичного списка и метаданных нет, приватные -маршруты сохраняют свои проверки доступа. Это явная публикация для любого -обладателя ссылки, а не персональное приглашение. Ссылка не содержит IAM/SDK -токенов, bucket остаётся приватным. +The library is shared by the installation, not restricted to the uploader. There are no per-file ACLs or workspace tenant boundaries. Stop writers when making consistent local backups. -При возврате к `private`, удалении либо состоянии не ready публичный маршрут -отвечает 404 до обращения к хранилищу. Ответы используют `no-store`, CSP и -`nosniff`; загрузка по умолчанию — attachment. Отзыв останавливает будущие -скачивания, но не отзывает уже полученные байты или начатый поток. -Прокси UI передаёт только путь и параметр preview, без cookies/Authorization. + -Открыто: квоты общего объёма, проверка вредоносного содержимого, преобразования/превью как сервис и reconciliation оставшихся объектов. PostgreSQL и S3 не образуют одну транзакцию. Приватность и резервирование внешнего bucket проверяет оператор конкретной установки. +## Public files -Источники: `apps/core/src/files`, `apps/core/src/files/storage`. ACL внешнего bucket проверяет оператор своей установки. +Files are private by default. A human with files/update, or a superuser, can make a ready file public in its side editor or through `PATCH /files/:id`. The UI offers Copy link after saving. Invalid visibility values are rejected. + +`GET /public/files/:id/content` in Core, exposed as `/api/public/files/:id/content`, serves public bytes without authentication. `preview=1` supports approved raster formats. There is no public listing or metadata endpoint. Private routes retain their access checks. + +Anyone with the link can download a public file. No IAM credentials or SDK tokens are needed, and the bucket itself remains private. Switching back to private, deleting the file, or changing it to a non-ready state returns 404 before storage access. Responses use `no-store`, CSP, `nosniff`, and attachment disposition by default. + +Revocation prevents future downloads; it cannot retract already downloaded bytes or a stream that has started. + + + +## Limits + +Quotas, malware scanning, a dedicated transformation service, and automatic storage reconciliation are not implemented. PostgreSQL and S3 do not share a transaction; operators remain responsible for backup consistency and bucket privacy. + +Sources: `apps/core/src/files/` and `apps/core/src/storage/`. diff --git a/docs/features/google-workspace.md b/docs/features/google-workspace.md index 14dddce..d21b557 100644 --- a/docs/features/google-workspace.md +++ b/docs/features/google-workspace.md @@ -1,126 +1,65 @@ -# Google Workspace в ассистенте - -Опциональный пакет `@asmblyr-collaborative/plugin-google-workspace` добавляет поиск файлов -Google Drive, чтение текстовых файлов и Google Docs, структуру Google Sheets -и чтение диапазонов. Запись готовится как предложение с конкретными данными; -пользователь проверяет его в правой панели и нажимает «Подтвердить изменение». - -## Настройка установки - -В Google Cloud включите Drive API и Sheets API. Создайте отдельный OAuth web client -для ассистента с External audience. Добавьте redirect URI -`https://admin.example.com/connections/google/callback`; для локальной разработки -допустим `http://localhost:3000/connections/google/callback`. - -В `/admin/settings/integrations` откройте Google Workspace, задайте client ID, -redirect URI и write-only client secret, затем включите подключение. -На сервере должен быть настроен [ключ local или Yandex KMS](./settings.md). -Проверка конфигурации проверяет параметры и шифрование. Корректность client secret -и разрешённого callback проверяется при реальном OAuth-подключении. - -Альтернатива — `GOOGLE_WORKSPACE_ENABLED=true`, `GOOGLE_WORKSPACE_CLIENT_ID`, -`GOOGLE_WORKSPACE_CLIENT_SECRET`, `GOOGLE_WORKSPACE_REDIRECT_URI` в конфигурации Core. -Наличие любого из этих env-параметров делает всю группу readonly в админке. -Пакет включается в корневом `asmblyr.plugins`, capability `connections.google` -подтверждается в `asmblyr.pluginPermissions`. Исключение пакета убирает инструменты, -сохраняя личные подключения до явного отключения. - -Запрашиваются `openid`, `email`, `profile`, полный `drive` и `spreadsheets`. -Это доступ к разрешённым пользователю файлам, включая общие диски согласно их ACL. -В режиме Google Testing подключаются только тестовые пользователи; refresh tokens -для этих scopes обычно истекают через семь дней. Для публичного приложения -нужны проверка Google и опубликованная политика конфиденциальности; администратор -Workspace может запретить стороннее приложение. См. [OAuth Google](https://developers.google.com/identity/protocols/oauth2) -и [scopes Drive](https://developers.google.com/workspace/drive/api/guides/api-specific-auth). - -## Личное подключение - -Пользователь открывает значок подключений в ассистенте или -`/settings?tab=applications`, нажимает «Подключить Google» и предоставляет согласие -в Google. Подключение принадлежит активному человеку и не доступно сервисным -аккаунтам, другим пользователям или администратору через интерфейс. Вход в Asmblyr -и Google API-подключение — разные потоки: email не связывает профили автоматически. - -OAuth использует authorization code, PKCE S256, state, nonce, проверку issuer, -ID token и subject через `openid-client`. Поток привязан к пользователю, конфигурации -клиента и HttpOnly browser cookie, действует десять минут и принимается один раз. -Callback требует действующую сессию Asmblyr. Отключение во время callback отменяет -поток. Токены и PKCE proof хранятся зашифрованными в PostgreSQL, с отдельным AAD -на запись; смена local/KMS пересчитывает и эти секреты в одной транзакции. -При недоступности KMS нет автоматического перехода на local. - -Токены обновляются на сервере; `invalid_grant`, смена OAuth client или отзыв доступа -требуют подключения заново. Токены не передаются модели, плагину или браузерному -JavaScript. Отключение удаляет локальные секреты и предложения, затем пытается -отозвать Google grant. При неудаче UI просит также отозвать его в аккаунте Google. -Недоступность шифрования отражается флагом `unavailable` в личном статусе; -метаданные аккаунта и локальное отключение остаются доступны. -Уже выполняющийся внешний запрос нельзя гарантированно отменить. - -## Инструменты и ограничения - -| Инструмент | Поведение | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `find-files` | Поиск по имени, 25 файлов на страницу, source URLs и следующий page token | -| `read-text` | Текстовые файлы и plaintext export Google Docs, до 30 КБ с явной отметкой обрезки | -| `describe-sheet` | Название таблицы, имена и размеры вкладок | -| `read-cells` | Явный A1 прямоугольник до 500 ячеек, ответ до 36 КБ | -| `propose-write` | Подготовка создания/замены текстового файла, переименования, перемещения в корзину, создания таблицы, замены ячеек или добавления строк | - -Значения Sheets записываются в режиме RAW: строки не становятся формулами. -Размер прямоугольника должен совпадать с массивом значений. `append_cells` -добавляет строки после логической таблицы, найденной Google в указанном диапазоне, -и сдвигает последующие строки; это отображается в подтверждении. -Редактирование содержимого нативного Google Docs, произвольные бинарные документы, -OCR, Slides, файловые вложения и постоянная индексация в этот пакет не входят. - -Инструменты доступны только при активном личном подключении, одобренной capability -и включённом доступе к данным в «Подключениях ассистента». Контекст страницы -включается отдельно и для Google не обязателен. Подключение позволяет пользоваться чатом без -прав на коллекции, но не добавляет права чтения данных Core. Коллекция, выключенная -из MCP, остаётся закрытой; личные инструменты Google продолжают работать. -Документы и ячейки являются недоверенными данными. Запрошенные фрагменты передаются -настроенному AI-провайдеру; сам grant не запускает обход всех файлов. - -Запросы Drive используют `www.googleapis.com/drive/v3`, а Sheets — -`sheets.googleapis.com/v4`, включая создание таблиц и запись ячеек. -Ошибки инструментов различают необходимость переподключения, отказ Google, -отсутствующий файл, ограничение частоты запросов, неверные параметры и сетевой сбой. -Сообщения берутся из фиксированного списка: содержимое ошибок Google и токены -модели не передаются. Сбой запроса не доказывает отсутствие доступа к файлу. - -Предложение действительно двадцать минут, до 32 ожидающих на пользователя. -Его данные зашифрованы, подтверждение привязано к тому же подключению и владельцу. -Модель не получает инструмент подтверждения; HTTP подтверждает сохранённый payload, -не принимая новое содержимое от браузера. Перед записью проверяются версия файла -или значения диапазона. Между этой проверкой и Google write остаётся небольшое -окно: API не предоставляет общей транзакции с PostgreSQL. - -Подтверждение принимается один раз. Сбой после отправки Google write даёт -`uncertain`; нужно проверить файл перед новым предложением. Перезапуск во время -записи оставляет `executing`, повтор недоступен. Журнал содержит только провайдера, -вид операции и результат. По истечении TTL предложения и flow удаляются очисткой. - -Карточка показывает отдельные исходы: «Выполнено», «Отклонено», «Не выполнено» -и «Проверьте результат». Последний используется для `uncertain` и оставшегося -`executing`: он не утверждает, что Google не изменил файл. У изменившейся цели -показывается причина отказа, предлагается подготовить новое изменение. -Кнопка «Посмотреть результат» остаётся доступной после завершения; в правой панели -можно просмотреть исходное предложение, краткий итог и обновить статус. -«Обновить статус» выполняет только GET, повторной записи нет. При потере ответа -подтверждения UI сверяет состояние с сервером и сохраняет запрет второй отправки. - -Для успешной операции Core сохраняет зашифрованный ограниченный итог: ссылку на -файл/таблицу, число изменённых ячеек/строк и диапазон, если Google их вернул. -Произвольный ответ провайдера не сохраняется; ссылки строятся из ID файлов на -доменах Google. У существующей цели ссылка доступна и при неизвестном результате. -Созданный файл получает ссылку только после подтверждённого ответа с его ID. -Отклонение сохраняет статус `cancelled` и не отправляет запрос записи в Google. -Просмотр предложения и итога ограничен прежними двадцатью минутами, владельцем -и тем же подключением. После истечения или отключения аккаунта серверный просмотр -недоступен. Восстановленная история чата не активирует старые карточки. - -Контракты Core `/connections/google`, OAuth start/callback и owner-bound -`/connections/google/writes/:id` описаны в [HTTP API](../reference/http.md). -Плагин использует обычные H3 file routes и `defineModelContext`; схемы генерирует Kit. -Проверки с временной БД и локальным Google adapter: `node scripts/test.mjs core-connections`. + + +# Google Workspace in the assistant + +The optional `@asmblyr-collaborative/plugin-google-workspace` package adds Google Drive file search, text and Google Docs reading, Google Sheets structure, and range reads. Writes are prepared as proposals with exact data; the user reviews them in a side panel and selects Confirm change. + + + +## Installation configuration + +Enable Drive API and Sheets API in Google Cloud. Create a separate OAuth web client for the assistant with an External audience. Register `https://admin.example.com/connections/google/callback`; local development may use `http://localhost:3000/connections/google/callback`. + +Open Google Workspace at `/admin/settings/integrations`, enter client ID, redirect URI, and a write-only client secret, then enable it. Configure a [local or Yandex KMS key](./connections.md) on the server. Configuration testing checks parameters and encryption; a real OAuth connection validates the client secret and registered callback. + +Alternatively set `GOOGLE_WORKSPACE_ENABLED=true`, `GOOGLE_WORKSPACE_CLIENT_ID`, `GOOGLE_WORKSPACE_CLIENT_SECRET`, and `GOOGLE_WORKSPACE_REDIRECT_URI` in Core. Any of these environment settings locks the entire admin group. Enable the package in root `asmblyr.plugins` and approve `connections.google` in `asmblyr.pluginPermissions`. Removing the package removes its tools but preserves personal connections until explicitly disconnected. + +Requested scopes are `openid`, `email`, `profile`, full `drive`, and `spreadsheets`. Access follows the user's file permissions, including shared-drive ACLs. Google Testing permits only listed testers; refresh tokens for these scopes normally expire after seven days. Public applications need Google verification and a published privacy policy. Workspace administrators may block third-party apps. See [Google OAuth](https://developers.google.com/identity/protocols/oauth2) and [Drive scopes](https://developers.google.com/workspace/drive/api/guides/api-specific-auth). + + + +## Personal connection + +Open Connections in the assistant or `/settings?tab=applications`, select Connect Google, and consent in Google. The connection belongs to the active human. Service accounts, other users, and administrators cannot use it through the interface. Collaborative sign-in and Google API connection are separate flows; email never automatically links profiles. + +OAuth uses authorization code, PKCE S256, state, nonce, issuer, ID-token, and subject validation through `openid-client`. The flow binds to the user, client configuration, and HttpOnly browser cookie, expires after ten minutes, and is single-use. Callback requires an active Collaborative session. Disabling the connection during callback cancels the flow. + +Tokens and PKCE proof are encrypted in PostgreSQL with per-record AAD. Switching local/KMS re-encrypts these secrets in the same transaction. KMS failures never automatically fall back to local encryption. + +Tokens refresh on the server. `invalid_grant`, client changes, or revoked access require reconnecting. Tokens never reach the model, plugin, or browser JavaScript. Disconnecting deletes local secrets/proposals and then attempts to revoke the Google grant. If revocation fails, the UI asks the user to revoke it in Google too. Encryption failure sets personal status `unavailable`; account metadata and local disconnect remain available. An external request already in flight cannot be reliably cancelled. + + + +## Tools and limits + +| Tool | Behavior | +| ---------------- | ------------------------------------------------------------------------------------------------------------ | +| `find-files` | Name search, 25 files per page, source URLs, next-page token | +| `read-text` | Text files and plaintext Google Docs export, up to 30 KB with explicit truncation | +| `describe-sheet` | Spreadsheet title, tab names and dimensions | +| `read-cells` | Explicit A1 rectangle, up to 500 cells and 36 KB | +| `propose-write` | Propose text-file creation/replacement, rename, trash, spreadsheet creation, cell replacement, or row append | + +Sheets values use RAW mode, so strings do not become formulas. Rectangle dimensions must match the values array. `append_cells` adds rows after the logical table Google finds within the given range and shifts later rows; confirmation explains this. + +Native Google Docs content editing, arbitrary binary documents, OCR, Slides, file attachments, and continuous indexing are outside this package. + +Tools require an active personal connection, approved capability, and enabled data access in Assistant connections. Page context is separate and unnecessary for Google. A connection allows chat without collection grants but adds no Core data access. Disabling a collection in MCP keeps it closed while personal Google tools remain available. + +Documents and cells are untrusted data. Requested excerpts are sent to the configured AI provider. Granting access does not initiate a crawl of all files. + +Drive calls use `www.googleapis.com/drive/v3`; Sheets calls, including creation and writes, use `sheets.googleapis.com/v4`. Tool errors distinguish reconnect requirements, Google denial, missing files, rate limits, invalid parameters, and network failure. Fixed messages prevent Google error contents and tokens reaching the model. A failed request does not prove lack of file access. + +Proposals expire after twenty minutes, with up to 32 pending per user. Payloads are encrypted; confirmation binds to the same owner and connection. The model has no confirmation tool. HTTP confirmation executes the saved payload and accepts no replacement content from the browser. File versions or range values are checked before writing, but a small race remains between that check and Google's write because PostgreSQL and Google share no transaction. + +Confirmation is single-use. Failure after sending the write becomes `uncertain`: inspect the file before proposing again. Restarting during a write leaves `executing`, without retry. Audit records only provider, operation kind, and outcome. Cleanup removes expired proposals and flows. + +Cards show Completed, Rejected, Failed, or Check result. The last covers uncertain and surviving executing states and does not claim Google left the file unchanged. Changed targets show a rejection reason and suggest a new proposal. + +View result remains available after completion. The side panel shows the original proposal, bounded outcome, and status refresh. Refresh performs only GET and never repeats a write. If a confirmation response is lost, the UI reconciles server status while preventing a second submission. + +Successful operations store an encrypted bounded result: file/spreadsheet link, changed cell/row counts, and range when supplied by Google. Arbitrary provider responses are not stored. Links are built from IDs on Google domains. Existing targets keep their link even when the outcome is unknown; new files receive one only after a confirmed response with their ID. + +Rejection records `cancelled` without sending a Google write. Proposal/result viewing remains limited to the original twenty-minute lifetime, owner, and connection. Expiry or disconnection disables server-side viewing. Restored chat history does not activate old cards. + +Core `/connections/google`, OAuth start/callback, and owner-bound `/connections/google/writes/:id` contracts appear in [HTTP API](../reference/http.md). The plugin uses ordinary H3 file routes and `defineModelContext`; Kit generates schemas. Disposable-database tests with a local Google adapter: `node scripts/test.mjs core-connections`. diff --git a/docs/features/identity.md b/docs/features/identity.md index 7b3ede3..10bbccd 100644 --- a/docs/features/identity.md +++ b/docs/features/identity.md @@ -1,104 +1,87 @@ -# Пользователи и вход + -Первый superuser создаётся в UI с setup secret. Следующие пользователи — через приглашение либо настроенный SSO flow. Email уникален; одинаковый email не означает разрешение автоматически объединить две внешние личности. +# Users and sign-in -## Профиль и дополнительные поля +Create the first superuser in the UI with the setup secret. Later users join through invitations or configured SSO. Email is unique, but matching email addresses do not authorize automatic linking of external identities. -Базовый профиль включает почту, отображаемое имя, необязательные имя и фамилию, описание «О себе», аватар и даты создания, изменения профиля, последнего входа и активности. UI выбирает отображаемое имя, затем имя с фамилией, затем почту. Существующее отображаемое имя сохраняется. Профиль не позволяет изменять почту, статус, superuser, credentials или системные даты. + -Аватар загружается кнопкой **Загрузить аватар** через `POST /users/me/avatar` в настроенное файловое хранилище, включая S3: PNG, JPEG, GIF или WebP до 2 MiB. Внешнюю ссылку на изображение в форме вводить не нужно. Право на библиотеку файлов для собственного аватара не требуется. Загрузка создаёт приватный файл; `PATCH /users/me` с `avatarId` прикрепляет его. При редактировании другого пользователя загруженный файл прикрепляется через `PATCH /users/:id/profile`, который требует `users/update`. Несохранённые загрузки остаются в библиотеке и могут быть удалены администратором. Собственный аватар видит его владелец; чужой — пользователь с доступом к разделу пользователей либо обычным правом на файл. Прикреплённый файл удалить нельзя. Отдельный `pictureUrl` остаётся в API для совместимости и публичного OAuth profile; форма его не редактирует, а приватный аватар туда не экспортируется. +## Profile and extensions -В настройках внешнего вида сохраняется IANA-часовой пояс пользователя, например `Asia/Yekaterinburg`. `null` означает часовой пояс устройства. Он применяется к датам профиля и сессий; значения в БД и API остаются UTC timestamps. `lastLoginAt` обновляется при выдаче новой сессии, а refresh его не продлевает. `lastActiveAt` приблизительный: успешные запросы с пользовательским access token обновляют его не чаще раза в пять минут; сервисные ключи учитываются отдельно. +The base profile includes email, display name, optional first/last name, About text, avatar, and creation, profile-update, last-login, and activity timestamps. Display-name fallback is display name → first/last name → email. Existing display names are preserved. Profile editing cannot change email, status, superuser, credentials, or system timestamps. -Должность, департамент, организация и руководитель не встроены в ядро. Для таких данных superuser создаёт обычную коллекцию с UUID-ключом и выбирает её в **Настройки → Пользователи → Расширение профиля**. Можно добавить любые поддержанные поля, M2O/O2M/M2M-связи и настроить форму стандартным редактором коллекций. Рекомендуется начать с пустой коллекции. У существующих записей ID должны совпадать с ID пользователей; один пользователь имеет одну запись профиля. Служебный FK защищает привязку к пользователю; переименование коллекции сохраняет настройку. +Upload avatar uses `POST /users/me/avatar` with configured file storage, including S3: PNG, JPEG, GIF, or WebP up to 2 MiB. No external image URL or file-library permission is needed for your own avatar. Upload creates a private file; `PATCH /users/me` with `avatarId` attaches it. Another user's avatar is attached through `PATCH /users/:id/profile`, requiring users/update. -Дополнительные данные открываются из собственного профиля и бокового окна пользователя. Первое сохранение через `PATCH /users/me/extension` либо `PATCH /users/:id/extension` создаёт запись с ID пользователя, последующие — изменяют её. Обычный `POST /items/:collection` для коллекции профиля не создаёт профили со случайными ID. Уже созданные записи и связи редактируются стандартным редактором записей. Отключение расширения сохраняет данные и FK; перед удалением коллекции расширение нужно отключить. +Unsaved uploads remain in the library for administrator cleanup. Owners can view their own avatar; viewing another requires users-section access or ordinary file access. An attached avatar cannot be deleted. `pictureUrl` remains in the API for compatibility and public OAuth profiles; the form does not edit it and private avatars are not exported there. -Настройка расширения сама по себе не выдаёт права. Пользователю необходимы обычные права коллекции: `read`, `create` для первого заполнения и `update` для изменений. Для личных данных задайте row filter: поле `id` (либо имя вашего UUID-ключа), оператор «Равно», параметр «Текущий пользователь» для всех этих действий. Права на связанные коллекции и файлы проверяются отдельно. Управление чужим профилем дополнительно требует `users/read` или `users/update`; скрытые поля и строки не раскрываются. Валидация, вычисляемые поля, audit и mutation hooks совпадают с обычными коллекциями. Схема расширения входит в `GET /schema` и стандартную генерацию CLI — [пример SDK](../reference/sdk-guide.md#профиль-пользователя). +Appearance preferences include an IANA time zone, such as `Asia/Yekaterinburg`; `null` uses the device time zone. It formats profile and session dates while database/API timestamps stay UTC. `lastLoginAt` changes on new-session creation, not refresh. `lastActiveAt` is approximate: successful user access-token requests update it at most every five minutes. Service-key activity is tracked separately. -## Настраиваемая информация профиля +Job title, department, organization, and manager are not built into Core. A superuser can create an ordinary UUID-key collection and select it under Settings → Users → Profile extension. Use supported fields, M2O/O2M/M2M relationships, and the standard form editor. An empty collection is recommended. Existing row IDs must match user IDs; each user has one profile row. An internal FK protects the association, and collection renaming preserves the setting. -В **Настройки → Пользователи → Вид профиля** superuser настраивает отдельный блок -для чтения в личном профиле и карточке пользователя. До 12 строк: подпись, источник, -порядок и галочка **Показывать владельцу профиля** (по умолчанию выключена). -Пустой заголовок использует переведённое «Дополнительная информация». +Extra data opens from the personal profile and user side panel. The first `PATCH /users/me/extension` or `PATCH /users/:id/extension` creates a row with the user's ID; later calls update it. Ordinary `POST /items/:collection` cannot create profiles with random IDs. Existing rows and relationships use the standard record editor. Disabling the extension preserves data and its FK; disable it before deleting the collection. -Источники — дополнительные поля системной коллекции `users`. Можно пройти до двух -одиночных M2O-связей и выбрать конечное значение (например, отдел → название или -отдел → руководитель). Поддержаны текст, email, числа, флаги, даты, теги и ссылка на -пользователя, которая раскрывает только отображаемое имя с fallback на ФИО/почту. -Фотографии и произвольные поля связанного пользователя не передаются. Системные -колонки, sensitive-поля, файлы, произвольный JSON, связи ко многим и коллекции -плагинов не доступны как источники. Настроенные значения читаются из исходных -записей при загрузке профиля, без копирования. +Selecting an extension grants no permissions. Users need ordinary collection read, create for initial completion, and update for changes. For personal data, apply a row condition on `id` (or the chosen UUID key): Equals → Current user, for each action. Related collections and files have separate checks. Another user's profile additionally requires users/read or users/update; hidden rows and fields stay hidden. -Это отдельное явное разрешение на проекцию: управляющие с `users/read` видят -настроенные строки; владелец — только строки с `selfVisible`. Для этих точечных -значений не требуются обычные права коллекций. Доступ к справочникам, другим -профилям и редактированию при этом не выдаётся. Выбирайте только сведения, -предназначенные для такой аудитории. Настраивать проекцию может только superuser. +Validation, computed fields, audit, and mutation hooks match ordinary collections. The extension schema is included in `GET /schema` and CLI generation; see the [SDK guide](../reference/sdk-guide.md). -Удалённый источник, изменение цели связи, пометка sensitive или удаление и -повторное создание столбца скрывают затронутую строку до повторной настройки. -Имена полей и физическая идентичность столбцов входят в проверку привязки; -переименование источников и восстановление БД с другими OID требуют повторного -сохранения конфигурации. Отсутствующее значение показывается как «Не указано». + -API: `GET|PUT /users/profile-display`, `GET /users/profile-display/sources`, -`GET /users/me/profile-display`, `GET /users/:id/profile-display`. -Проверки включены в `node scripts/test.mjs core-profile`. +## Configurable profile information -## Пароль и сессии +A superuser configures a separate read-only block under Settings → Users → Profile display. It supports up to 12 rows with label, source, order, and Show to profile owner, off by default. An empty title uses the translated Additional information label. -Пароли хранятся как Argon2id hash отдельно от пользователя. Access/refresh — случайные непрозрачные токены; в БД только SHA-256 hashes. Access живёт 15 минут, сессия до 30 дней; refresh вращается. Повтор использованного refresh отзывает сессию. +Sources are custom fields of the system `users` collection. Paths may traverse up to two single M2O relations to a final value, such as department → name or department → manager. Supported values include text, email, numbers, flags, dates, tags, and user references exposing only a display name with name/email fallback. Photos and arbitrary fields of linked users are not returned. -Для браузера Core выдаёт отдельную непрозрачную HttpOnly cookie `asmblyr_session` -(SameSite=Lax, Secure при HTTPS). В БД хранится только SHA-256 hash. Сессия действует -до 30 дней без продления срока; каждый запрос проверяет срок, отзыв и статус -пользователя. Refresh-токены браузеру не выдаются: параллельные вкладки и реплики -используют одну проверяемую в БД сессию без гонок refresh. Профиль позволяет менять -собственные данные/пароль, смотреть сессии и отзывать их. +System columns, sensitive fields, files, arbitrary JSON, to-many relations, and plugin collections cannot be sources. Values are read from their source records when loading the profile, without copying. -Вход браузера: `/api/auth/browser/login`, `/api/auth/browser/invitations/claim`, -`/api/auth/browser/passkeys/options` и `/api/auth/browser/passkeys/login`. Ответ -успешного входа — `{ok:true}`, секрет передаётся только через Set-Cookie. Выход: -`POST /api/auth/browser/logout`. Записывающие запросы с cookie и все browser auth -операции требуют точного `Origin`, равного `AUTH_UI_URL`; чужой или отсутствующий -Origin отклоняется. Явный Authorization имеет приоритет над cookie. +This is an explicit, separate projection permission: managers with users/read see configured rows; owners see only rows with `selfVisible`. Ordinary collection grants are unnecessary for these specific values. The projection grants no directory access, access to other profiles, or editing rights. Choose information suitable for that audience. Only superusers configure it. -При обновлении с версии с BFF нужно повторно войти: прежние access/refresh cookies -не используются. Данные, учётные записи, passkey и API-сессии сохраняются. +Deleting a source, changing a relation target, marking it sensitive, or dropping/recreating a column hides the affected row until reconfiguration. Binding validation includes field names and physical column identity. Source renames or database restoration with different OIDs require saving the configuration again. Missing values display Not specified. -Администратор указывает email и передаёт пользователю ссылку `/invite#token=…` любым выбранным способом. Ссылка одноразовая, живёт семь дней и входит в аккаунт без почтовой доставки и обязательного пароля. Fragment удаляется из адреса страницы до отправки; GET сам по себе ссылку не активирует. Ссылка подтверждает владение секретом приглашения, а не почтовым ящиком. После входа можно добавить пароль, passkey или привязать SSO. Для повторного входа необходимо сохранить хотя бы один из этих способов; повторная активация использованной ссылки запрещена. +API: `GET|PUT /users/profile-display`, `GET /users/profile-display/sources`, `GET /users/me/profile-display`, and `GET /users/:id/profile-display`. Tests run with `node scripts/test.mjs core-profile`. -Приглашения требуют section `users/update`. Менеджер повторно выдаёт приглашение только ещё не активированному аккаунту без пароля, passkey и SSO, все политики которого входят в его разрешённый набор. Собственный аккаунт и пользователи с личным правом делегирования закрыты для такого действия. UI скрывает недоступную кнопку; сервер перепроверяет состояние. [Правила делегирования](./access.md#ограниченное-назначение-политик) описаны отдельно. + -## Passkey и восстановление +## Passwords and sessions -WebAuthn реализован через SimpleWebAuthn: discoverable credential, обязательное подтверждение пользователя на устройстве, проверка challenge, подписи, origin, RP ID и счётчика. Challenge живёт пять минут, одноразовый; регистрация привязана к конкретной сессии. До десяти ключей на аккаунт. Закрытый ключ остаётся на устройстве. Добавление/удаление и установка первого пароля требуют входа за последние пять минут; refresh не продлевает этот срок. Последний доступный способ входа удалить нельзя. +Passwords are stored separately as Argon2id hashes. Access/refresh tokens are random opaque values; only SHA-256 hashes are stored. Access lasts 15 minutes, sessions up to 30 days, and refresh rotates. Reusing a consumed refresh token revokes its session. -Активному обычному пользователю superuser со свежей сессией выдаёт одноразовую ссылку восстановления на 30 минут. При её использовании отзываются прежние сессии, пароль, passkey, SSO-привязки и OAuth-согласия. Выдача ссылки сама по себе ничего не сбрасывает. Такое восстановление не делегируется менеджерам и не применяется к superuser. Потерявшего доступ администратора восстанавливает оператор на сервере через `scripts/operations/admin-recover.mjs`; инструкция в [эксплуатации](../development/operations.md). +Browsers receive a separate opaque HttpOnly `asmblyr_session` cookie, SameSite=Lax and Secure on HTTPS. Its database value is hashed. Sessions last up to 30 days without sliding expiry; every request checks expiration, revocation, and user status. Browsers receive no refresh tokens, so tabs and replicas share a database-validated session without refresh races. Profiles allow editing personal information/passwords and viewing/revoking sessions. -`AUTH_UI_URL` задаёт точный публичный origin и обязателен в production. HTTPS обязателен, кроме локального localhost. Опциональный `PASSKEY_RP_ID` должен совпадать с hostname. При смене домена нужны новые ключи. Core дополнительно связывает passkey challenge с HttpOnly cookie браузера. +Browser sign-in endpoints are `/api/auth/browser/login`, `/api/auth/browser/invitations/claim`, `/api/auth/browser/passkeys/options`, and `/api/auth/browser/passkeys/login`. Success returns `{ok:true}`; the secret is sent only through Set-Cookie. Logout uses `POST /api/auth/browser/logout`. Cookie-authenticated mutations and all browser-auth operations require an exact Origin matching `AUTH_UI_URL`; missing or foreign origins are rejected. Explicit Authorization takes precedence over cookies. -## Подключение CLI +Upgrading from the former BFF requires signing in again: old access/refresh cookies are unused. Data, users, passkeys, and API sessions are preserved. -`asm connect` открывает `/sdk/connect` в админке для входа и подтверждения получения -схемы. CLI использует PKCE S256, одноразовый код на 60 секунд и точный callback на -`127.0.0.1` со случайным портом. Доступ `schema:read` действует 10 минут, связан с -активной сессией и принимается только `GET /schema`. Он не даёт читать или менять -записи. CLI не сохраняет токен; следующий онлайн-запрос снова требует входа либо -отдельного API-ключа из env/stdin. Подробнее — [CLI](../reference/cli-guide.md). +Administrators specify an email and share a one-time `/invite#token=…` link through any channel. It expires after seven days and signs in without email delivery or a mandatory password. The fragment is removed before submission; GET alone does not consume it. It proves possession of the invitation secret, not ownership of the mailbox. After sign-in, add a password, passkey, or linked SSO for future access. Consumed invitations cannot be reused. + +Invitations require users/update. Managers can re-invite only unactivated accounts without password, passkey, or SSO, whose policies all belong to their allowed set. Their own account and users with delegation rights are excluded. UI controls and server checks enforce this independently. See [delegation](./access.md#bounded-policy-delegation). + + + +## Passkeys and recovery + +SimpleWebAuthn implements discoverable credentials with required device user verification and checks for challenge, signature, origin, RP ID, and counter. Challenges are single-use, expire in five minutes, and registration binds to the session. Accounts support up to ten passkeys; private keys stay on the device. + +Adding/removing passkeys and setting the first password require sign-in within the past five minutes; refresh does not extend this period. The last available sign-in method cannot be removed. + +A freshly authenticated superuser can issue a one-time 30-minute recovery link to an active ordinary user. Claiming it revokes prior sessions, password, passkeys, SSO links, and OAuth consents. Issuing it alone resets nothing. Recovery is not delegated to managers and cannot target superusers. Operators recover locked-out administrators on the server using `scripts/operations/admin-recover.mjs`; see [operations](../development/operations.md). + +`AUTH_UI_URL` sets the exact public origin and is mandatory in production. HTTPS is required except for local localhost. Optional `PASSKEY_RP_ID` must match the hostname. Domain changes require new passkeys. Core also binds the challenge to an HttpOnly browser cookie. + + + +## CLI connection + +`asm connect` opens `/sdk/connect` for sign-in and schema consent. It uses PKCE S256, a one-time 60-second code, and an exact `127.0.0.1` callback on a random port. The session-bound `schema:read` token lasts ten minutes and is accepted only by `GET /schema`, never for record reads or writes. + +The CLI does not persist the token. Another online request requires sign-in or a separate API key from env/stdin. See [CLI](../reference/cli-guide.md). ## SSO -Поддержаны OIDC и OAuth через конфигурацию провайдеров. Ключ провайдера задаёт URL -`/sign/sso//callback` на публичном домене. Core обслуживает начало входа -`POST /sign/sso/:provider` и callback, проверяет browser proof и создаёт сессию. -Программные endpoints `/auth/sso/:provider/start` и `/callback` также сохраняются. +Configured providers support OIDC and OAuth. The provider key determines the public `/sign/sso//callback` URL. Core serves `POST /sign/sso/:provider` and its callback, validates browser proof, and creates the session. Programmatic `/auth/sso/:provider/start` and `/callback` endpoints remain available. -OIDC использует discovery и проверку issuer, state, PKCE и nonce. Привязать новый провайдер можно из уже подтверждённой учётной записи; совпадение email само по себе не связывает аккаунты. Секреты провайдеров остаются в серверной конфигурации. +OIDC validates discovery, issuer, state, PKCE, and nonce. New providers are linked from an already authenticated account; matching email alone never links accounts. Provider secrets stay in server configuration. -Ограничения: нет отдельного MFA flow, общего LDAP/SAML-коннектора и самостоятельного email-восстановления пароля. SMTP не требуется для первой беты. Общий лимит credential endpoints хранится в PostgreSQL; дополнительные ограничения маршрутов локальны процессу. На встроенном публичном listener Core видит соединение браузера напрямую. За внешним reverse proxy общий лимит источника относится к адресу proxy: внешним forwarded headers Core не доверяет. +There is no separate MFA flow, general LDAP/SAML connector, or self-service email password recovery. SMTP is unnecessary for the first beta. The shared credential-endpoint rate limit lives in PostgreSQL; additional route limits are process-local. Core's built-in public listener sees browser connections directly. Behind an external reverse proxy, the shared source limit applies to the proxy address because Core does not trust external forwarded headers. -Источники: `apps/core/src/auth`, `apps/core/src/auth/browser`. Авторизацию для внешних приложений описывает [интеграционная страница](./integrations.md). +Sources: `apps/core/src/auth` and `apps/core/src/auth/browser`. External application authorization is covered under [integrations](./integrations.md). diff --git a/docs/features/integrations.md b/docs/features/integrations.md index bda9807..d368790 100644 --- a/docs/features/integrations.md +++ b/docs/features/integrations.md @@ -1,58 +1,48 @@ -# Сервисы, федерации и OAuth-приложения + -## Сервисные аккаунты +# Services, federations, and OAuth applications -Аккаунту назначают политики. Секрет ключа показывается один раз, в БД хранится hash. `POST /auth/service-token` меняет ключ на короткий Bearer token до 15 минут, без refresh. Отзыв ключа/отключение аккаунта проверяется при последующих обращениях. + -В карточке каждого ключа видны дата и время выпуска, срок действия, последняя -активность и число обращений к API. Последняя активность учитывает успешный -обмен ключа на токен и HTTP-запросы с успешно проверенным токеном этого ключа. -Счётчик увеличивается один раз на такой HTTP-запрос, даже если затем операция -отклонена из-за прав. Получение токена, неверные/истёкшие/отозванные токены, -запросы без проверки сервисного токена и федеративные токены в него не входят. -Статистика читается при открытии аккаунта; обращения до её включения не восстанавливаются. +## Service accounts -`GET /service-accounts/:id` и ответ выпуска ключа содержат `createdAt`, -`lastUsedAt` (последний обмен), `lastActivityAt` и `requestCount`. Счётчик передаётся -десятичной строкой, сохраняя точность PostgreSQL bigint. Новые ключи начинают с -`"0"`; существующим ключам миграция переносит известное время обмена в последнюю -активность, оставляя счётчик нулевым. Отзыв и отключение сохраняют статистику. -Откат миграции запрещён, если он удалит записанную статистику. +Assign policies to a service account. A key secret is shown once; only its hash is stored. `POST /auth/service-token` exchanges it for a Bearer token lasting up to 15 minutes, without refresh. Subsequent requests check key revocation and account status. -Просмотр требует section `services/read` или `update`; изменение — `services/update`. Для менеджера все текущие и назначаемые политики сервиса должны входить в личный разрешённый набор администратора. Эта проверка действует и для выпуска/отзыва ключей и изменения федераций. Сервис с закрытой политикой доступен только для просмотра. Подробнее — [ограниченное назначение политик](./access.md#ограниченное-назначение-политик). +Each key shows issuance time, expiry, last activity, and API request count. Activity includes successful key-to-token exchange and HTTP requests with a successfully validated token from that key. The count increments once per such HTTP request, even if permissions later reject the operation. Token issuance, invalid/expired/revoked tokens, requests without service-token verification, and federated tokens are excluded. -Сервисный токен использует права на данные, но не человеческие настройки/профиль. Ранее выданные ключи не отзываются автоматически при сокращении разрешённого набора менеджера; их отзывает администратор отдельно. +Statistics load when opening the account; activity predating their introduction is not reconstructed. `GET /service-accounts/:id` and key-issuance responses contain `createdAt`, `lastUsedAt` (last exchange), `lastActivityAt`, and `requestCount`. The counter is a decimal string preserving PostgreSQL bigint precision. New keys start at `"0"`. Migration seeds old keys' activity from their known exchange time and leaves counters at zero. Revocation and disabling preserve statistics; rollback refuses to discard recorded statistics. + +Viewing requires services/read or update; changes require services/update. For managers, all current and requested service policies must belong to their personal delegation set. Keys and federation changes use the same check. A service with a protected policy is read-only. See [bounded delegation](./access.md#bounded-policy-delegation). + +Service tokens use data permissions, not human settings/profile access. Reducing a manager's allowed set does not automatically revoke previously issued keys; administrators revoke them separately. ## GitLab CI federation -Реализован конкретный issuer `https://gitlab.com`. Проверяются RS256, issuer, audience, subject, project/job-project ID и path, защищённая ветка, время действия и jti. Merge-request pipeline не допускается. Повтор assertion блокируется хешем jti в БД. +The supported issuer is `https://gitlab.com`. Checks cover RS256, issuer, audience, subject, project/job-project ID and path, protected branch, validity times, and jti. Merge-request pipelines are rejected. A database jti hash blocks assertion replay. + +This signs a service into Collaborative; it is not a general catalog of OIDC issuers. Core's Yandex Object Storage federation is separate infrastructure-account configuration. + + -Это вход сервиса в Asmblyr, а не универсальный каталог всех OIDC issuer. Федерация доступа Core к Yandex Object Storage — отдельная настройка инфраструктурной учётной записи. +## Collaborative as an OAuth/OIDC provider -## Asmblyr как OAuth/OIDC provider +The OAuth section creates applications that let external services authenticate through a Collaborative account. The base profile is fixed: ID, email, name, and optional picture. Admission can use configured user/email-domain rules or allow all eligible users. -В разделе `oauth` создаются приложения для входа других сервисов через аккаунт Asmblyr. Базовый профиль фиксирован: ID, email, имя и при наличии изображение. Доступ приложения можно ограничить настроенными правилами, включая пользователей/домен email, либо разрешить всем подходящим пользователям. +Consent is stored per user/application. Existing grants avoid repeated consent for already approved scopes. Users can revoke applications in their profile. Application OAuth tokens are not Core API tokens. -Согласие сохраняется для пары пользователь–приложение. Повторный вход с действующим grant не требует повторного согласия для уже разрешённого набора. Пользователь может отозвать приложение в профиле. OAuth-токен приложения не является Core API-токеном. +`oidc-provider` serves `/oauth/*`. Obtain protocol endpoints from discovery, not the static REST matrix. Administrative `/oauth-apps` and `/oauth-interactions` routes are documented separately. -Протокол обслуживает `oidc-provider` через `/oauth/*`. Полные protocol endpoints берутся из его discovery, а не из статической REST-матрицы. Административные `/oauth-apps` и `/oauth-interactions` описаны отдельно. + -### Доступ через политики +### Policy-managed access -В приложении на вкладке «Доступ» включите **«Управлять доступом через политики»**. -На вкладке «Интеграция» задайте Audience и каталог разрешений: техническое значение -для сервиса и понятное название для редактора. Сам каталог не выдаёт права. -Затем в редакторе политики → «Приложения» разрешите вход, отметьте разрешения -и назначьте политику пользователям. Разрешения всех политик пользователя объединяются. -Без подходящей политики вход запрещён, в том числе superuser Collaborative. +Enable Manage access through policies on the application's Access tab. On Integration, configure Audience and the permission catalog: a technical service value and a readable editor label. The catalog itself grants nothing. -При включённом режиме прежние правила по пользователям и доменам не участвуют -в проверке. При выключении снова действуют эти правила; назначенные права политик -сохраняются, но не используются. Перед выключением проверьте прежний режим доступа. -Существующие приложения сохраняют прежнее поведение: миграция не включает новый режим. +In a policy's Applications tab, allow sign-in, select permissions, and assign the policy to users. User policies combine grants. Without a matching policy, sign-in is denied even to a Collaborative superuser. -В режиме политик приложение запрашивает только `openid profile email`. -Core добавляет персональные разрешения в подписанный access token: +While this mode is enabled, old user/domain admission rules do not participate. Disabling it restores those rules; policy assignments remain stored but unused. Review the previous admission mode before switching off. Existing applications keep their behavior: migration does not enable policy management automatically. + +In policy mode, the application requests only `openid profile email`. Core adds personal permissions to its signed access token: ```json { @@ -64,29 +54,18 @@ Core добавляет персональные разрешения в под } ``` -Сервис должен поддерживать чтение этого claim; универсального преобразования -произвольных политик в формат каждого сервиса нет. Для LavinMQ установите -`resource_server_id = lavinmq`, `audience = lavinmq`, `verify_aud = true` и -`mgmt_scopes = openid profile email`. Удалите общие сервисные scopes из запроса входа. -Теги управления и разрешения на vhost/ресурсы задаются отдельно по правилам LavinMQ. +The external service must understand this claim; there is no universal conversion to every service's policy format. For LavinMQ, set `resource_server_id = lavinmq`, `audience = lavinmq`, `verify_aud = true`, and `mgmt_scopes = openid profile email`. Remove shared service scopes from the sign-in request. Management tags and vhost/resource permissions follow separate LavinMQ rules. + +Consent displays personal permission labels. Adding rights requires new consent. If rights change while confirmation is open, Core returns 409; restart sign-in to review current rights. Code exchange rechecks access and intersects current rights with consented rights, preventing a pending sign-in from receiving newly added permissions. + +Changes apply when issuing a new token. External services may accept an existing JWT until expiry, five minutes, and manage active connections under their own rules. LavinMQ users remain OAuth identities; local broker accounts are not created. -На согласии видны названия персональных разрешений. Расширение прав требует нового -согласия. Если набор изменился, пока открыто окно подтверждения, Core возвращает -409: нужно начать вход заново и увидеть актуальные права. При обмене кода Core повторно проверяет доступ и пересекает текущие права -с подтверждёнными: начатый вход не может получить позднее добавленное право. -Изменения учитываются при новой выдаче токена. Уже выданный JWT внешний сервис может -принимать до истечения его срока (5 минут); активное соединение сервис может -закрывать по собственным правилам. Пользователи LavinMQ остаются OAuth-идентичностями, -локальные учётные записи в брокере не создаются. +Application REST fields are `policyManaged` (default false), `scopes` (catalog), and `scopeLabels` (value → label). Omitting policyManaged or scopeLabels on update preserves them. Removing a catalog value removes policy assignments for it; adding it back does not restore old grants. Changing Audience or access mode resets consent. -REST-поля приложения: `policyManaged` (по умолчанию `false`), `scopes` (каталог), -`scopeLabels` (словарь значение → название). Пропущенные `policyManaged` и -`scopeLabels` при обновлении сохраняют текущую настройку. Удаление значения из -каталога удаляет его из назначений политик; добавление обратно не восстанавливает -старые права. Смена Audience или режима доступа сбрасывает согласия. +A local LavinMQ example is available. Its live test requires a running container and is not part of the ordinary full suite. Localhost HTTP allowances are not production settings: external issuers need HTTPS, persistent signing keys, and correct proxy/origin configuration. -Локальный пример LavinMQ существует; его live-проверка зависит от поднятого контейнера и не выполняется обычным полным набором тестов. Не переносите разрешение HTTP для localhost в production: внешнему issuer нужны HTTPS, постоянные ключи подписи и корректные proxy/origin настройки. + -## Личные подключения Google +## Personal Google connections -[Google Workspace](./google-workspace.md) использует отдельный OAuth client для ассистента, личные зашифрованные токены и подтверждение внешней записи. Это не вход через SSO и не выдача доступа приложениям Asmblyr OAuth provider. +[Google Workspace](./google-workspace.md) uses a separate OAuth client for the assistant, encrypted personal tokens, and confirmation before external writes. It is separate from SSO login and from applications using Collaborative as their OAuth provider. diff --git a/docs/features/localization.md b/docs/features/localization.md index d3eab96..c3f0b46 100644 --- a/docs/features/localization.md +++ b/docs/features/localization.md @@ -1,27 +1,24 @@ -# Переводы + -Язык интерфейса (`ru`, `en`) сохраняется в личных настройках вместе с независимыми -режимом и цветовым стилем. Основа построена на i18next/react-i18next: каталоги -поставляются с приложением, внешний сервис переводов для работы не нужен. -Экземпляры изолированы при серверном рендеринге. +# Translations -## Кто переводит подписи +The interface language (`ru`, `en`) is saved in personal preferences independently of color mode and style. The admin uses i18next/react-i18next with bundled catalogs, so it needs no external translation service. Server-rendered instances are isolated. -- Интерфейс и системные поля: JSON-каталоги `packages/contracts/locales/`. -- Плагины: собственные `locales/ru.json`, `locales/en.json` и namespace плагина. -- Пользовательские коллекции и поля: раздел «Переводы» в правом редакторе настроек. - Для коллекции сохраняется подпись, для поля — подпись, описание и плейсхолдер. + -Основная подпись остаётся запасным вариантом, если пользователь не заполнил -перевод. Языковые каталоги плагинов используют русский fallback. Технические имена -API, значения записей, enum-значения и шаблоны подписи записей не переводятся. -Переключение языка не перезаписывает исходные настройки схемы. +## Label ownership + +- Interface and system fields: JSON catalogs in `packages/contracts/locales/`. +- Plugins: their own `locales/ru.json`, `locales/en.json`, and plugin namespace. +- Custom collections and fields: the Translations section in the right-side settings editor. Collections have translated labels; fields also support descriptions and placeholders. + +The original label is the fallback when a custom translation is missing. Plugin catalogs fall back to Russian. Technical API names, record values, enum values, and record label templates are not translated. Switching languages does not overwrite the underlying schema settings. ## HTTP API -Core: `GET /translations?locale=en`. Прокси админки: -`GET /api/translations?locale=en`. SDK: `client.translations.get("en")`. -Язык по умолчанию — русский; другие языки и неизвестные query-параметры дают 400. +Core: `GET /translations?locale=en`. On the admin domain: `GET /api/translations?locale=en`. SDK: `client.translations.get("en")`. + +The API defaults to Russian. Unsupported locales and unknown query parameters return 400. The documentation website's English default does not change this contract. ```json { @@ -43,28 +40,18 @@ Core: `GET /translations?locale=en`. Прокси админки: } ``` -Требуется активный пользователь или сервисный аккаунт. `schema` использует тот же -набор видимых коллекций и полей, что и `GET /collections`: права создания, чтения -и изменения объединяются для метаданных. Это не разрешает читать записи. -Системный ключ присутствует, timestamps — только при доступе к ним. В ответе нет -данных записей, defaults, настроек или секретов. `plugins` содержит статические -публичные каталоги всех активных расширений, в том числе без собственного UI. -Ответ имеет `Cache-Control: private, no-store`. - -## Покрытие - -Каталоги RU/EN покрывают встроенные формы и сообщения: редакторы коллекций, -полей и записей, фильтры, права, файлы, профиль и безопасность, интеграции, -ассистент и настройки расширений. Даты и форматированные числа учитывают язык, -точные decimal-значения сохраняют свою точность. Смена языка обновляет и подписи, -пришедшие с сервера. Пользовательские значения, названия политик и сообщения чата -сохраняются в исходном виде; переводы пользовательской схемы задаются явно. - -Плагины переводят подписи своих настроек ключами `settings..label`, -`settings..description` и `settings..options.`. При отсутствии -перевода используется исходная подпись. Новые встроенные тексты добавляются -в оба каталога; проверки контролируют наличие ключей и совпадение параметров. - -Каталоги — обычные JSON-файлы со стабильными ключами; их можно готовить внешними -инструментами и проверять в ревью. Автоматических вызовов AI, платного backend -переводов или автоматической публикации переводов в текущей реализации нет. +An active human or service account is required. `schema` uses the same visible collections and fields as `GET /collections`: create, read, and update grants are combined for metadata. This does not authorize reading records. + +The system key is included; timestamps appear only when accessible. Responses contain no record data, defaults, settings, or secrets. `plugins` includes static public catalogs from all active plugins, even those without UI. Responses use `Cache-Control: private, no-store`. + + + +## Coverage + +RU/EN catalogs cover built-in forms and messages: collection, field, and record editors; filters; permissions; files; profile and security; integrations; assistant; and plugin settings. Dates and formatted numbers follow the locale without losing decimal precision. Switching language also refreshes server-provided labels. + +User values, policy names, and chat messages keep their original text. Custom schema translations are explicit. + +Plugin settings use `settings..label`, `settings..description`, and `settings..options.`, falling back to the original label. Add built-in strings to both catalogs; checks enforce key and interpolation-parameter parity. + +Catalogs are ordinary JSON with stable keys. External tools can prepare translations for review. Automatic AI calls, a paid translation backend, and automatic translation publication are not implemented. diff --git a/docs/features/monitoring.md b/docs/features/monitoring.md index 8693fa5..a9fd7c9 100644 --- a/docs/features/monitoring.md +++ b/docs/features/monitoring.md @@ -1,133 +1,79 @@ -# Мониторинг и Sentry - -Мониторинг опционален и по умолчанию выключен. Раздел `/admin/settings/monitoring` -доступен только суперпользователю. Настройки открываются в правом редакторе и -сохраняются через существующий механизм [подключений и шифрования](./connections.md). -Новая установка и существующая база работают без Sentry, DSN и дополнительных миграций. - -## Подключение - -В редакторе есть две вкладки: **«Сервер»** для Core/API и **«Интерфейс»** для -браузера. В каждой укажите свой DSN и выберите, что собирать. Можно использовать -один проект Sentry, указав одинаковый DSN в обеих вкладках, или отдельные проекты. -Для серверного проекта выбирайте платформу Node.js, для браузерного — Browser -JavaScript. Тег `component` различает `core` и `browser`. - -Включение мониторинга, окружение, релиз и доля трассировок общие для обеих вкладок. -Чтобы подключить только одну сторону, отключите сбор для другой. По умолчанию -выбраны ошибки обеих сторон; производительность и SQL-операции выключены. При -отсутствии нужного DSN редактор откроет вкладку, которую нужно заполнить. - -| Переключатель | Что собирается | -| ----------------------------- | ----------------------------------------------------------------------------------------------- | -| Ошибки сервера | Неожиданные ошибки обработчиков API с кодом 5xx | -| Ошибки интерфейса | JavaScript errors и unhandled Promise rejections в авторизованной админке | -| Производительность API | Длительность запроса до окончания ответа; локальные p50/p95/p99 и выбранные Sentry transactions | -| Производительность интерфейса | Navigation Timing текущей загрузки и новые same-origin fetch/XHR к основным API админки | -| Время SQL-операций | DB spans внутри выбранных API-трассировок; только тип операции и длительность | - -«Доля трассировок» задаётся от 0 до 100%. Она управляет отправкой производительности -в Sentry, а не выборкой ошибок. Локальные процентили используют все измеренные -запросы, в том числе при 0% трассировок. При выключенном сборе SDK не создаётся; -браузер не загружает его пакет и не устанавливает обработчики. - -Кнопка проверки проверяет формат DSN и доступность сохранённых ключей, без тестового -события. Она не подтверждает доставку в Sentry. Для проверки доставки включите -нужный сбор и проверьте появление событий в своём проекте Sentry. - -## p95/p99 и пределы измерений - -Локальная таблица показывает **текущий экземпляр Core**, последние 15 минут и до -2 000 наблюдений на маршрут. При превышении лимита остаются последние наблюдения, -и появляется отметка ограничения. Поддерживается до 128 маршрутов; превышение -также отмечается. Это ограниченная диагностика в памяти, без накопления в БД; -перезапуск или выключение сбора очищает её. При балансировке экземпляров ответы -могут поступать от разных процессов. Общие показатели смотрите в Sentry. - -p95 означает, что 95% измеренных запросов заняли не больше указанного времени; -p99 — 99%. Используется nearest-rank percentile. Админка показывает p95 от 20 -наблюдений, p99 от 100. Это порог отображения, а не гарантия статистической -устойчивости. В Sentry процентили считаются по отправленным трассировкам и выбранному -периоду; малая выборка может плохо отражать редкие задержки. - -Длительность Core не включает внешнюю сеть и браузер; для streaming она включает -передачу всего ответа. Browser Timing измеряется отдельно. Не объединяйте эти -две серии в один показатель. SQL spans не включают ожидание соединения в пуле; -очередь пула показана отдельным текущим счётчиком. RSS/heap отражают текущую память -процесса. p99 event loop берётся с момента включения сбора, с разрешением 20 мс. - -## Данные и жизненный цикл - -События проходят общий allowlist. Отправляются тип/место ошибки, очищенный стек -(basename файла, строка, функция), шаблон маршрута, длительности и технические -параметры окружения/релиза. Текст ошибки заменяется общей фразой. Значения URL, -query, заголовки, cookies, тела запросов/ответов, записи, сообщения ассистента, -SQL, bindings, пользователи и breadcrumbs исключены. Имя коллекции и ID в -шаблоне маршрута заменяются параметрами. Логи, Replay и CPU profiling не включены. - -Sentry получает сетевое соединение от Core или браузера и может обрабатывать -сетевые метаданные по своим настройкам. Оператор установки выбирает Sentry -и его правила обработки данных. DSN интерфейса содержит публичный ключ приёма -событий: при включённом браузерном мониторинге его видят авторизованные -пользователи. Отдельный серверный DSN браузеру не выдаётся. DSN не является -административным токеном Sentry; указывать такой токен нельзя. - -Сохранённые DSN шифруются как остальные ключи подключения. API настроек возвращает -только отдельные признаки их наличия. Изменения применяются сразу в экземпляре, обработавшем -сохранение; остальные экземпляры перечитывают конфигурацию каждые 5 секунд, браузер -— каждые 30 секунд и при локальном сохранении. Неактивные вкладки приостанавливают -проверку и возобновляют её при показе. При ошибке конфигурации мониторинг выключается, -а основные функции продолжают работать. Отключение без смены DSN доступно даже при -недоступном KMS; сохранённые ключи при этом остаются в БД. - -Сбор Core исключает health/ready, auth, OAuth, личные подключения, конфигурацию -мониторинга и неизвестные маршруты. Браузерные resource timings ограничены API -items, collections, files, preferences, presence, notifications и assistant. -Автоматического сбора Next.js SSR errors и React component boundaries нет. -Ошибки фона вне HTTP-запроса также не перехватываются этим подключением. - -## Конфигурация через env - -Любое непустое значение из списка `SENTRY_*` ниже блокирует группу в админке. -Параметры становятся read-only; изменить их можно в конфигурации сервера. -`SENTRY_ENABLED=false` явно выключает и блокирует группу. - -| Переменная | По умолчанию / значение | -| ---------------------------- | --------------------------------------------------------- | -| `SENTRY_ENABLED` | `true`, если задан DSN; иначе `false` | -| `SENTRY_DSN` | Серверный DSN; HTTPS, публичный ключ, числовой ID проекта | -| `SENTRY_BROWSER_DSN` | DSN интерфейса; при отсутствии используется `SENTRY_DSN` | -| `SENTRY_ENVIRONMENT` | `production`, до 64 символов, без пробелов/слешей/`None` | -| `SENTRY_RELEASE` | Пустая строка; до 120 символов | -| `SENTRY_ERRORS_CORE` | `true` при наличии серверного DSN | -| `SENTRY_ERRORS_BROWSER` | `true` при наличии DSN интерфейса | -| `SENTRY_PERFORMANCE_CORE` | `false` | -| `SENTRY_PERFORMANCE_BROWSER` | `false` | -| `SENTRY_DATABASE_SPANS` | `false`; работает вместе с performance Core | -| `SENTRY_TRACES_SAMPLE_RATE` | `0.1`; число от 0 до 1 | - -Boolean-параметры принимают только `true`/`false`. HTTP DSN допускается только для -loopback при локальной проверке. Не используйте DSN с паролем, query или fragment. -Неверная опциональная конфигурация отключает мониторинг и показывает его недоступность. - -Для совместимости с общей конфигурацией `SENTRY_DSN` используется также для -браузера, если `SENTRY_BROWSER_DSN` не задана. Для сбора только на сервере явно -отключите `SENTRY_ERRORS_BROWSER` и `SENTRY_PERFORMANCE_BROWSER`. Если задана только -`SENTRY_BROWSER_DSN`, сбор серверных ошибок по умолчанию выключен. - -API принимает `secrets.serverDsn` и `secrets.browserDsn`. Ранее сохранённый общий -DSN остаётся резервным значением для каждой стороны до редактирования ключей. -При замене или удалении одного ключа значение другой стороны сохраняется отдельно, -а общий слот удаляется в той же транзакции. `null` удаляет ключ выбранной стороны; -если её сбор включён, сохранение отклоняется. При выключенном мониторинге можно -удалить оба ключа. Совместимый ключ `secrets.dsn` допускается только отдельно от -других ключей: он задаёт общий DSN и очищает отдельные, а `null` удаляет все DSN. -Без изменения ключей шифротекст сохраняется, включая возможность выключить -мониторинг при недоступном KMS. Миграция базы для этого не нужна. - -`GET /monitoring/browser` требует активную человеческую сессию и возвращает только -браузерную конфигурацию. `GET /settings/monitoring` требует human superuser и -возвращает локальные показатели. Оба ответа имеют `private, no-store`. -Сохранение — `PUT /settings/integrations/monitoring`, проверка — -`POST /settings/integrations/monitoring/test`, с общей optimistic revision и правилами -write-only secrets. Контракты приведены в [OpenAPI](../reference/http.md). + + +# Monitoring and Sentry + +Monitoring is optional and disabled by default. Only superusers access `/admin/settings/monitoring`. A right-side editor saves settings through the existing [connections and encryption](./connections.md) mechanism. New and existing installations work without Sentry, DSNs, or extra migrations. + + + +## Connection + +The editor separates Server (Core/API) and Interface (browser) tabs. Enter a DSN and select collection options for each. Use one Sentry project with the same DSN in both tabs, or separate projects. Choose Node.js for the server and Browser JavaScript for the browser. The `component` tag distinguishes `core` and `browser`. + +Enablement, environment, release, and trace sample rate are shared. To monitor one side only, disable collection for the other. Both error switches default on; performance and SQL spans default off. If a required DSN is missing, the editor opens its tab. + +| Switch | Collected data | +| --------------------- | ----------------------------------------------------------------------------------------------- | +| Server errors | Unexpected API handler errors with 5xx status | +| Interface errors | JavaScript errors and unhandled Promise rejections in the authenticated admin | +| API performance | Request duration through response completion; local p50/p95/p99 and sampled Sentry transactions | +| Interface performance | Current-load Navigation Timing and new same-origin fetch/XHR to selected admin APIs | +| SQL duration | Database spans in sampled API traces, with operation type and duration only | + +Trace sampling ranges from 0–100% and controls performance submission, not errors. Local percentiles use all measured requests, even at 0% sampling. Disabled collection does not instantiate the SDK; browsers do not load its package or install handlers. + +Test checks DSN format and saved-key availability without sending an event. It does not prove delivery to Sentry. Enable the relevant collection and verify received events in your Sentry project. + + + +## p95/p99 and measurement limits + +The local table covers the **current Core instance**, the last 15 minutes, and up to 2,000 observations per route. Over the cap, it retains the latest observations and marks the limit. Up to 128 routes are supported, also with a limit indicator. This is bounded in-memory diagnostics, without database accumulation. Restarting or disabling collection clears it. Load-balanced requests may reach different processes; use Sentry for aggregate observations. + +p95 means 95% of measured requests took no longer than the displayed duration; p99 means 99%. Calculation uses nearest rank. The admin shows p95 from 20 observations and p99 from 100. These are display thresholds, not guarantees of statistical stability. Sentry computes percentiles over submitted traces and the selected period; small samples may misrepresent rare delays. + +Core duration excludes external network and browser time; streaming includes the full response transfer. Browser Timing is separate: do not combine these series. SQL spans exclude connection-pool wait, shown as a separate current queue counter. RSS/heap show current process memory. Event-loop p99 covers the period since enabling collection, at 20 ms resolution. + + + +## Data and lifecycle + +A shared allowlist retains error type/location, sanitized stack (file basename, line, function), route template, durations, and technical environment/release settings. Error text is replaced with a generic message. URL values, queries, headers, cookies, request/response bodies, records, assistant messages, SQL, bindings, users, and breadcrumbs are excluded. Collection names and IDs become route parameters. Logs, Replay, and CPU profiling are not enabled. + +Sentry receives network connections from Core or browsers and may process network metadata under its settings. Operators choose Sentry and its data-processing configuration. The browser DSN contains a public event-ingestion key visible to authenticated users when browser monitoring is enabled. A separate server DSN is never sent to browsers. A DSN is not a Sentry administration token; never supply one as a DSN. + +Saved DSNs use the same encryption as other connection secrets; settings responses expose only separate presence flags. Changes apply immediately on the saving instance. Other instances reload every five seconds; browsers every 30 seconds and after local saves. Hidden tabs pause polling and resume when visible. + +Invalid configuration disables monitoring while core product functions continue. Monitoring can be disabled without changing DSNs even when KMS is unavailable; saved keys remain in the database. + +Core excludes health/ready, auth, OAuth, personal connections, monitoring configuration, and unknown routes. Browser resource timings cover items, collections, files, preferences, presence, notifications, and assistant APIs. This connection does not automatically collect Next.js SSR errors, React error boundaries, or background errors outside HTTP requests. + + + +## Environment configuration + +Any nonempty setting below locks the group in the admin. Edit locked values in server configuration. `SENTRY_ENABLED=false` explicitly disables and locks the group. + +| Variable | Default / value | +| ---------------------------- | ----------------------------------------------------------- | +| `SENTRY_ENABLED` | true when a DSN is set; otherwise false | +| `SENTRY_DSN` | Server DSN: HTTPS, public key, numeric project ID | +| `SENTRY_BROWSER_DSN` | Browser DSN; falls back to SENTRY_DSN | +| `SENTRY_ENVIRONMENT` | production; up to 64 characters, no whitespace/slashes/None | +| `SENTRY_RELEASE` | Empty; up to 120 characters | +| `SENTRY_ERRORS_CORE` | true when server DSN exists | +| `SENTRY_ERRORS_BROWSER` | true when browser DSN exists | +| `SENTRY_PERFORMANCE_CORE` | false | +| `SENTRY_PERFORMANCE_BROWSER` | false | +| `SENTRY_DATABASE_SPANS` | false; requires Core performance | +| `SENTRY_TRACES_SAMPLE_RATE` | 0.1; number from 0 to 1 | + +Booleans accept only true/false. HTTP DSNs are allowed only for loopback during local testing. DSNs with passwords, queries, or fragments are invalid. Invalid optional configuration disables monitoring and shows it as unavailable. + +For backward compatibility, SENTRY_DSN also supplies the browser when SENTRY_BROWSER_DSN is absent. Server-only collection requires explicitly disabling SENTRY_ERRORS_BROWSER and SENTRY_PERFORMANCE_BROWSER. With only SENTRY_BROWSER_DSN configured, server errors default off. + +The API accepts `secrets.serverDsn` and `secrets.browserDsn`. A previously saved shared DSN remains each side's fallback until editing keys. Replacing/removing one side preserves the other separately and removes the shared slot in the same transaction. Null removes that side's key; saving is rejected if its collection remains enabled. Both keys can be removed while monitoring is disabled. + +The compatible `secrets.dsn` key can be supplied only without other keys. It sets a shared DSN and clears separate values; null removes all DSNs. Without key changes, ciphertext stays intact, including when disabling during a KMS outage. No database migration is needed. + +`GET /monitoring/browser` requires an active human and returns only browser configuration. `GET /settings/monitoring` requires a human superuser and returns local metrics. Both use private, no-store. Save through `PUT /settings/integrations/monitoring`; test through `POST /settings/integrations/monitoring/test`, with shared optimistic revision and write-only-secret rules. See [OpenAPI](../reference/http.md). diff --git a/docs/features/notifications.md b/docs/features/notifications.md index 5a5a106..c59b56a 100644 --- a/docs/features/notifications.md +++ b/docs/features/notifications.md @@ -1,60 +1,45 @@ -# Обсуждения и уведомления - -Уведомления открываются колокольчиком в шапке админки. Ассистент остаётся отдельным -виджетом; отключение AI не отключает обсуждения или уведомления. - -## Подписка на запись - -Во вкладке «Обсуждение» плагина комментариев нажмите **Следить**. Первый отправленный -комментарий также включает подписку. Кнопка **Вы следите** отключает её; последующие -собственные комментарии не включают явно отключённую подписку заново. - -Новый комментарий другого участника создаёт уведомление подписчикам с текущим -правом чтения записи. Собственные комментарии уведомлений не создают. Редактирование -обновляет выдержку существующего уведомления; удаление комментария удаляет его -уведомления. Email, push и уведомления о простом изменении полей не реализованы. - -## Входящие - -Колокольчик показывает автора, короткий текст, коллекцию, ключ записи и время. -Нажатие открывает обсуждение и выделяет адресованный комментарий, включая старые -комментарии вне первой страницы. Уведомление отмечается прочитанным при переходе. -Если комментарий уже недоступен, обсуждение остаётся открытым с пояснением. - -Открытие списка само по себе не отмечает уведомления прочитанными. **Прочитать все** -отмечает собственные уведомления до времени снимка списка; новые события после него -остаются непрочитанными. Статус сохраняется в PostgreSQL и общий для устройств. -Обновление — каждые 30 секунд в видимой вкладке, при возвращении в окно и открытии -колокольчика. Переход использует защиту несохранённых изменений редактора. - -Core хранит последние 200 уведомлений каждого пользователя, по умолчанию возвращает -50 с возможностью показать остальные. Счётчик относится к доступным уведомлениям -в этом окне хранения. Содержимое и счётчик повторно проверяют права на запись, -включая правила строк. Выключенный плагин скрывает свои события, сохраняя данные. -Удаление записи очищает подписки и уведомления; удаление коллекции очищает их по -стабильному ID коллекции. Повторное создание таблицы с тем же именем их не наследует. - -## API и Kit - -- `GET /notifications?limit=50`: `{ data, unread, total, readBefore }`. -- `POST /notifications/:id/read`: 204; чужой или недоступный ID — 404. -- `POST /notifications/read-all`: `{ before: readBefore }`, ответ 204. - -Все три endpoint требуют активной человеческой сессии. Сервисный аккаунт может -написать комментарий с обычными правами на запись, но не читать личные входящие -и не управлять человеческими подписками. - -SDK: `client.notifications.list()`, `.read(id)`, `.readAll(result.readBefore)`. -В UI те же пути доступны напрямую в Core под `/api` с браузерной HttpOnly-сессией. - -Плагин объявляет и получает одобрение возможности `notifications`. Только обычный -HTTP-контекст `useAsmblyr(event)` получает `notifications.following`, `follow`, -`publish`, `update` и `remove`. Core связывает источник с namespace пакета, -автора — с текущей идентичностью, получателей — с подписками и правами записи. -Model handlers не получают этот привилегированный контекст. Публикация выполняется -внутри `withRecord`, в одной транзакции с комментарием; внешних сообщений нет. - -Plugin UI получает необязательные `active` и `targetId` в `RecordPanelProps`. -Ссылка записи `?panel=plugin:namespace:panel-id&target=entity-id` выбирает только -существующую панель. Плагин сам проверяет доступ и показывает адресованную сущность. -Хост не содержит специальных условий для комментариев. + + +# Discussions and notifications + +The notification bell is separate from the assistant. Disabling AI does not disable notifications. + + + +## Following a discussion + +Use Follow in a record's Discussion tab. Posting your first comment automatically follows that discussion. Explicitly unfollowing it prevents later comments of your own from enabling following again. + +A new comment notifies other followers who can currently read the record. Your own comments do not notify you. Editing a comment updates its notification excerpt; deleting it removes associated notifications. Email, push, and notifications for ordinary field changes are not implemented. + + + +## Inbox behavior + +The bell shows the author, excerpt, collection, record, and time. Clicking a notification opens the record, finds the comment even on an older page, highlights it, and marks the notification as read. If the comment no longer exists, the UI explains this. + +Opening the list alone does not mark notifications as read. Mark all as read uses the list's snapshot time; newer arrivals remain unread. Read state is stored in PostgreSQL and shared across devices. + +The UI refreshes every 30 seconds while visible, on window focus, and when opening the bell. Navigation respects the unsaved-changes guard. + +The inbox retains the latest 200 notifications per user and initially displays 50, with Load more. The unread count includes accessible notifications within this window. Row permissions are checked again when reading. Disabling the plugin hides its notifications but keeps the data. Deleting a record removes its subscriptions and notifications. Collections are tracked by stable ID, so a new collection with the same name does not inherit them. + + + +## API and SDK + +- `GET /notifications?limit=50` returns `{ data, unread, total, readBefore }`. +- `POST /notifications/:id/read` returns 204; another user's or unavailable notification returns 404. +- `POST /notifications/read-all` with `{ before: readBefore }` returns 204. + +An active human account is required. Service accounts may comment with the usual grants but have no human inbox or subscriptions. The SDK exposes list, read, and readAll operations. The admin calls Core through `/api` using an HttpOnly session. + + + +## Plugin contract + +The `notifications` capability requires administrator approval. In a normal HTTP handler, `useAsmblyr` provides following, follow, publish, update, and remove operations. Core derives the plugin namespace and author from the caller and selects recipients through its permission checks. Model handlers do not receive privileged notification operations. + +Publish inside `withRecord` to use the same transaction as the comment. This capability does not send external messages. + +`RecordPanelProps` supports optional `active` and `targetId`. Links use `?panel=plugin:namespace:panel-id&target=entity-id`; the host accepts only a registered panel. The plugin checks access and renders the target. The host remains independent of comment-specific behavior. diff --git a/docs/features/plugins.md b/docs/features/plugins.md index 8f1191b..c57010e 100644 --- a/docs/features/plugins.md +++ b/docs/features/plugins.md @@ -1,66 +1,57 @@ -# Расширения + -Если вы пишете первое расширение, начните с [пошагового руководства](../development/first-extension.md). +# Plugins -Плагин — npm/workspace-пакет. Рабочие пакеты проекта находятся в `packages`, демонстрационные — в `examples/plugins`. Обнаружение идёт по списку пакетов `asmblyr.plugins` в корневом `package.json`, а не по имени каталога. Подключённый пакет должен быть установлен. Установка из UI пока не реализована. +Plugins are npm or workspace packages explicitly listed in the root `asmblyr.plugins` configuration. A directory name alone does not install a plugin. Installing packages through the UI is not supported. -В разделе «Настройки → Расширения» показывается список плагинов. Нажатие на строку или название открывает штатный правый редактор с настройками выбранного плагина и разрешёнными возможностями. Закрытие несохранённого черновика требует подтверждения; во время сохранения редактор блокирует закрытие. Плагины без параметров также открываются для просмотра возможностей. +Settings shows a list of plugins. Clicking one opens its right-side editor with capabilities and settings. Closing a dirty editor requires confirmation; closing is blocked during save. Plugins without configurable parameters still expose their capability view. -## Контракт + -- `plugin.ts`: `definePlugin({})`, без отдельного реестра роутов. -- `server/api/**/*.get.ts` / `.post.ts`: H3 v2 handlers; имя файла определяет путь/метод. -- `server/hooks/*.ts`: lifecycle hooks. -- `server/settings.ts`: декларация настроек. -- Browser entry: отдельный `./ui`, страницы и field interfaces. -- `defineModelContext` + аннотация: явная публикация handler во внутреннем MCP/ассистенте. +## Package contract -`@asmblyr-collaborative/kit` — серверный/браузерный контракт расширений. `@asmblyr-collaborative/sdk` — HTTP-клиент, не доступ к Knex. `useItems(event)` всегда использует права caller; `storage.own` — привилегированное хранилище собственных таблиц, перед ним плагин обязан проверять доменный доступ. +- `plugin.ts` uses `definePlugin`; there is no manual route registry. +- `server/api/**/*.get.ts`, `*.post.ts`, and other method suffixes define H3 v2 file routes. +- `server/hooks` and `server/settings` provide hooks and settings. +- The `./ui` entry contains browser code. +- Model-visible handlers require an explicit `defineModelContext` annotation. -Сгенерированные Kit model handlers экспортируют входные и выходные JSON-контракты -через `GET /schema` с учётом `AccessGate`. CLI создаёт типизированные методы -`client.plugins.[methodId](input)`, которые вызывают исходные HTTP routes. -Права на данные и личное OAuth-подключение проверяются при выполнении. Legacy -actions без выходной схемы в этот клиент не попадают. Подробнее — -[генерация SDK](../reference/cli-guide.md). +Kit is the server/browser extension API. SDK is an HTTP client, not access to Knex. `useItems` keeps the caller's permissions. `storage.own` provides privileged access to the plugin's own tables; the plugin must enforce its domain permissions. -## Декларации и установка +Annotated model handlers expose schemas through `GET /schema`, filtered by AccessGate. Generated clients call `client.plugins.namespace[methodId](input)` over HTTP. Runtime data and OAuth checks still apply. Legacy handlers without output schemas are excluded from generation. -Capabilities заявляются в `asmblyr.manifest.capabilities` и отдельно одобряются для пакета в `asmblyr.pluginPermissions`. По умолчанию нет одобренных возможностей. Это проверка поддерживаемого API Kit; произвольный Node-код она не изолирует. + -Первичная установка таблиц транзакционная, имена `plugin__`. Нельзя занять чужую существующую таблицу. Изменения установленной схемы требуют версионных неизменяемых миграций; отключение пакета не удаляет данные. +## Capabilities and storage -Пакеты этой платформы используют scope `@asmblyr-collaborative`: Core/UI, -Contracts/SDK/Kit/CLI, встроенные Comments/Google Workspace и учебные плагины. -При обновлении старой установки замените имена встроенных пакетов в зависимостях, -`asmblyr.plugins` и ключах `asmblyr.pluginPermissions`, затем выполните -`pnpm install`, `pnpm db:migrate` и сборку перед запуском Core/UI. -Миграция `20261005220000_collaborative_package_names` меняет только npm-имена -известных владельцев в реестре. Она проверяет соответствие namespace и отказывается -от переноса при конфликте владельцев; сторонние пакеты не переименовываются. -Namespace `comments`/`google`, таблицы, настройки, история миграций, URL и ключ -конфигурации `asmblyr` сохраняются. Старый workspace-алиас `@asmblyr/kit` -в Comments нужен неизменяемой ранее выпущенной миграции и сохранён намеренно. +A capability must be declared in the manifest and approved in `pluginPermissions`. None are granted by default. This mechanism is not a Node.js sandbox. -В списке расширений название Google Workspace берётся из каталогов переводов -плагина. `GET /settings/plugins` также возвращает `title` для начального отображения: -RU `settings.title`, заголовок настроек или техническое имя. Каталог выбранного -языка уточняет его после загрузки переводов. Это работает и для плагинов без UI -или собственных настроек. Технические namespace и npm-имя остаются отдельными -идентификаторами. +First installation creates tables transactionally under reserved `plugin__` names. A plugin cannot adopt someone else's table. Versioned migrations are immutable. Disabling a plugin preserves its data. -Таблицы плагинов не показываются в редакторе структуры `/admin/collections`, -включая счётчик коллекций. Плагин продолжает использовать их через свои страницы -и API с обычными проверками доступа; скрытие из редактора не меняет права. +All product packages use the `@asmblyr-collaborative` scope. Existing installations must update dependency names, `asmblyr.plugins`, and permission keys, then run install, migrations, and build before starting Core and UI. Migration `20261005220000_collaborative_package_names` updates only known registry owners, validates namespace/conflicts, and leaves third-party plugins unchanged. -Hooks исполняются до commit в транзакции: ошибка откатывает изменение. Внешние запросы/отправку сообщений нельзя считать гарантированно доставленными; для этого позже нужен outbox. Все контекстные операции необходимо await. +Namespaces, comment/Google tables, settings, history, URLs, and the `asmblyr` configuration key stay compatible. The old `@asmblyr/kit` name in a historical Comments migration is retained because migrations are immutable. -## Где смотреть точные API + -Плагин комментариев использует возможность `notifications` для подписок на запись -и транзакционной публикации событий в отдельный колокольчик. Получателей выбирает -Core; новые права на данные не выдаются. Подробнее — [обсуждения и уведомления](./notifications.md). +## Names and visibility -[Kit](../reference/kit-guide.md), [Hooks](../reference/kit-hooks.md), [Field interfaces](../reference/kit-fields.md). Исходники и примеры — `packages/kit`, `packages/plugin-comments` и `examples/plugins`. Source scanner и production build index должны давать одинаковые маршруты. +Plugin names come from their locale catalogs. `GET /settings/plugins` falls back from Russian `settings.title` to the settings title and then the technical name. Once the selected locale loads, the UI refines the label. This works for plugins without UI or settings. Namespaces and npm identifiers remain unchanged. -Доверяйте устанавливаемому пакету как коду Core: server plugin может импортировать Node-модули и обращаться к окружению, browser plugin работает в контексте админки. Для недоверенных пакетов нужна отдельная процессная/браузерная изоляция. +Plugin-owned tables are hidden from `/admin/collections`, including its counts. They remain available to permitted APIs and plugin pages; hiding them does not change authorization. + + + +## Hooks and notifications + +Before-commit hook failures roll back the transaction. External requests do not guarantee delivery; reliable delivery requires an outbox, which is not implemented. Await every context operation. + +The `notifications` capability supports discussion subscriptions and the core bell in the same transaction. Core selects recipients without granting extra access. + +See [Kit](../reference/kit-guide.md), [hooks](../reference/kit-hooks.md), and [fields](../reference/kit-fields.md). Discovery must behave consistently in source and built package indexes. + + + +## Trust boundary + +Server plugins run with Core's Node.js and environment access. Browser plugins run in the admin's context. Untrusted plugins require separate process or browser isolation. diff --git a/docs/features/realtime.md b/docs/features/realtime.md index eb9e767..fdba66c 100644 --- a/docs/features/realtime.md +++ b/docs/features/realtime.md @@ -1,72 +1,47 @@ # Real-time collaboration -Collaborative Live работает для коллекций и записей, доступных текущему -пользователю. Таблица подписывается на коллекцию и обновляется после сохранения -записи. Карточка показывает участников, состояние соединения, изменения и -редактируемые поля. В клиентском интерфейсе данные перечитываются через обычный -HTTP API: событие содержит только идентификатор, имена разрешённых изменённых -полей и номер события истории. Несохранённый ввод остаётся в редакторе. +Live subscriptions operate on accessible collections and records. Tables refresh after saved changes; record forms show participants, connection state, and active field editing. Record data is fetched over HTTP. Events contain identifiers, permitted field names, and history event IDs, and preserve unsaved edits. ## Presence -SSE-подписка открывает 30-секундную lease присутствия. Core обновляет её пока -соединение активно, удаляет при закрытии и проверяет право чтения при -подключении и при каждом событии. Участники одного человека объединяются; -отображаются имя, аватар и число окон. После аварийного закрытия остаток lease -исчезает не позднее 30 секунд. Ранее доступный HTTP presence API сохранён. -Каждое продление активной SSE-подписки отправляет актуальный состав участников; -так исчезают и окна, чья lease истекла без события выхода. -Компоненты админки разделяют один поток на scope. Новый подписчик сразу получает -текущее состояние соединения и последний состав участников. При обрыве этот -снимок сбрасывается до получения актуальных данных после переподключения. - -## Live updates и разрешения - -Core публикует `record.created`, `record.updated`, `record.deleted` из -транзакции записи. PostgreSQL доставляет уведомление только после commit и между -экземплярами Core. Подписка на запись проверяет read коллекции и строки по тем -же правилам, что HTTP `/items`. Имена полей фильтруются по фактически видимым -полям строки; sensitive поля не объявляются. Подписка на коллекцию получает -только `collection.changed`, если изменённая строка доступна читателю. При -условных правах удаление не объявляется, поскольку удалённую строку уже нельзя -проверить. Изменение прав обнаруживается при следующем событии или продлении -lease (до 20 секунд), после чего поток закрывается. - -## Field locks и конфликты - -Фокус в редактируемом поле получает lease на 30 секунд. Core проверяет read и -update поля и строки. Вторая сессия получает `409 FIELD_LOCKED`; блокировка -одного поля не мешает редактировать другие. При выходе из поля lease удаляется, -а при обрыве сети истекает автоматически. Клиент повторяет запрос каждые 10 -секунд, пока поле активно. - -Блокировка помогает людям видеть редактирование, но не гарантирует сохранение. -Атомарный draft commit по-прежнему сравнивает `expectedValues` изменяемых полей -и возвращает `409 ITEM_CHANGED`, когда чужая правка затрагивает то же поле. -Редактор оставляет черновик и показывает существующий выбор значений. Обычный -`PATCH` без precondition сохраняет прежний контракт. -Чистая форма принимает новое состояние записи только если к моменту ответа HTTP -у пользователя всё ещё нет несохранённых правок. Вместе с полями обновляется -база `expectedValues`; если ввод начался во время запроса, ответ не заменяет форму. - -## Reconnect и SDK - -SDK `client.realtime.connect()` возвращает типизированное соединение. -`subscribe(scope, callback)` возвращает функцию отписки; `on(type, callback)` -и `onState(callback)` возвращают очистку слушателя. `locks.acquire`, `refresh` -и `release` работают с `{ collection, recordId, field, clientId }`. -При обрыве SDK переподключается с backoff и jitter, повторно регистрирует -presence и посылает `collection.changed`, чтобы клиент перечитал данные. -Это событие получают и callback подписки, и обработчики `on("collection.changed")`. -Повторные ID в одном потоке пропускаются. Отписка и `close()` освобождают -соединение. Браузерная админка открывает `/api/realtime/stream` напрямую в Core -с HttpOnly cookie; прямому клиенту Core нужен действующий пользовательский Bearer token. - -## Границы - -Поток передаёт события операций через Core, включая Kit, но прямой SQL в -пользовательские таблицы не создаёт событий. Комментарии и изменения прав пока -не имеют собственных live-событий. SSE не хранит очередь событий: после -переподключения клиент перечитывает текущие данные. Presence и locks не -поддерживают сервисные ключи. Workspace в envelope равен `null`, поскольку -рабочие пространства группируют коллекции и не изолируют данные. +An SSE subscription creates a 30-second presence lease. It is renewed while active and removed on close. Core checks row read access on connection and events. Multiple windows of the same human are combined into one participant with name, avatar, and window count. A crashed window disappears when its lease expires, within 30 seconds. The legacy HTTP presence API remains available. + +Each renewal sends the current participant list, also removing windows that expired without a leave event. Admin components share one stream per scope. A new subscriber immediately receives connection state and the latest presence snapshot. Disconnection clears that snapshot until fresh data arrives after reconnection. + + + +## Live updates and permissions + +Core emits `record.created`, `record.updated`, and `record.deleted` inside the write transaction. PostgreSQL delivers notifications after commit and across Core instances. + +Record subscriptions use the same collection and row read rules as HTTP `/items`. Field names are filtered by actual visibility; sensitive fields are excluded. A collection subscription receives only `collection.changed`, and only for a readable changed row. With conditional permissions, deletion is not announced because the deleted row can no longer be checked. + +Revoked permissions are detected on the next event or lease renewal, within 20 seconds, and close the stream. + + + +## Field locks and conflicts + +Focusing an editable field acquires a 30-second lease. Core checks read and update permissions for the field and row. A second session receives `409 FIELD_LOCKED`; other fields remain editable. Leaving the field releases its lease; network loss lets it expire. The client renews an active field every 10 seconds. + +Locks communicate editing activity but do not guarantee a successful save. Atomic draft commits still compare `expectedValues` for changed fields and return `409 ITEM_CHANGED` when another edit touches the same field. The editor retains the draft and offers the existing value choices. Ordinary `PATCH` without a precondition keeps its existing contract. + +A clean form accepts refreshed data only if it is still clean when the HTTP response arrives. The `expectedValues` baseline updates with the fields. If typing started during the request, the response does not replace the form. + + + +## Reconnection and SDK + +`client.realtime.connect()` returns a typed connection. `subscribe(scope, callback)` returns an unsubscribe function. `on(type, callback)` and `onState(callback)` return listener cleanup functions. `locks.acquire`, `refresh`, and `release` accept `{ collection, recordId, field, clientId }`. + +The SDK reconnects with backoff and jitter, registers presence again, and emits `collection.changed` so clients reread data. Both subscription callbacks and global `on("collection.changed")` handlers receive this event. Duplicate IDs within one stream are ignored. Unsubscribing and `close()` release the connection. + +The browser admin opens `/api/realtime/stream` directly in Core using its HttpOnly cookie. A direct Core client needs a valid human Bearer token. + + + +## Boundaries + +Events cover Core operations, including Kit writes. Direct SQL writes do not emit events. Comments and permission changes do not yet have dedicated live events. SSE does not retain an event queue; reconnecting clients fetch current data. + +Presence and locks do not support service keys. The envelope's workspace is `null`: workspaces group collections and do not isolate data. diff --git a/docs/features/settings.md b/docs/features/settings.md index ed2f904..692d908 100644 --- a/docs/features/settings.md +++ b/docs/features/settings.md @@ -1,62 +1,51 @@ -# Настройки админки + -Общая точка входа — `/admin/settings`. Она видна, если есть право хотя бы на одну страницу настроек. Навигация и страницы содержат только разрешённые разделы; Core проверяет право повторно на каждом запросе. +# Admin settings -На небольших экранах разделы выбираются в компактном меню над содержимым. -На широком экране они остаются в боковой навигации; обе версии показывают -только доступные пользователю разделы. «Термины» — словарь для ассистента. +`/admin/settings` is visible when the user can access at least one settings page. Navigation and pages include only permitted sections. Core checks access again on every request. -Библиотека файлов находится в главном меню на `/files` и не дублируется в меню -или обзоре системных настроек. Права `files/read` и `files/update` по-прежнему -назначаются в политиках и видны при просмотре доступа пользователя. Если -доступна только библиотека файлов, вход в `/admin/settings` перенаправляет туда. -Настройка S3/Yandex Object Storage находится в разделе «Подключения». +Small screens use a compact section menu above the content; wide screens use side navigation. Both show only accessible sections. Terms is the assistant's shared glossary. -Суперпользователю также доступен раздел [подключений и защиты секретов](./connections.md) -на `/admin/settings/integrations`: S3/Yandex Object Storage, AI-провайдер и локальное -шифрование либо Yandex KMS. Этот раздел не делегируется правами остальных настроек. +The file library lives at `/files` in the main menu and is not duplicated in system settings. Policies still grant files/read and files/update, and the user access viewer shows them. If the library is the only available section, `/admin/settings` redirects there. S3/Yandex Object Storage configuration lives under Connections. -Опциональный [мониторинг Sentry](./monitoring.md) находится отдельно на -`/admin/settings/monitoring`: ошибки, производительность, SQL spans и локальные -p95/p99. Он также доступен только суперпользователю и по умолчанию выключен. +Superusers manage [connections and secret protection](./connections.md) at `/admin/settings/integrations`: S3/Yandex Object Storage, AI provider, and local encryption or Yandex KMS. Other settings grants cannot delegate this section. -| Раздел | Ключ в permission | Возможности | -| ---------------- | ----------------- | ------------------------------------------------------------ | -| Пользователи | users | Список, приглашения, просмотр эффективного доступа | -| Политики | policies | Permissions, политики и назначения | -| Расширения | plugins | Список загруженных пакетов, декларации и доступные настройки | -| Ассистент | assistant | Настройки модели/инструкций и телеметрия | -| Термины | terms | Общий справочник смыслов для ассистента | -| Сервисы | services | Аккаунты, ключи и федерации | -| OAuth-приложения | oauth | Приложения, секреты, правила допуска | +Optional [Sentry monitoring](./monitoring.md) lives at `/admin/settings/monitoring`: errors, performance, SQL spans, and local p95/p99. It also requires a superuser and is disabled by default. -Назначение: откройте политику → блок настроек → выберите для разделов «Нет доступа», «Просмотр» или «Изменение» → сохраните → назначьте политику пользователю. Права на коллекции настраиваются отдельно. +| Section | Permission key | Features | +| ------------------ | -------------- | ------------------------------------------------- | +| Users | users | List, invitations, effective access | +| Policies | policies | Permissions, policies, assignments | +| Plugins | plugins | Loaded packages, declarations, available settings | +| Assistant | assistant | Model/instructions and telemetry | +| Terms | terms | Shared glossary for the assistant | +| Services | services | Accounts, keys, federations | +| OAuth applications | oauth | Applications, secrets, admission rules | -Пользователь только с правами настроек попадает в настройки без требования доступа к данным. Без данных и разделов видит отдельный экран отсутствия доступа. Личный профиль остаётся на `/settings`. +To grant access, open a policy, select No access, View, or Edit for each settings section, save it, and assign the policy. Collection grants are configured separately. -Старые URL `/access`, `/services`, `/oauth-apps` перенаправляются в новые разделы. Системные права доступны через `GET /settings/access`; каталог данных для редактора политики берётся через ограниченную проекцию `/settings/options/collections`. +A user with settings-only grants can open settings without data access. Users with neither see a no-access screen. Personal preferences remain at `/settings`. -Управление структурой и рабочими пространствами пока не делегируется этими флажками. Установка пакетов и одобрение plugin capabilities остаются в конфигурации проекта. Для каждого раздела доступны отдельные права `read` и `update`. `update` включает просмотр; разрешения разных политик объединяются. Изменение политик и управление сервисами дополнительно ограничены правилами делегирования ниже. `read` разрешает списки, подробности, телеметрию и ограниченные справочники для селекторов, но не сохранение, приглашения, назначения, выпуск/отзыв ключей, федераций или замену OAuth-секрета. GET не возвращает ранее выданные секреты. +Legacy `/access`, `/services`, and `/oauth-apps` URLs redirect to the current sections. `GET /settings/access` exposes settings permissions; the policy editor uses the restricted `/settings/options/collections` catalog. -`GET /settings/access` возвращает `sections` (разделы для просмотра) и `editableSections` (разделы для изменения). Навигация использует первый список; страницы передают режим просмотра в свои формы. В режиме просмотра доступно раскрытие правой панели, условий и вкладок; управляющие действия скрыты, значения защищены от изменения. Сервер проверяет отдельное право изменения независимо от UI. `GET /users/:id/access` показывает оба списка для выбранного пользователя. +Structure and workspace management are not delegated by these switches. Package installation and plugin capability approval remain project configuration. Every section supports read and update; update includes viewing, and policies combine grants. Policy and service management also enforce delegation rules. -Ответ также содержит `canManagePolicies` (только superuser) и `delegatablePolicyIds` (явный набор готовых политик для назначения). `policies/update` у менеджера разрешает только назначения из этого набора; состав прав остаётся для просмотра. Для сервисов набор ограничивает и назначения, и выпуск ключей/федерации. Superuser не ограничен массивом, который для него пуст. Настройка набора находится в правой панели доступа пользователя; подробнее — [делегирование](./access.md#ограниченное-назначение-политик). +Read allows lists, details, telemetry, and restricted selector catalogs. It cannot save, invite, assign, issue/revoke keys or federations, or replace OAuth secrets. GET never returns previously issued secrets. -Миграция `20261003040000_policy_delegations.cjs` добавляет личный разрешённый набор, пустой для существующих пользователей. Старые назначения сохраняются; не-superuser больше не меняет состав политик и должен получить явный набор от администратора для новых назначений. Down отказывается удалять непустые настройки делегирования. +`GET /settings/access` returns `sections` for viewing and `editableSections` for modification. Navigation uses the first list; pages pass read-only mode to forms. Read-only users can open side panels, conditions, and tabs, while values are protected and mutation actions hidden. The server independently checks update access. `GET /users/:id/access` shows both lists for the selected user. -Миграция `20261003030000_settings_read_permissions.cjs` добавляет действие `read` и уникальность `(section, action)`, не меняя существующие grants. Down отказывается удалять поддержку просмотра, пока есть permissions `read`. +The response also includes `canManagePolicies` (superusers only) and `delegatablePolicyIds` (the explicit allowed set of existing policies). For managers, policies/update permits assignments only from this set; policy contents remain read-only. For services, the set restricts assignments, keys, and federation. Superusers are unrestricted even though their array is empty. See [delegation](./access.md#bounded-policy-delegation). -Миграция `20261003010000_settings_permissions.cjs` сохраняет существующие права коллекций и добавляет альтернативную цель section. Down откажется работать, пока существуют section permissions: молча терять назначенные права нельзя. +Migration `20261003040000_policy_delegations.cjs` adds empty personal allowed sets for existing users, preserving assignments. Non-superusers must receive an explicit set for new assignments and can no longer edit policy contents. Down refuses to remove nonempty delegation settings. -## Оформление и язык +Migration `20261003030000_settings_read_permissions.cjs` adds read and uniqueness on `(section, action)` without changing existing grants. Down refuses while read permissions exist. -Во вкладке «Внешний вид» личных настроек `/settings` режим (`light`, `dark`, `system`) выбирается -отдельно от стиля (`neutral`, `ocean`, `coral`). Каждый цветовой стиль поддерживает -светлый и тёмный режим. Настройки сохраняются в профиле через -`GET/PATCH /users/me/preferences`; частичный PATCH не сбрасывает остальные значения. -Пустой профиль возвращает `theme: null`, `style: "neutral"`, `locale: "ru"`. +Migration `20261003010000_settings_permissions.cjs` preserves collection grants and adds section targets. Down refuses while section permissions exist, preventing silent access loss. -Интерфейс поддерживает основу локализации RU/EN; подробнее — -[переводы и их API](./localization.md). Старые адреса `/system-settings/...` -перенаправляются на `/admin/settings/...`. Личные настройки, коллекции, файлы -и страницы расширений сохраняют свои адреса. + + +## Appearance and language + +The Appearance tab at `/settings` separates mode (`light`, `dark`, `system`) from style (`neutral`, `ocean`, `coral`). Every color style supports light and dark modes. Preferences use `GET/PATCH /users/me/preferences`; a partial PATCH preserves other values. An empty profile returns `theme: null`, `style: "neutral"`, and `locale: "ru"`. + +The interface supports RU/EN; see [translations and their API](./localization.md). Old `/system-settings/...` URLs redirect to `/admin/settings/...`. Personal settings, collections, files, and plugin pages keep their addresses. diff --git a/docs/features/workspaces.md b/docs/features/workspaces.md index c4b6a97..835ff2a 100644 --- a/docs/features/workspaces.md +++ b/docs/features/workspaces.md @@ -1,13 +1,17 @@ -# Рабочие пространства и представления + -Workspace объединяет выбранные коллекции для удобства работы. Переключатель находится в шапке sidebar; текущий выбор сохраняется за пользователем. Коллекции можно организовать папками и виртуальной вложенностью в другие коллекции. +# Workspaces and views -Workspace фильтрует навигацию, но не изолирует данные между арендаторами. Права определяются политиками. Пользователь видит только доступные коллекции; скрытие коллекции в меню не является запретом на API или связь. +Workspaces group selected collections for convenient navigation. The switcher lives in the sidebar header, and the selected workspace is saved per user. Collections can be organized into folders and virtual hierarchies. -## Сохранённые виды +A workspace filters navigation; it does not isolate tenants. Policies determine access. Users see only permitted collections, and hiding a collection from a menu does not prohibit API access or relationships. -Виды таблицы имеют область personal, collection или workspace. Личные редактирует владелец, общие — superuser. Определение проверяется против доступной схемы/полей. При потере прав или изменении схемы несовместимый вид не должен возвращать скрытые данные. + -Default выбирается по приоритету personal → workspace → collection. Количество — до 30 видов на scope/коллекцию. Изменения колоночных настроек и профиля используют пользовательские preferences; не все произвольные UI-состояния имеют общий механизм хранения. +## Saved views -Фильтр вида лишь уточняет выборку: он не задаёт row-level security. Права на изменение самих workspaces остаются у superuser. +Table views have personal, collection, or workspace scope. Owners edit personal views; superusers edit shared ones. Definitions are validated against accessible schema and fields. After permission or schema changes, an incompatible view must not expose hidden data. + +Default priority is personal → workspace → collection. Each scope/collection supports up to 30 views. Column settings and profile choices use user preferences; arbitrary UI state does not all share a persistence mechanism. + +A view filter narrows a query; it does not define row-level security. Editing workspaces themselves requires a superuser. diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md index 7ac800b..34ff367 100644 --- a/docs/guide/deployment.md +++ b/docs/guide/deployment.md @@ -1,63 +1,69 @@ -# Развёртывание и обновление - -Для локальной разработки начните с [первого запуска](./getting-started.md). -Основная поставка использует отдельные образы Core и UI из одного коммита; -PostgreSQL 17+ и S3-совместимое хранилище подключаются отдельно. Готовые сборки -публикуются в `ghcr.io/asmblyr/collaborative-core` и -`ghcr.io/asmblyr/collaborative-ui`; закрепляйте оба образа одной версии по digest. -Одна собственная установка предназначена для одной команды; workspace не создаёт -отдельный tenant. - -| Сценарий | Где начать | -| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Основная поставка с внешними PostgreSQL/S3 | [`deploy/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.yaml) и [`deploy/env.example`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/env.example) | -| Локальный стенд с PostgreSQL и MinIO | [`deploy/local/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/local/compose.yaml) | -| Совместимый монолитный образ | [`deploy/compose.monolith.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.monolith.yaml) | - -Точные команды сборки и запуска, настройки портов, GHCR и контейнерные проверки — -в [руководстве `deploy`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.md). -Основной Compose выполняет миграции отдельным коротким запуском образа Core -перед стартом приложения. Он публикует общий адрес на loopback-порту 3000 -по умолчанию. Не переносите локальные секреты и HTTP origin в публичную -установку. - -## Конфигурация - -Для основной поставки скопируйте `deploy/env.example` в приватный файл вне -репозитория. Core читает `DATABASE_URL` и остальные параметры из окружения; -полный шаблон с комментариями — + + +# Deployment and upgrades + +For local development, start with [first-time setup](./getting-started.md). +The primary distribution uses separate Core and UI images from the same commit; +PostgreSQL 17+ and S3-compatible storage are connected separately. Images are +published as `ghcr.io/asmblyr/collaborative-core` and +`ghcr.io/asmblyr/collaborative-ui`; pin both to the same release and their +respective digests. Each installation serves one team; a workspace is not a tenant. + +| Scenario | Start here | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Primary deployment with external PostgreSQL/S3 | [`deploy/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.yaml) and [`deploy/env.example`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/env.example) | +| Local environment with PostgreSQL and MinIO | [`deploy/local/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/local/compose.yaml) | +| Legacy monolithic image | [`deploy/compose.monolith.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.monolith.yaml) | + +Build and run commands, port settings, GHCR, and container checks are covered in +the [deployment README](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.md). +The primary Compose setup runs migrations in a separate short-lived Core container +before starting the application. By default, it exposes the shared address on +loopback port 3000. Do not carry local secrets or an HTTP origin into a public installation. + + + +## Configuration + +For the primary deployment, copy `deploy/env.example` to a private file outside +the repository. Core reads `DATABASE_URL` and other settings from the environment. +See the complete commented template in [`apps/core/.env.example`](https://github.com/Asmblyr/Collaborative/blob/main/apps/core/.env.example). -Основные группы настроек: - -- `ASMBLYR_SETUP_TOKEN` нужен до создания первого superuser. -- `AUTH_UI_URL` задаёт точный внешний origin UI и обязателен в production. -- `SECRETS_LOCAL_KEY` защищает сохраняемые секреты интеграций. -- S3 можно настроить в системных настройках или через `FILES_STORAGE`, `FILES_BUCKET` - и параметры выбранного провайдера. Bucket создаёт оператор. -- `OPENAI_API_KEY` и `OPENAI_API_MODEL` включают опционального ассистента, - если `ASSISTANT_ENABLED` не установлен в `false`. -- `OAUTH_ISSUER_URL` и `OAUTH_KEYS_FILE` относятся к Asmblyr как OAuth provider; - ключи создаются отдельно и сохраняются между обновлениями. - -Значения и ограничения описаны в env-шаблонах и на страницах -[интеграций](../features/integrations.md), [файлов](../features/files.md) и -[ассистента](../features/assistant.md). Секреты и ключи держите вне Git и образов. - -## Публичная установка - -Разместите UI и Core API за HTTPS reverse proxy. PostgreSQL и bucket должны -оставаться приватными; основной Compose публикует порты только на loopback. -Оператор настраивает TLS, IAM, резервные копии и мониторинг. `GET /health` -проверяет процесс, `GET /ready` — БД и обязательные миграции. Скрипты -резервирования локального стенда не обслуживают внешний облачный bucket. - -## Обновление и восстановление - -Перед обновлением сохраните согласованную копию PostgreSQL и объектов, проверьте -восстановление на отдельной установке и выполните миграции **до запуска нового Core**. -Старые миграции не редактируются; `db:rollback` может удалить данные и не заменяет -восстановление из копии. Откат образа возможен только при совместимой схеме. -Для дополнительного локального стенда доступны -[команды backup/restore и проверка статуса](../development/operations.md#копии-локальной-docker-установки). - -Рекомендации по защите установки — в [границах безопасности](../security/overview.md). +The main settings are: + +- `ASMBLYR_SETUP_TOKEN` is required until the first superuser is created. +- `AUTH_UI_URL` specifies the exact external UI origin and is required in production. +- `SECRETS_LOCAL_KEY` protects stored integration secrets. +- Configure S3 in system settings or with `FILES_STORAGE`, `FILES_BUCKET`, and + provider-specific settings. The operator creates the bucket. +- `OPENAI_API_KEY` and `OPENAI_API_MODEL` enable the optional assistant unless + `ASSISTANT_ENABLED` is `false`. +- `OAUTH_ISSUER_URL` and `OAUTH_KEYS_FILE` configure Collaborative as an OAuth + provider. Generate signing keys separately and preserve them across upgrades. + +Values and limits are documented in the environment templates and the guides for +[integrations](../features/integrations.md), [files](../features/files.md), and +[the assistant](../features/assistant.md). Keep secrets and keys out of Git and images. + + + +## Public installations + +Put UI and Core API behind an HTTPS reverse proxy. Keep PostgreSQL and the bucket +private; the primary Compose setup binds ports to loopback only. The operator owns +TLS, IAM, backups, and monitoring. `GET /health` checks the process; `GET /ready` +checks the database and required migrations. Local backup scripts do not manage +an external cloud bucket. + + + +## Upgrades and recovery + +Before an upgrade, take a consistent backup of PostgreSQL and objects, test recovery +in a separate installation, and apply migrations **before starting the new Core**. +Do not edit old migrations. `db:rollback` can delete data and does not replace +restoring a backup. An image rollback is possible only when its schema is compatible. +The optional local environment has +[backup/restore commands and status checks](../development/operations.md). + +For installation hardening, see [security boundaries](../security/overview.md). diff --git a/docs/guide/features.md b/docs/guide/features.md index bb44af0..576ea4a 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -1,22 +1,26 @@ -# Карта возможностей + -Карта текущей реализации. Колонка «Ограничения» описывает границы функций; сроки дальнейших изменений не закреплены. +# Feature map -| Область | Реализовано | Ограничения | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| Коллекции | Создание, single/multiple, тип ключа, поля, подписи, формы, индексы, поиск, системные даты и состояние | Подключение произвольных существующих таблиц/views; перенос между БД как продукт | -| Записи | Таблица, страницы, сортировка, колонки, боковой редактор, адрес записи, пакетные действия, атомарный draft, live invalidation | Внешний SQL не создаёт live-события | -| Связи | M:1, обратные 1:M, M:N через junction, выбор/создание/отвязка, поля связи | Нет обещания произвольной глубины вложенных запросов | -| Фильтры | Группы AND/OR, типовые условия, поля связей, сохранённые виды | Условия permissions: прямые поля, И/ИЛИ и параметры участника запроса | -| Права | Действия/поля/условия коллекции, переиспользуемые permissions, политики M:N, просмотр настроек и ограниченное назначение готовых политик | DDL остаётся у superuser; per-object file ACL | -| Пользователи | Setup, вход по ссылке, пароль/passkey, профиль, сессии, SSO, восстановление администратором | Отдельный MFA flow, самостоятельное email-восстановление, LDAP/SAML | -| Интеграции | Сервисные аккаунты/ключи, GitLab CI federation, собственный OAuth/OIDC provider | Универсальная федерация любых issuer; публичный MCP | -| Файлы | S3/YC, метаданные, ссылки, files/read и update, проверка чтения | Per-object ACL, antivirus/преобразования, reconciliation DB/S3 | -| Пространства | Переключатель, набор коллекций, пользовательский выбор | Не являются tenant isolation | -| Ассистент | Контекст, stream, инструменты, model-handlers, telemetry, общие лимиты и опциональный retention | Денежный бюджет провайдера; доказанная устойчивость к prompt injection | -| Плагины | pnpm/local packages, H3 file routes, типизированный Kit, миграции, hooks, UI/fields/settings, MCP annotations | Песочница, marketplace/install из UI, гарантированная доставка внешних событий | -| SDK | HTTP items list/get/create/update/delete/commit, users.me, presence touch/leave и realtime stream/locks | Обёртки остальных системных endpoints, генерация схем коллекций | -| Коллаборация | Проверка исходных значений, выбор при конфликте, SSE presence, live updates, TTL-блокировки полей | Нет синхронизации комментариев и прав через live protocol | -| Протоколы | HTTP и SSE; OIDC протокол отдельно | Общий GraphQL API | +This table describes the current implementation. The limits column records +feature boundaries; it does not promise dates for future changes. -Подробности — на функциональных страницах, требования допуска — в [матрице](../security/access-matrix.md). +| Area | Implemented | Limits | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Collections | Creation, single/multiple mode, key types, fields, labels, forms, indexes, search, system dates and status; import of read-only materialized views | Import of arbitrary existing tables or ordinary views; database migration as a product feature | +| Records | Tables, pagination, sorting, columns, side-panel editor, record URLs, bulk actions, atomic drafts, live invalidation | External SQL does not produce live events | +| Relationships | M:1, inverse 1:M, M:N through junctions, select/create/unlink, relationship fields | Arbitrarily deep nested queries are not guaranteed | +| Filters | AND/OR groups, typed conditions, relationship fields, saved views | Permission conditions use direct fields, AND/OR, and request-principal parameters | +| Access | Collection actions/fields/conditions, reusable permissions, M:N policies, settings read access, bounded assignment of existing policies | DDL remains superuser-only; no per-object file ACL | +| Users | Setup, invitation links, passwords/passkeys, profile, sessions, SSO, administrator-assisted recovery | No separate MFA flow, self-service email recovery, or LDAP/SAML | +| Integrations | Service accounts/keys, GitLab CI federation, built-in OAuth/OIDC provider | No universal federation for arbitrary issuers or public MCP endpoint | +| Files | S3/YC, metadata, references, files/read and update, read checks | No per-object ACL, antivirus/transformation pipeline, or DB/S3 reconciliation | +| Workspaces | Switcher, collection sets, per-user selection | Not tenant isolation | +| Assistant | Context, streaming, tools, model handlers, telemetry, shared limits, optional retention | Provider spending limits remain external; resistance to prompt injection is not proven | +| Plugins | pnpm/local packages, H3 file routes, typed Kit, migrations, hooks, UI/fields/settings, MCP annotations | No sandbox, marketplace/UI installation, or guaranteed external event delivery | +| SDK | HTTP items list/get/create/update/delete/commit, users.me, presence and realtime/locks; schema generation and fluent queries through the CLI | Not every system endpoint has a dedicated client wrapper | +| Collaboration | Baseline checks, conflict resolution, SSE presence, live updates, TTL field locks | Comments and policy changes are not synchronized through the live protocol | +| Protocols | HTTP and SSE; separate OIDC protocol | No general GraphQL API | + +Feature pages explain the details. Access requirements are in the +[access matrix](../security/access-matrix.md). diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 3c4b5d0..6fcbbf4 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -1,9 +1,11 @@ -# Локальный запуск + -Нужны Node.js 22+, pnpm 11.13.1, Docker Compose. Команды выполняются из корня репозитория. +# Local setup -На Unix shell используйте `cp -n apps/core/.env.example apps/core/.env` вместо -`Copy-Item` ниже. Существующий файл окружения сохраняйте. +Install Node.js 22+, pnpm 11.13.1, and Docker Compose. Run commands from the repository root. + +In a Unix shell, use `cp -n apps/core/.env.example apps/core/.env` instead of +`Copy-Item` below. Preserve any existing environment file. ```powershell pnpm install --frozen-lockfile @@ -13,29 +15,41 @@ pnpm db:migrate pnpm dev ``` -Копируйте пример только при отсутствии своего `.env`. До первой настройки задайте в нём случайный `ASMBLYR_SETUP_TOKEN` длиной не менее 32 символов. Не сохраняйте секрет в Git. +Copy the example only if you do not have an `.env` file. Before first-time setup, +set a random `ASMBLYR_SETUP_TOKEN` of at least 32 characters in that file. Keep it out of Git. -| Компонент | Адрес | +| Component | Address | | ---------------- | ----------------------------- | -| Админка | `http://localhost:3000` | +| Admin UI | `http://localhost:3000` | | Core | `http://127.0.0.1:3001` | -| PostgreSQL | 127.0.0.1:5433 | -| Первая настройка | `http://localhost:3000/setup` | +| PostgreSQL | `127.0.0.1:5433` | +| First-time setup | `http://localhost:3000/setup` | + +At `/setup`, enter the setup secret, email, and a password of at least 12 characters. +This creates the first superuser. Once a user exists, the endpoint closes and you +can remove the setup secret from configuration. Public password registration is not available. -В `/setup` введите setup secret, email и пароль от 12 символов. Создаётся первый superuser. После появления пользователя endpoint закрывается; секрет можно удалить из конфигурации. Публичной регистрации по паролю нет. +`pnpm dev` first builds SDK, Kit, and bundled plugins. The server-side UI setting +`CORE_URL` points to local Core by default. `GET /health` checks the process; +`/ready` checks the database and required migrations. -`pnpm dev` предварительно собирает SDK, Kit и встроенные плагины. `CORE_URL` в серверной конфигурации UI по умолчанию указывает на локальный Core. `GET /health` проверяет процесс, `/ready` — БД и обязательные миграции. +`pnpm db:down` stops PostgreSQL without deleting its volume. Do not add `-v` if +you need to retain the data. Run `pnpm db:migrate` before upgrading an existing +installation; rolling back DDL can destroy data. -`pnpm db:down` останавливает PostgreSQL без удаления volume. Не добавляйте `-v`, если данные нужно сохранить. Перед обновлением существующего проекта выполняйте `pnpm db:migrate`; откат DDL может уничтожить данные. +For Docker Compose installation, external PostgreSQL/S3, and production upgrades, +see [deployment](./deployment.md). -Установка Docker Compose, внешние PostgreSQL/S3 и порядок production-обновлений — -в [руководстве по развёртыванию](./deployment.md). + -## Документация +## Documentation ```powershell pnpm docs:dev pnpm docs:build ``` -Первый запуск генерирует справочники. Статический сайт получается в `docs/.vitepress/dist`; его можно разместить на любом статическом хостинге. OpenAPI доступен отдельным JSON. Страницы можно печатать средствами браузера; отдельного конвейера PDF-книги пока нет. +The first command generates the reference pages. The static site is built into +`docs/.vitepress/dist` and can be served by any static host. English is at `/` and +Russian at `/ru/`. OpenAPI is also available as JSON. Use the browser's print +feature to print pages; there is no separate PDF book pipeline. diff --git a/docs/index.md b/docs/index.md index ee82394..cb32bdd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,58 +1,63 @@ # Collaborative -Открытая платформа для работы с данными в PostgreSQL. Админка объединяет структуру -коллекций, редакторы записей, права команды, файлы и интеграции. -Проект находится в ранней бете; одна команда использует отдельную установку. +An open-source workspace for PostgreSQL data. The admin interface brings together +collection schemas, record editors, team permissions, files, and integrations. +The project is in early beta; each team runs its own installation. -[GitHub](https://github.com/Asmblyr/Collaborative) · [Релизы](https://github.com/Asmblyr/Collaborative/releases) +[GitHub](https://github.com/Asmblyr/Collaborative) · [Releases](https://github.com/Asmblyr/Collaborative/releases) -## Начать + -- [Возможности и ограничения](./guide/features.md) -- [Первый запуск](./guide/getting-started.md) -- [Docker, production и обновления](./guide/deployment.md) +## Get started + +- [Features and limits](./guide/features.md) +- [First-time setup](./guide/getting-started.md) +- [Docker, production, and upgrades](./guide/deployment.md) - [HTTP API](./reference/http.md) -- [TypeScript SDK](./reference/sdk-guide.md) и [CLI](./reference/cli-guide.md) - -## Работа с продуктом - -Руководства по [данным](./features/data.md), [правам](./features/access.md), -[пользователям](./features/identity.md), [файлам](./features/files.md), -[настройкам](./features/settings.md), [пространствам](./features/workspaces.md), -[мониторингу](./features/monitoring.md), -[интеграциям](./features/integrations.md), [ассистенту](./features/assistant.md), -[обсуждениям и уведомлениям](./features/notifications.md), -[локализации](./features/localization.md) и [расширениям](./features/plugins.md) -описывают текущие контракты и их границы. -[Личные подключения Google Drive и Sheets](./features/google-workspace.md) описаны отдельно. -[Совместная работа в реальном времени](./features/realtime.md) описывает -присутствие, изменения записей, блокировки полей и ограничения транспорта. - -| Задача | Где читать | -| --------------------------------------------------- | ------------------------------------------------------------- | -| Коллекции, поля, связи, записи, фильтры и поиск | [Данные](./features/data.md) | -| Виды таблицы и рабочие пространства | [Пространства](./features/workspaces.md) | -| Пользователи, вход и сессии | [Авторизация](./features/identity.md) | -| Политики, разрешения и условия строк | [Права](./features/access.md) | -| Файлы и S3 | [Файлы](./features/files.md) | -| Аутентификация API, страницы, ошибки и примеры кода | [HTTP](./reference/http.md) и [SDK](./reference/sdk-guide.md) | - -## Разработка и эксплуатация - -- [Локальная разработка и проверки](./development/local-development.md) -- [Архитектура](./development/architecture.md) -- [Архитектура Collaborative Live](./architecture/realtime.md) -- [Первое расширение](./development/first-extension.md) -- [Архитектура ассистента и model context](./development/assistant-architecture.md) -- [Опубликованные пакеты](./reference/packages.md) -- [Сопровождение документации](./development/documentation.md) -- [Эксплуатация и восстановление](./development/operations.md) -- [Границы безопасности](./security/overview.md) -- [Визуальная идентичность и публичные assets](./assets/brand/README.md) -- [Участие в проекте](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md) - -Руководства по Kit и SDK поддерживаются в исходных пакетах и копируются в -`reference` при генерации сайта. Технические детали размещаются рядом с -контрактом; эта главная страница служит маршрутом к ним. Основной язык сейчас -русский; при добавлении других языков следует сохранить общие исходные -контракты и отдельную навигацию для каждого языка. +- [TypeScript SDK](./reference/sdk-guide.md) and [CLI](./reference/cli-guide.md) + + + +## Using the product + +The guides cover [data](./features/data.md), [permissions](./features/access.md), +[users](./features/identity.md), [files](./features/files.md), +[settings](./features/settings.md), [workspaces](./features/workspaces.md), +[monitoring](./features/monitoring.md), [integrations](./features/integrations.md), +[the assistant](./features/assistant.md), +[discussions and notifications](./features/notifications.md), +[localization](./features/localization.md), and [plugins](./features/plugins.md). +Each describes the current contract and its limits. +[Personal Google Drive and Sheets connections](./features/google-workspace.md) have a separate guide. +[Live collaboration](./features/realtime.md) covers presence, saved-record updates, +field locks, and transport limits. + +| Task | Read more | +| ---------------------------------------------------------------- | --------------------------------------------------------------- | +| Collections, fields, relationships, records, filters, and search | [Data](./features/data.md) | +| Table views and workspaces | [Workspaces](./features/workspaces.md) | +| Users, sign-in, and sessions | [Identity](./features/identity.md) | +| Policies, permissions, and row conditions | [Access](./features/access.md) | +| Files and S3 | [Files](./features/files.md) | +| API authentication, pagination, errors, and examples | [HTTP](./reference/http.md) and [SDK](./reference/sdk-guide.md) | + + + +## Development and operations + +- [Local development and checks](./development/local-development.md) +- [Architecture](./development/architecture.md) +- [Collaborative Live architecture](./architecture/realtime.md) +- [Your first plugin](./development/first-extension.md) +- [Assistant architecture and model context](./development/assistant-architecture.md) +- [Published packages](./reference/packages.md) +- [Documentation workflow](./development/documentation.md) +- [Operations and recovery](./development/operations.md) +- [Security boundaries](./security/overview.md) +- [Branding and public assets](./assets/brand/README.md) +- [Contributing](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.md) + +The site is available in English and Russian. English is the default; the language +menu opens the corresponding page in the other language. SDK and Kit guides are +maintained in their source packages and copied into `reference` during generation. +API identifiers and contracts are shared by both versions. diff --git a/docs/public/api/index.html b/docs/public/api/index.html index b48869e..9b2c14e 100644 --- a/docs/public/api/index.html +++ b/docs/public/api/index.html @@ -1,12 +1,18 @@ - + - Asmblyr HTTP API + Collaborative HTTP API + +
+ + + diff --git a/docs/public/ru/openapi.json b/docs/public/ru/openapi.json new file mode 100644 index 0000000..9748a82 --- /dev/null +++ b/docs/public/ru/openapi.json @@ -0,0 +1,17887 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Collaborative Core HTTP API", + "version": "0.0.0", + "description": "Все статические маршруты Core и их требования доступа. Часть операций имеет только route-only описание; спецификация не является полной схемой для генерации клиента. OAuth protocol и динамические маршруты плагинов описаны отдельно." + }, + "servers": [ + { + "url": "http://localhost:3000/api", + "description": "Публичный API на домене админки" + }, + { + "url": "http://localhost:3001", + "description": "Самостоятельный Core" + } + ], + "paths": { + "/assistant/conversations": { + "get": { + "tags": [ + "assistant" + ], + "operationId": "get__assistant_conversations", + "summary": "GET /assistant/conversations", + "description": "Активный человек с доступом к чату; только собственные сессии. Источник: apps/core/src/assistant/history-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Личная история текущего пользователя", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "maxItems": 30, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "compactionCount": { + "type": "integer", + "minimum": 0 + }, + "busyUntil": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "createdAt", + "updatedAt", + "compactionCount", + "busyUntil" + ] + } + }, + "nextCursor": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "items", + "nextCursor" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "404": { + "description": "Сессия отсутствует или принадлежит другому пользователю" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/assistant/history-routes.ts" + }, + "post": { + "tags": [ + "assistant" + ], + "operationId": "post__assistant_conversations", + "summary": "POST /assistant/conversations", + "description": "Активный человек с доступом к чату; создаёт собственную сессию с пустым контекстом. Источник: apps/core/src/assistant/history-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "201": { + "description": "Новая сессия с пустым контекстом", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "compactionCount": { + "type": "integer", + "minimum": 0 + }, + "busyUntil": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "createdAt", + "updatedAt", + "compactionCount", + "busyUntil" + ] + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/assistant/history-routes.ts", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": {}, + "required": [] + } + } + } + } + } + }, + "/assistant/conversations/{id}": { + "delete": { + "tags": [ + "assistant" + ], + "operationId": "delete__assistant_conversations_id", + "summary": "DELETE /assistant/conversations/:id", + "description": "Активный человек с доступом к чату; только собственная неактивная сессия. Источник: apps/core/src/assistant/history-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Сессия, сообщения и краткий контекст удалены" + }, + "404": { + "description": "Сессия отсутствует или принадлежит другому пользователю" + }, + "409": { + "description": "В сессии ещё готовится ответ" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/assistant/history-routes.ts" + }, + "get": { + "tags": [ + "assistant" + ], + "operationId": "get__assistant_conversations_id", + "summary": "GET /assistant/conversations/:id", + "description": "Активный человек с доступом к чату; только собственная история, включая superuser. Источник: apps/core/src/assistant/history-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "pattern": "^[1-9][0-9]{0,9}$" + } + } + ], + "responses": { + "200": { + "description": "Личная история текущего пользователя", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "conversation": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "compactionCount": { + "type": "integer", + "minimum": 0 + }, + "busyUntil": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "createdAt", + "updatedAt", + "compactionCount", + "busyUntil" + ] + }, + "messages": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "role": { + "enum": [ + "user", + "assistant" + ] + }, + "content": { + "type": "string" + }, + "activity": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "status", + "note" + ] + }, + "text": { + "type": "string", + "maxLength": 8000 + } + }, + "required": [ + "kind", + "text" + ] + } + }, + "status": { + "enum": [ + "pending", + "completed", + "failed", + "cancelled", + "interrupted" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "contextScope": { + "type": "string" + }, + "contextLabel": { + "type": "string" + }, + "truncated": { + "type": "boolean" + }, + "summary": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "role", + "content", + "activity", + "status", + "createdAt", + "contextScope", + "contextLabel", + "truncated", + "summary" + ] + } + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "conversation", + "messages", + "nextCursor" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "404": { + "description": "Сессия отсутствует или принадлежит другому пользователю" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/assistant/history-routes.ts" + } + }, + "/assistant/filter/validate": { + "post": { + "tags": [ + "assistant" + ], + "operationId": "post__assistant_filter_validate", + "summary": "POST /assistant/filter/validate", + "description": "Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/assistant/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/assistant/routes.ts" + } + }, + "/assistant/messages": { + "post": { + "tags": [ + "assistant" + ], + "operationId": "post__assistant_messages", + "summary": "POST /assistant/messages", + "description": "Человек с доступом к ассистенту. conversationId/messageId/content сохраняют сообщение в собственной сессии; legacy messages остаётся stateless. dataAccess отдельно включает инструменты базы, Google и плагинов; workspaceId должен совпадать с context. Без dataAccess сохраняется прежнее поведение: инструменты только при context. История для модели изолирована по странице и режиму доступа. NDJSON выдаёт started/progress/activity/text-delta/answer/error.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "JSON-ответ либо поток NDJSON; в сохранённой сессии включает receipt", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": true, + "properties": { + "content": { + "type": "string" + }, + "activity": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "status", + "note" + ] + }, + "text": { + "type": "string", + "maxLength": 8000 + } + }, + "required": [ + "kind", + "text" + ] + } + }, + "truncated": { + "type": "boolean" + }, + "summary": { + "type": "object", + "additionalProperties": true + }, + "conversation": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "userMessageId": { + "type": "string", + "format": "uuid" + }, + "assistantMessageId": { + "type": "string", + "format": "uuid" + }, + "compactionCount": { + "type": "integer" + } + }, + "required": [ + "id", + "userMessageId", + "assistantMessageId", + "compactionCount" + ] + } + }, + "required": [ + "content", + "truncated", + "summary" + ] + } + }, + "required": [ + "data" + ] + } + }, + "application/x-ndjson": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Сессия отсутствует или принадлежит другому пользователю" + }, + "409": { + "description": "Сессия занята либо messageId уже отправлен с другим содержимым" + }, + "429": { + "description": "Лимит запросов, модели или контекста" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/assistant/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "conversationId": { + "type": "string", + "format": "uuid" + }, + "messageId": { + "type": "string", + "format": "uuid" + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 8000 + }, + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "reasoningEffort": { + "enum": [ + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": "boolean" + } + } + }, + "context": { + "anyOf": [ + { + "description": "Контекст раздела, таблицы или сохранённой записи. Core проверяет workspace, MCP и права чтения. Данные черновика не принимаются.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "anyOf": [ + { + "enum": [ + "home", + "collections", + "files", + "access", + "services", + "settings", + "system-settings", + "search" + ] + }, + { + "type": "string", + "pattern": "^extensions/[a-z][a-z0-9_]{0,30}/[a-z][a-z0-9-]{0,31}$" + } + ] + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "page" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "const": "items" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "collection": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "table": { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "type": "integer", + "minimum": 1, + "maximum": 2147483647 + }, + "size": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "sort": { + "type": "string", + "maxLength": 63 + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + }, + "q": { + "type": "string", + "maxLength": 255 + }, + "filter": { + "type": "string", + "maxLength": 8192 + }, + "selectedCount": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "editorOpen": { + "type": "boolean" + } + }, + "required": [ + "page", + "size", + "sort", + "direction", + "q", + "filter", + "selectedCount", + "editorOpen" + ] + } + }, + "required": [ + "page", + "collection", + "table" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "const": "items" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "collection": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "record": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + }, + "required": [ + "id" + ] + } + }, + "required": [ + "page", + "collection", + "record" + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "dataAccess": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "enabled", + "workspaceId" + ] + } + }, + "required": [ + "conversationId", + "messageId", + "content" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "messages": { + "type": "array", + "minItems": 1, + "maxItems": 31, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "role": { + "enum": [ + "user", + "assistant" + ] + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 8000 + } + }, + "required": [ + "role", + "content" + ] + } + }, + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "reasoningEffort": { + "enum": [ + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": "boolean" + } + } + }, + "context": { + "anyOf": [ + { + "description": "Контекст раздела, таблицы или сохранённой записи. Core проверяет workspace, MCP и права чтения. Данные черновика не принимаются.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "anyOf": [ + { + "enum": [ + "home", + "collections", + "files", + "access", + "services", + "settings", + "system-settings", + "search" + ] + }, + { + "type": "string", + "pattern": "^extensions/[a-z][a-z0-9_]{0,30}/[a-z][a-z0-9-]{0,31}$" + } + ] + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "page" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "const": "items" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "collection": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "table": { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "type": "integer", + "minimum": 1, + "maximum": 2147483647 + }, + "size": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "sort": { + "type": "string", + "maxLength": 63 + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + }, + "q": { + "type": "string", + "maxLength": 255 + }, + "filter": { + "type": "string", + "maxLength": 8192 + }, + "selectedCount": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "editorOpen": { + "type": "boolean" + } + }, + "required": [ + "page", + "size", + "sort", + "direction", + "q", + "filter", + "selectedCount", + "editorOpen" + ] + } + }, + "required": [ + "page", + "collection", + "table" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { + "const": "items" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "collection": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "record": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + }, + "required": [ + "id" + ] + } + }, + "required": [ + "page", + "collection", + "record" + ] + } + ] + }, + { + "type": "null" + } + ] + }, + "dataAccess": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "workspaceId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "enabled", + "workspaceId" + ] + } + }, + "required": [ + "messages" + ] + } + ] + } + } + } + } + } + }, + "/assistant/messages/{id}/cancel": { + "post": { + "tags": [ + "assistant" + ], + "operationId": "post__assistant_messages_id_cancel", + "summary": "POST /assistant/messages/:id/cancel", + "description": "Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/assistant/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/assistant/routes.ts" + } + }, + "/assistant/selection/validate": { + "post": { + "tags": [ + "assistant" + ], + "operationId": "post__assistant_selection_validate", + "summary": "POST /assistant/selection/validate", + "description": "Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/assistant/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/assistant/routes.ts" + } + }, + "/assistant/status": { + "get": { + "tags": [ + "assistant" + ], + "operationId": "get__assistant_status", + "summary": "GET /assistant/status", + "description": "Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/assistant/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/assistant/routes.ts" + } + }, + "/auth/browser/google/start": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_google_start", + "summary": "POST /auth/browser/google/start", + "description": "Активный пользователь, точный Origin AUTH_UI_URL; proof в HttpOnly cookie.. Источник: apps/core/src/auth/browser/google.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "URL для авторизации; browser proof хранится только в HttpOnly cookie", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/google.ts" + } + }, + "/auth/browser/invitations/claim": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_invitations_claim", + "summary": "POST /auth/browser/invitations/claim", + "description": "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.. Источник: apps/core/src/auth/browser/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Браузерная сессия создана. API tokens не возвращаются.", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "pattern": "^asm_(inv|rec)_[A-Za-z0-9_-]{43}$" + } + }, + "required": [ + "token" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/auth/browser/login": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_login", + "summary": "POST /auth/browser/login", + "description": "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.. Источник: apps/core/src/auth/browser/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Браузерная сессия создана. API tokens не возвращаются.", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/auth/browser/logout": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_logout", + "summary": "POST /auth/browser/logout", + "description": "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.. Источник: apps/core/src/auth/browser/routes.ts", + "security": [], + "parameters": [], + "responses": { + "204": { + "description": "Текущая сессия отозвана, cookies удалены", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/routes.ts" + } + }, + "/auth/browser/passkeys/login": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_passkeys_login", + "summary": "POST /auth/browser/passkeys/login", + "description": "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.. Источник: apps/core/src/auth/browser/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Браузерная сессия создана. API tokens не возвращаются.", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "response": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "string" + }, + "type": { + "const": "public-key" + }, + "response": { + "type": "object", + "properties": { + "authenticatorData": { + "type": "string" + }, + "clientDataJSON": { + "type": "string" + }, + "signature": { + "type": "string" + }, + "userHandle": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "authenticatorData", + "clientDataJSON", + "signature" + ], + "additionalProperties": false + }, + "clientExtensionResults": { + "type": "object", + "additionalProperties": true + }, + "authenticatorAttachment": { + "enum": [ + "platform", + "cross-platform" + ] + } + }, + "required": [ + "id", + "rawId", + "type", + "response", + "clientExtensionResults" + ], + "additionalProperties": false + } + }, + "required": [ + "challengeId", + "response" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/auth/browser/passkeys/options": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_browser_passkeys_options", + "summary": "POST /auth/browser/passkeys/options", + "description": "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.. Источник: apps/core/src/auth/browser/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "options": { + "type": "object", + "additionalProperties": true, + "properties": { + "challenge": { + "type": "string" + }, + "timeout": { + "type": "integer" + }, + "rpId": { + "type": "string" + }, + "userVerification": { + "const": "required" + } + }, + "required": [ + "challenge", + "rpId", + "userVerification" + ] + } + }, + "required": [ + "challengeId", + "options" + ], + "additionalProperties": false + } + } + }, + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/routes.ts" + } + }, + "/auth/cli/authorize": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_cli_authorize", + "summary": "POST /auth/cli/authorize", + "description": "Human-session approval for CLI schema generation. The code is session-bound, expires after 60 seconds and requires PKCE S256 plus an exact loopback redirect.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Success; cache-control: no-store", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "redirectTo" + ], + "properties": { + "redirectTo": { + "type": "string" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Active human session required" + }, + "429": { + "description": "Rate limited" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/cli/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "redirectUri", + "challenge", + "state" + ], + "properties": { + "redirectUri": { + "type": "string", + "pattern": "^http://127\\.0\\.0\\.1:[0-9]+/callback$" + }, + "challenge": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{43}$" + }, + "state": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{43}$" + } + } + } + } + } + } + } + }, + "/auth/cli/config": { + "get": { + "tags": [ + "auth" + ], + "operationId": "get__auth_cli_config", + "summary": "GET /auth/cli/config", + "description": "Public CLI discovery. Returns the configured admin consent URL; no credential or session is issued.", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Success; cache-control: no-store", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "authorizationUrl" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + } + } + } + } + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/cli/routes.ts" + } + }, + "/auth/cli/token": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_cli_token", + "summary": "POST /auth/cli/token", + "description": "Public one-use PKCE exchange. Issues a 10-minute credential accepted only by GET /schema; session revocation also revokes this access. Credentials are never usable for item reads/writes.", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Success; cache-control: no-store", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "accessToken", + "scope", + "expiresIn" + ], + "properties": { + "accessToken": { + "type": "string", + "pattern": "^asm_sc_[A-Za-z0-9_-]{43}$" + }, + "scope": { + "const": "schema:read" + }, + "expiresIn": { + "const": 600 + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Invalid, expired or consumed code; invalid PKCE or revoked session" + }, + "429": { + "description": "Rate limited" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/cli/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "redirectUri", + "code", + "verifier" + ], + "properties": { + "redirectUri": { + "type": "string", + "pattern": "^http://127\\.0\\.0\\.1:[0-9]+/callback$" + }, + "code": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{43}$" + }, + "verifier": { + "type": "string", + "minLength": 43, + "maxLength": 128, + "pattern": "^[A-Za-z0-9._~-]+$" + } + } + } + } + } + } + } + }, + "/auth/federation-token": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_federation_token", + "summary": "POST /auth/federation-token", + "description": "Подписанный GitLab CI assertion и активная федерация. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/federation-routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/federation-routes.ts" + } + }, + "/auth/invitations/accept": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_invitations_accept", + "summary": "POST /auth/invitations/accept", + "description": "Одноразовый invitation token и новый пароль. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/routes.ts" + } + }, + "/auth/invitations/claim": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_invitations_claim", + "summary": "POST /auth/invitations/claim", + "description": "Одноразовый invitation/recovery token; создаёт человеческую сессию. Recovery отзывает прежние сеансы и способы входа. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokenType": { + "const": "Bearer" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "expiresIn": { + "type": "integer" + }, + "refreshExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "tokenType", + "accessToken", + "refreshToken", + "expiresIn", + "refreshExpiresAt" + ], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Истёкшая/использованная ссылка или отключённый пользователь" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "pattern": "^asm_(inv|rec)_[A-Za-z0-9_-]{43}$" + } + }, + "required": [ + "token" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_login", + "summary": "POST /auth/login", + "description": "Проверка email и пароля; ограничение частоты. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokenType": { + "const": "Bearer" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "expiresIn": { + "type": "integer" + }, + "refreshExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "tokenType", + "accessToken", + "refreshToken", + "expiresIn", + "refreshExpiresAt" + ], + "description": "Токены передаются напрямую, без обёртки data." + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "email", + "password" + ] + } + } + } + } + } + }, + "/auth/logout": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_logout", + "summary": "POST /auth/logout", + "description": "Refresh token в теле либо Bearer сессии. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/routes.ts" + } + }, + "/auth/me": { + "get": { + "tags": [ + "auth" + ], + "operationId": "get__auth_me", + "summary": "GET /auth/me", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Источник: apps/core/src/auth/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "email", + "superuser", + "hasPassword", + "displayName", + "firstName", + "lastName", + "description", + "pictureUrl", + "avatarId", + "createdAt", + "updatedAt", + "lastLoginAt", + "lastActiveAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "superuser": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + }, + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "lastActiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/routes.ts" + } + }, + "/auth/passkeys/login": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_passkeys_login", + "summary": "POST /auth/passkeys/login", + "description": "Одноразовый challenge, подпись сохранённого passkey, точный origin/RP ID и user verification; активный пользователь. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokenType": { + "const": "Bearer" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "expiresIn": { + "type": "integer" + }, + "refreshExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "tokenType", + "accessToken", + "refreshToken", + "expiresIn", + "refreshExpiresAt" + ], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "WebAuthn verification failed or challenge used/expired" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "response": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "string" + }, + "type": { + "const": "public-key" + }, + "response": { + "type": "object", + "properties": { + "authenticatorData": { + "type": "string" + }, + "clientDataJSON": { + "type": "string" + }, + "signature": { + "type": "string" + }, + "userHandle": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "authenticatorData", + "clientDataJSON", + "signature" + ], + "additionalProperties": false + }, + "clientExtensionResults": { + "type": "object", + "additionalProperties": true + }, + "authenticatorAttachment": { + "enum": [ + "platform", + "cross-platform" + ] + } + }, + "required": [ + "id", + "rawId", + "type", + "response", + "clientExtensionResults" + ], + "additionalProperties": false + } + }, + "required": [ + "challengeId", + "response" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/auth/passkeys/options": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_passkeys_options", + "summary": "POST /auth/passkeys/options", + "description": "Публичный WebAuthn challenge, пять минут; общий credential rate limit. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "options": { + "type": "object", + "additionalProperties": true, + "properties": { + "challenge": { + "type": "string" + }, + "timeout": { + "type": "integer" + }, + "rpId": { + "type": "string" + }, + "userVerification": { + "const": "required" + } + }, + "required": [ + "challenge", + "rpId", + "userVerification" + ] + } + }, + "required": [ + "challengeId", + "options" + ], + "additionalProperties": false + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts" + } + }, + "/auth/providers": { + "get": { + "tags": [ + "auth" + ], + "operationId": "get__auth_providers", + "summary": "GET /auth/providers", + "description": "Публичные ID и подписи провайдеров. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/sso/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/sso/routes.ts" + } + }, + "/auth/refresh": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_refresh", + "summary": "POST /auth/refresh", + "description": "Действительный refresh token; ротация и защита от повторного использования. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokenType": { + "const": "Bearer" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "expiresIn": { + "type": "integer" + }, + "refreshExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "tokenType", + "accessToken", + "refreshToken", + "expiresIn", + "refreshExpiresAt" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + }, + "required": [ + "refreshToken" + ] + } + } + } + } + } + }, + "/auth/service-token": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_service_token", + "summary": "POST /auth/service-token", + "description": "Действительный сервисный ключ. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/routes.ts" + } + }, + "/auth/setup": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_setup", + "summary": "POST /auth/setup", + "description": "Setup secret и отсутствие созданных пользователей; ограничение частоты. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/routes.ts" + } + }, + "/auth/setup/status": { + "get": { + "tags": [ + "auth" + ], + "operationId": "get__auth_setup_status", + "summary": "GET /auth/setup/status", + "description": "Публичный статус первой настройки. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/routes.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/routes.ts" + } + }, + "/auth/sso/{provider}/callback": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_sso_provider_callback", + "summary": "POST /auth/sso/:provider/callback", + "description": "Проверка SSO flow, state/PKCE и ответа провайдера. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/sso/routes.ts", + "security": [], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/sso/routes.ts" + } + }, + "/auth/sso/{provider}/start": { + "post": { + "tags": [ + "auth" + ], + "operationId": "post__auth_sso_provider_start", + "summary": "POST /auth/sso/:provider/start", + "description": "Начало SSO; привязка требует действующей пользовательской сессии. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/sso/routes.ts", + "security": [], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/sso/routes.ts" + } + }, + "/collections": { + "get": { + "tags": [ + "collections" + ], + "operationId": "get__collections", + "summary": "GET /collections", + "description": "Активный principal; только доступные коллекции и поля. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + }, + "post": { + "tags": [ + "collections" + ], + "operationId": "post__collections", + "summary": "POST /collections", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}": { + "delete": { + "tags": [ + "collections" + ], + "operationId": "delete__collections_name", + "summary": "DELETE /collections/:name", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/lifecycle-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/lifecycle-routes.ts" + } + }, + "/collections/{name}/display": { + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_display", + "summary": "PUT /collections/:name/display", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/fields": { + "post": { + "tags": [ + "collections" + ], + "operationId": "post__collections_name_fields", + "summary": "POST /collections/:name/fields", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/fields/{field}": { + "delete": { + "tags": [ + "collections" + ], + "operationId": "delete__collections_name_fields_field", + "summary": "DELETE /collections/:name/fields/:field", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/lifecycle-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/lifecycle-routes.ts" + }, + "patch": { + "tags": [ + "collections" + ], + "operationId": "patch__collections_name_fields_field", + "summary": "PATCH /collections/:name/fields/:field", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/fields/{field}/configuration": { + "post": { + "tags": [ + "collections" + ], + "operationId": "post__collections_name_fields_field_configuration", + "summary": "POST /collections/:name/fields/:field/configuration", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Field configuration saved atomically; data contains the updated collection. Index creation uses the separate search endpoint.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Invalid input, forbidden access or missing collection/field. Search settings require superuser." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "field": { + "type": "object", + "additionalProperties": true, + "description": "Field definition; creation requires name matching the URL and a supported type." + }, + "presentation": { + "type": "object", + "additionalProperties": true + }, + "searchable": { + "type": "boolean" + }, + "relationSearchable": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [ + "field" + ], + "minProperties": 1 + } + } + } + } + }, + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_fields_field_configuration", + "summary": "PUT /collections/:name/fields/:field/configuration", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Field configuration saved atomically; data contains the updated collection. Index creation uses the separate search endpoint.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Invalid input, forbidden access or missing collection/field. Search settings require superuser." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "field": { + "type": "object", + "additionalProperties": true, + "description": "Field definition; creation requires name matching the URL and a supported type." + }, + "presentation": { + "type": "object", + "additionalProperties": true + }, + "searchable": { + "type": "boolean" + }, + "relationSearchable": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [], + "minProperties": 1 + } + } + } + } + } + }, + "/collections/{name}/fields/{field}/impact": { + "get": { + "tags": [ + "collections" + ], + "operationId": "get__collections_name_fields_field_impact", + "summary": "GET /collections/:name/fields/:field/impact", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/lifecycle-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/lifecycle-routes.ts" + } + }, + "/collections/{name}/fields/{field}/presentation": { + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_fields_field_presentation", + "summary": "PUT /collections/:name/fields/:field/presentation", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/fields/{field}/search": { + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_fields_field_search", + "summary": "PUT /collections/:name/fields/:field/search", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "searchable": { + "type": "boolean" + }, + "indexed": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [ + "searchable", + "indexed" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Invalid input, forbidden access or missing collection/field. Search settings require superuser." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "searchable": { + "type": "boolean" + }, + "indexed": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [ + "searchable", + "indexed" + ], + "minProperties": 1 + } + } + } + } + } + }, + "/collections/{name}/folder": { + "patch": { + "tags": [ + "collections" + ], + "operationId": "patch__collections_name_folder", + "summary": "PATCH /collections/:name/folder", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/form": { + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_form", + "summary": "PUT /collections/:name/form", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/impact": { + "get": { + "tags": [ + "collections" + ], + "operationId": "get__collections_name_impact", + "summary": "GET /collections/:name/impact", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/lifecycle-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/lifecycle-routes.ts" + } + }, + "/collections/{name}/materialized-view": { + "delete": { + "tags": [ + "collections" + ], + "operationId": "delete__collections_name_materialized_view", + "summary": "DELETE /collections/:name/materialized-view", + "description": "Disconnect the materialized collection and its catalog metadata. The PostgreSQL object, indexes and rows are retained. Relation dependencies prevent disconnection. No refresh or physical deletion is performed.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Disconnected" + }, + "403": { + "description": "Superuser required" + }, + "404": { + "description": "Materialized connection not found" + }, + "409": { + "description": "Dependent collections exist" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/materialized-routes.ts" + } + }, + "/collections/{name}/navigation": { + "patch": { + "tags": [ + "collections" + ], + "operationId": "patch__collections_name_navigation", + "summary": "PATCH /collections/:name/navigation", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/relations": { + "post": { + "tags": [ + "collections" + ], + "operationId": "post__collections_name_relations", + "summary": "POST /collections/:name/relations", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/relations/{field}/search": { + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_relations_field_search", + "summary": "PUT /collections/:name/relations/:field/search", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "searchable": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [ + "searchable" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Invalid input, forbidden access or missing collection/field. Search settings require superuser." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "searchable": { + "type": "boolean" + }, + "searchPriority": { + "enum": [ + "primary", + "secondary", + null + ], + "description": "null restores automatic priority from the record label." + } + }, + "required": [ + "searchable" + ], + "minProperties": 1 + } + } + } + } + } + }, + "/collections/{name}/settings": { + "patch": { + "tags": [ + "collections" + ], + "operationId": "patch__collections_name_settings", + "summary": "PATCH /collections/:name/settings", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/collections/{name}/terms": { + "get": { + "tags": [ + "collections" + ], + "operationId": "get__collections_name_terms", + "summary": "GET /collections/:name/terms", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + }, + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_terms", + "summary": "PUT /collections/:name/terms", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + } + }, + "/collections/{name}/terms/{id}": { + "delete": { + "tags": [ + "collections" + ], + "operationId": "delete__collections_name_terms_id", + "summary": "DELETE /collections/:name/terms/:id", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + }, + "put": { + "tags": [ + "collections" + ], + "operationId": "put__collections_name_terms_id", + "summary": "PUT /collections/:name/terms/:id", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + } + }, + "/connections/google": { + "delete": { + "tags": [ + "connections" + ], + "operationId": "delete__connections_google", + "summary": "DELETE /connections/google", + "description": "Активная человеческая сессия; удаляет собственные токены, flows и предложения, пытается отозвать Google grant. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "disconnected": { + "const": true + }, + "revoked": { + "type": "boolean" + } + }, + "required": [ + "disconnected", + "revoked" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts" + }, + "get": { + "tags": [ + "connections" + ], + "operationId": "get__connections_google", + "summary": "GET /connections/google", + "description": "Активная человеческая сессия; состояние только собственного подключения, без токенов. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "google" + }, + "enabled": { + "type": "boolean" + }, + "unavailable": { + "type": "boolean" + }, + "connected": { + "type": "boolean" + }, + "reconnect": { + "type": "boolean" + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "connectedAt": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "provider", + "enabled", + "unavailable", + "connected", + "reconnect", + "email", + "connectedAt" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts" + } + }, + "/connections/google/callback": { + "get": { + "tags": [ + "connections" + ], + "operationId": "get__connections_google_callback", + "summary": "GET /connections/google/callback", + "description": "Редирект результата; подключение требует активную сессию, одноразовые state и browser proof, совпадение владельца.. Источник: apps/core/src/auth/browser/google.ts", + "security": [], + "parameters": [ + { + "name": "code", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "error", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "303": { + "description": "Проверены одноразовое состояние и browser proof; результат в перенаправлении. Для linking требуется текущая сессия.", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + }, + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/google.ts", + "servers": [ + { + "url": "http://localhost:3000", + "description": "Публичный origin без /api; callback сохраняет зарегистрированный путь" + } + ] + }, + "post": { + "tags": [ + "connections" + ], + "operationId": "post__connections_google_callback", + "summary": "POST /connections/google/callback", + "description": "Активная человеческая сессия; собственный flow, browser proof, state/nonce/PKCE и та же конфигурация клиента. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "const": "google" + }, + "enabled": { + "type": "boolean" + }, + "unavailable": { + "type": "boolean" + }, + "connected": { + "type": "boolean" + }, + "reconnect": { + "type": "boolean" + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "connectedAt": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "provider", + "enabled", + "unavailable", + "connected", + "reconnect", + "email", + "connectedAt" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "browserToken": { + "type": "string", + "maxLength": 128 + }, + "query": { + "type": "string", + "maxLength": 12000 + } + }, + "required": [ + "browserToken", + "query" + ] + } + } + } + } + } + }, + "/connections/google/start": { + "post": { + "tags": [ + "connections" + ], + "operationId": "post__connections_google_start", + "summary": "POST /connections/google/start", + "description": "Активная человеческая сессия; одноразовый OAuth flow владельца с browser proof и PKCE. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + }, + "browserToken": { + "type": "string", + "description": "Только для HttpOnly cookie UI, не для ассистента" + } + }, + "required": [ + "url", + "browserToken" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": {}, + "required": [] + } + } + } + } + } + }, + "/connections/google/writes/{id}": { + "delete": { + "tags": [ + "connections" + ], + "operationId": "delete__connections_google_writes_id", + "summary": "DELETE /connections/google/writes/:id", + "description": "Активная человеческая сессия; отмена собственного ожидающего предложения. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "cancelled": { + "const": true + } + }, + "required": [ + "cancelled" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts" + }, + "get": { + "tags": [ + "connections" + ], + "operationId": "get__connections_google_writes_id", + "summary": "GET /connections/google/writes/:id", + "description": "Owner-bound preview and final receipt until the proposal's existing 20-minute expiry. GET never executes a write. Cancelled proposals remain reviewable; executing/uncertain operations cannot be replayed. Result URLs are constructed from Google file IDs; provider response bodies are not retained.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "provider": { + "const": "google" + }, + "operation": { + "enum": [ + "create_text", + "update_text", + "rename_file", + "trash_file", + "create_sheet", + "update_cells", + "append_cells" + ] + }, + "target": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "status": { + "enum": [ + "pending", + "executing", + "cancelled", + "succeeded", + "failed", + "uncertain" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "result": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": [ + "string", + "null" + ] + }, + "updatedCells": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "updatedRows": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "range": { + "type": [ + "string", + "null" + ], + "maxLength": 200 + } + }, + "required": [ + "url", + "updatedCells", + "updatedRows", + "range" + ] + }, + { + "type": "null" + } + ] + }, + "failure": { + "enum": [ + "target_changed", + null + ] + } + }, + "required": [ + "id", + "provider", + "operation", + "target", + "title", + "content", + "expiresAt", + "status", + "url", + "result", + "failure" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts" + } + }, + "/connections/google/writes/{id}/confirm": { + "post": { + "tags": [ + "connections" + ], + "operationId": "post__connections_google_writes_id_confirm", + "summary": "POST /connections/google/writes/:id/confirm", + "description": "Активная человеческая сессия; однократное подтверждение собственного предложения, перепроверка подключения и исходных данных; не MCP-инструмент. Источник: apps/core/src/connections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "const": "succeeded" + }, + "result": { + "type": "object", + "additionalProperties": true, + "description": "Ограниченный ответ Google: ID файла/таблицы или число изменённых ячеек" + } + }, + "required": [ + "status", + "result" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "const": "uncertain" + }, + "message": { + "type": "string" + } + }, + "required": [ + "status", + "message" + ] + } + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Неверный OAuth flow или параметры" + }, + "401": { + "description": "Нужна активная человеческая сессия" + }, + "403": { + "description": "Сервисным аккаунтам подключение недоступно" + }, + "404": { + "description": "Предложение недоступно, истекло или принадлежит другому пользователю" + }, + "409": { + "description": "Нужно переподключение, провайдер выключен или изменились исходные данные" + }, + "429": { + "description": "Превышен лимит" + }, + "503": { + "description": "Провайдер или шифрование недоступны" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/connections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": {}, + "required": [] + } + } + } + } + } + }, + "/extensions": { + "get": { + "tags": [ + "extensions" + ], + "operationId": "get__extensions", + "summary": "GET /extensions", + "description": "Активный principal; метаданные включённых UI-плагинов. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/plugins/ui-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/plugins/ui-routes.ts" + } + }, + "/extensions/{namespace}/drafts/{id}": { + "get": { + "tags": [ + "extensions" + ], + "operationId": "get__extensions_namespace_drafts_id", + "summary": "GET /extensions/:namespace/drafts/:id", + "description": "Активный principal; подготовленный черновик только своего principal и namespace, с TTL. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/plugins/action-draft-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "namespace", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/plugins/action-draft-routes.ts" + } + }, + "/files": { + "get": { + "tags": [ + "files" + ], + "operationId": "get__files", + "summary": "GET /files", + "description": "Человек + files/read либо update; superuser bypass. Общая библиотека команды. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + }, + "post": { + "tags": [ + "files" + ], + "operationId": "post__files", + "summary": "POST /files", + "description": "Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + } + }, + "/files/{id}": { + "delete": { + "tags": [ + "files" + ], + "operationId": "delete__files_id", + "summary": "DELETE /files/:id", + "description": "Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + }, + "get": { + "tags": [ + "files" + ], + "operationId": "get__files_id", + "summary": "GET /files/:id", + "description": "Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + }, + "patch": { + "tags": [ + "files" + ], + "operationId": "patch__files_id", + "summary": "PATCH /files/:id", + "description": "Человек + files/update; superuser bypass. Изменение title/description и visibility private/public одного ready-файла; публикация и отзыв ссылки записываются в историю.. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Обновлён ready-файл. visibility=private отзывает будущие публичные скачивания.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/files/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": { + "type": "string", + "maxLength": 4000 + }, + "visibility": { + "enum": [ + "private", + "public" + ] + } + } + } + } + } + } + } + }, + "/files/{id}/content": { + "get": { + "tags": [ + "files" + ], + "operationId": "get__files_id_content", + "summary": "GET /files/:id/content", + "description": "Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + } + }, + "/files/{id}/events": { + "get": { + "tags": [ + "files" + ], + "operationId": "get__files_id_events", + "summary": "GET /files/:id/events", + "description": "Человек + files/read либо update; superuser bypass. Общая библиотека команды. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + } + }, + "/files/resolve": { + "get": { + "tags": [ + "files" + ], + "operationId": "get__files_resolve", + "summary": "GET /files/resolve", + "description": "Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/files/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/files/routes.ts" + } + }, + "/filter-presets/{collection}": { + "get": { + "tags": [ + "filter-presets" + ], + "operationId": "get__filter_presets_collection", + "summary": "GET /filter-presets/:collection", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/filter-preset-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/filter-preset-routes.ts" + }, + "post": { + "tags": [ + "filter-presets" + ], + "operationId": "post__filter_presets_collection", + "summary": "POST /filter-presets/:collection", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/filter-preset-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/filter-preset-routes.ts" + } + }, + "/filter-presets/{collection}/{id}": { + "delete": { + "tags": [ + "filter-presets" + ], + "operationId": "delete__filter_presets_collection_id", + "summary": "DELETE /filter-presets/:collection/:id", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/filter-preset-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/filter-preset-routes.ts" + }, + "put": { + "tags": [ + "filter-presets" + ], + "operationId": "put__filter_presets_collection_id", + "summary": "PUT /filter-presets/:collection/:id", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/filter-preset-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/filter-preset-routes.ts" + } + }, + "/folders": { + "post": { + "tags": [ + "folders" + ], + "operationId": "post__folders", + "summary": "POST /folders", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/folders/{id}": { + "delete": { + "tags": [ + "folders" + ], + "operationId": "delete__folders_id", + "summary": "DELETE /folders/:id", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + }, + "patch": { + "tags": [ + "folders" + ], + "operationId": "patch__folders_id", + "summary": "PATCH /folders/:id", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/folders/{id}/order": { + "patch": { + "tags": [ + "folders" + ], + "operationId": "patch__folders_id_order", + "summary": "PATCH /folders/:id/order", + "description": "Человек: superuser; системная и plugin-owned структура дополнительно защищена. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/collections/routes.ts" + } + }, + "/health": { + "get": { + "tags": [ + "health" + ], + "operationId": "get__health", + "summary": "GET /health", + "description": "Публичная проверка процесса. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/app.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/app.ts" + } + }, + "/item-events/{collection}": { + "get": { + "tags": [ + "item-events" + ], + "operationId": "get__item_events_collection", + "summary": "GET /item-events/:collection", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/routes.ts" + } + }, + "/items/{collection}": { + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection", + "summary": "GET /items/:collection", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Имена через запятую; пустое значение возвращает только ключ. Пропуск — все доступные поля." + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 100 + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "direction", + "in": "query", + "schema": { + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Буквальный поиск по читаемым настроенным полям и связям." + }, + { + "name": "order", + "in": "query", + "schema": { + "enum": [ + "field", + "relevance" + ] + }, + "description": "При q без явных sort/direction используется relevance; иначе field. Без q эффективный режим field. При relevance sort/direction разрешают равные совпадения." + }, + { + "name": "filter", + "in": "query", + "schema": { + "type": "string" + }, + "description": "JSON группы {logic: and|or, children: [...]}; это фильтр запроса, не grant." + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "page": { + "type": "object", + "properties": { + "number": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "string" + }, + "sort": { + "type": "string" + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + } + }, + "required": [ + "number", + "size", + "total", + "sort", + "direction", + "order" + ] + } + }, + "required": [ + "data", + "labels", + "page" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/routes.ts" + }, + "patch": { + "tags": [ + "items" + ], + "operationId": "patch__items_collection", + "summary": "PATCH /items/:collection", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/routes.ts" + }, + "post": { + "tags": [ + "items" + ], + "operationId": "post__items_collection", + "summary": "POST /items/:collection", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Сохранено; data=null при отсутствии права чтения.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "/items/{collection}/{id}": { + "delete": { + "tags": [ + "items" + ], + "operationId": "delete__items_collection_id", + "summary": "DELETE /items/:collection/:id", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Удалено, без тела ответа." + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/routes.ts" + }, + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection_id", + "summary": "GET /items/:collection/:id", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Имена через запятую; пустое значение возвращает только ключ. Пропуск — все доступные поля." + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "label": { + "type": "string" + } + }, + "required": [ + "data", + "label" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/routes.ts" + }, + "patch": { + "tags": [ + "items" + ], + "operationId": "patch__items_collection_id", + "summary": "PATCH /items/:collection/:id", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Сохранено; data=null при отсутствии права чтения.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "/items/{collection}/{id}/related": { + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection_id_related", + "summary": "GET /items/:collection/:id/related", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/routes.ts" + } + }, + "/items/{collection}/{id}/relations/{field}": { + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection_id_relations_field", + "summary": "GET /items/:collection/:id/relations/:field", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-routes.ts" + }, + "patch": { + "tags": [ + "items" + ], + "operationId": "patch__items_collection_id_relations_field", + "summary": "PATCH /items/:collection/:id/relations/:field", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-routes.ts" + }, + "post": { + "tags": [ + "items" + ], + "operationId": "post__items_collection_id_relations_field", + "summary": "POST /items/:collection/:id/relations/:field", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-routes.ts" + } + }, + "/items/{collection}/{id}/relations/{field}/candidates": { + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection_id_relations_field_candidates", + "summary": "GET /items/:collection/:id/relations/:field/candidates", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-routes.ts" + } + }, + "/items/{collection}/{id}/relations/{field}/links/{linkId}": { + "get": { + "tags": [ + "items" + ], + "operationId": "get__items_collection_id_relations_field_links_linkId", + "summary": "GET /items/:collection/:id/relations/:field/links/:linkId", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-link-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "linkId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-link-routes.ts" + }, + "patch": { + "tags": [ + "items" + ], + "operationId": "patch__items_collection_id_relations_field_links_linkId", + "summary": "PATCH /items/:collection/:id/relations/:field/links/:linkId", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-link-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "linkId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-link-routes.ts" + } + }, + "/items/{collection}/{id}/relations/{field}/links/to/{targetId}": { + "post": { + "tags": [ + "items" + ], + "operationId": "post__items_collection_id_relations_field_links_to_targetId", + "summary": "POST /items/:collection/:id/relations/:field/links/to/:targetId", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-link-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "targetId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-link-routes.ts" + } + }, + "/items/{collection}/{id}/relations/{field}/records": { + "post": { + "tags": [ + "items" + ], + "operationId": "post__items_collection_id_relations_field_records", + "summary": "POST /items/:collection/:id/relations/:field/records", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/relation-link-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/relation-link-routes.ts" + } + }, + "/items/{collection}/commit": { + "post": { + "tags": [ + "items" + ], + "operationId": "post__items_collection_commit", + "summary": "POST /items/:collection/commit", + "description": "Активный principal; личные read/create/update и поля всех затронутых записей. expectedValues требует чтения обновляемых полей; конфликт откатывает весь commit. Источник: apps/core/src/items/relation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "409": { + "description": "Конфликт, включая ITEM_CHANGED. Весь черновик и его история откатываются. Ответ не содержит закрытых значений.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": true + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "code", + "message", + "details", + "requestId" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/items/relation-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + } + } + } + } + }, + "/materialized-views": { + "get": { + "tags": [ + "materialized-views" + ], + "operationId": "get__materialized_views", + "summary": "GET /materialized-views", + "description": "Superuser-only discovery of existing materialized views in public. Reserved Core/plugin names are excluded. Discovery does not register or refresh sources.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Candidates with compatibility reasons", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "populated", + "connected", + "problem", + "keys", + "fields" + ], + "properties": { + "name": { + "type": "string" + }, + "populated": { + "type": "boolean" + }, + "connected": { + "type": "boolean" + }, + "problem": { + "enum": [ + null, + "unsupported-name", + "unsupported-fields", + "missing-key", + "not-populated" + ] + }, + "keys": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "type": { + "enum": [ + "uuid", + "serial", + "bigserial", + "text" + ] + } + } + } + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "type", + "nullable" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": [ + "string", + "null" + ] + }, + "nullable": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Superuser required" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/materialized-routes.ts" + }, + "post": { + "tags": [ + "materialized-views" + ], + "operationId": "post__materialized_views", + "summary": "POST /materialized-views", + "description": "Atomically register an existing populated public materialized view and presentation metadata. Requires a single UUID, positive integer/bigint or nonempty text key (maximum 255 characters) with a valid full single-column UNIQUE index. No SQL, physical changes or REFRESH are accepted. Returns the collection catalog entry. Item writes and physical structure changes remain forbidden, including for superuser.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "201": { + "description": "Connected collection", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object" + } + } + } + } + } + }, + "400": { + "description": "Incompatible source, key or presentation" + }, + "403": { + "description": "Superuser required; reserved sources excluded" + }, + "404": { + "description": "Source does not exist" + }, + "409": { + "description": "Already registered" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/collections/materialized-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "primaryKey" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "primaryKey": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "displayName": { + "type": [ + "string", + "null" + ], + "maxLength": 120 + }, + "folderId": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "workspaceId": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "displayField": { + "type": [ + "string", + "null" + ] + }, + "displayTemplate": { + "type": [ + "string", + "null" + ] + }, + "presentations": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Ordinary field presentation metadata; no computed/write rules or relation filters. Primary-key presentation is excluded." + } + } + } + } + } + } + } + } + }, + "/monitoring/browser": { + "get": { + "tags": [ + "monitoring" + ], + "operationId": "get__monitoring_browser", + "summary": "GET /monitoring/browser", + "description": "Активная человеческая сессия; только публичный браузерный DSN и разрешённые флаги, без серверного DSN. Сервисные токены не допускаются. Источник: apps/core/src/monitoring/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех; private, no-store. Метрики только текущего экземпляра Core.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "dsn", + "environment", + "release", + "errors", + "performance", + "tracesSampleRate" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "dsn": { + "type": "string", + "description": "Только публичный браузерный DSN; пустая строка при отключении. Серверный DSN не возвращается." + }, + "environment": { + "type": "string" + }, + "release": { + "type": "string" + }, + "errors": { + "type": "boolean" + }, + "performance": { + "type": "boolean" + }, + "tracesSampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + } + } + } + } + }, + "401": { + "description": "Нужна активная человеческая сессия; сервисные токены не допускаются." + }, + "503": { + "description": "База/конфигурация недоступна." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/monitoring/routes.ts" + } + }, + "/notifications": { + "get": { + "tags": [ + "notifications" + ], + "operationId": "get__notifications", + "summary": "GET /notifications", + "description": "Активная человеческая сессия; собственные уведомления включённых плагинов, доступ к каждой записи проверяется заново. Источник: apps/core/src/notifications/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "Последние доступные уведомления; до 200 на пользователя", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "source": { + "type": "string" + }, + "panelId": { + "type": "string" + }, + "targetId": { + "type": "string" + }, + "collection": { + "type": "string" + }, + "collectionDisplayName": { + "type": [ + "string", + "null" + ] + }, + "item": { + "type": "string" + }, + "actorName": { + "type": [ + "string", + "null" + ] + }, + "preview": { + "type": "string", + "maxLength": 300 + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "readAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "source", + "panelId", + "targetId", + "collection", + "collectionDisplayName", + "item", + "actorName", + "preview", + "createdAt", + "readAt" + ] + } + }, + "unread": { + "type": "integer", + "minimum": 0 + }, + "total": { + "type": "integer", + "minimum": 0 + }, + "readBefore": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "data", + "unread", + "total", + "readBefore" + ] + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/notifications/routes.ts" + } + }, + "/notifications/{id}/read": { + "post": { + "tags": [ + "notifications" + ], + "operationId": "post__notifications_id_read", + "summary": "POST /notifications/:id/read", + "description": "Активная человеческая сессия; собственное уведомление и текущий доступ к записи. Источник: apps/core/src/notifications/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Прочитано" + }, + "404": { + "description": "Уведомление чужое, отсутствует или запись недоступна" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/notifications/routes.ts" + } + }, + "/notifications/read-all": { + "post": { + "tags": [ + "notifications" + ], + "operationId": "post__notifications_read_all", + "summary": "POST /notifications/read-all", + "description": "Активная человеческая сессия; только собственные уведомления до границы снимка списка. Источник: apps/core/src/notifications/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "204": { + "description": "Прочитано" + }, + "400": { + "description": "Некорректная граница снимка" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/notifications/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "before": { + "type": "string", + "format": "date-time", + "description": "readBefore из ответа списка; будущие даты запрещены" + } + }, + "required": [ + "before" + ] + } + } + } + } + } + }, + "/oauth-apps": { + "get": { + "tags": [ + "oauth-apps" + ], + "operationId": "get__oauth_apps", + "summary": "GET /oauth-apps", + "description": "OAuth application configuration. policyManaged defaults to false; omission on update preserves policyManaged and scopeLabels. Catalog scopes require an audience, label keys must belong to scopes. In policy mode authorization requests use openid/profile/email only; personal permissions are emitted as resource_access[audience].roles. Changing audience or policy-management mode resets consent. Existing client type cannot change.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "name", + "description", + "enabled", + "clientType", + "redirectUris", + "userIds", + "audience", + "scopes" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "enabled": { + "type": "boolean" + }, + "clientType": { + "enum": [ + "public", + "confidential" + ] + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 10 + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "accessMode": { + "enum": [ + "all", + "selected", + "domains" + ] + }, + "emailDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "audience": { + "type": "string", + "maxLength": 200 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + }, + "policyManaged": { + "type": "boolean", + "description": "Use assigned policies instead of user/domain rules. Omission preserves the existing value; defaults to false on creation." + }, + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid application or permission catalog" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and settings access required" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/oauth/routes.ts" + }, + "post": { + "tags": [ + "oauth-apps" + ], + "operationId": "post__oauth_apps", + "summary": "POST /oauth-apps", + "description": "OAuth application configuration. policyManaged defaults to false; omission on update preserves policyManaged and scopeLabels. Catalog scopes require an audience, label keys must belong to scopes. In policy mode authorization requests use openid/profile/email only; personal permissions are emitted as resource_access[audience].roles. Changing audience or policy-management mode resets consent. Existing client type cannot change.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "201": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "application" + ], + "properties": { + "application": { + "type": "object", + "additionalProperties": true, + "required": [ + "name", + "description", + "enabled", + "clientType", + "redirectUris", + "userIds", + "audience", + "scopes" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "enabled": { + "type": "boolean" + }, + "clientType": { + "enum": [ + "public", + "confidential" + ] + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 10 + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "accessMode": { + "enum": [ + "all", + "selected", + "domains" + ] + }, + "emailDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "audience": { + "type": "string", + "maxLength": 200 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + }, + "policyManaged": { + "type": "boolean", + "description": "Use assigned policies instead of user/domain rules. Omission preserves the existing value; defaults to false on creation." + }, + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "clientSecret": { + "type": "string", + "description": "Returned once for a new confidential application" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid application or permission catalog" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and settings access required" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/oauth/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "description", + "enabled", + "clientType", + "redirectUris", + "userIds", + "audience", + "scopes" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "enabled": { + "type": "boolean" + }, + "clientType": { + "enum": [ + "public", + "confidential" + ] + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 10 + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "accessMode": { + "enum": [ + "all", + "selected", + "domains" + ] + }, + "emailDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "audience": { + "type": "string", + "maxLength": 200 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + }, + "policyManaged": { + "type": "boolean", + "description": "Use assigned policies instead of user/domain rules. Omission preserves the existing value; defaults to false on creation." + } + } + } + } + } + } + } + }, + "/oauth-apps/{id}": { + "put": { + "tags": [ + "oauth-apps" + ], + "operationId": "put__oauth_apps_id", + "summary": "PUT /oauth-apps/:id", + "description": "OAuth application configuration. policyManaged defaults to false; omission on update preserves policyManaged and scopeLabels. Catalog scopes require an audience, label keys must belong to scopes. In policy mode authorization requests use openid/profile/email only; personal permissions are emitted as resource_access[audience].roles. Changing audience or policy-management mode resets consent. Existing client type cannot change.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "application" + ], + "properties": { + "application": { + "type": "object", + "additionalProperties": true, + "required": [ + "name", + "description", + "enabled", + "clientType", + "redirectUris", + "userIds", + "audience", + "scopes" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "enabled": { + "type": "boolean" + }, + "clientType": { + "enum": [ + "public", + "confidential" + ] + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 10 + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "accessMode": { + "enum": [ + "all", + "selected", + "domains" + ] + }, + "emailDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "audience": { + "type": "string", + "maxLength": 200 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + }, + "policyManaged": { + "type": "boolean", + "description": "Use assigned policies instead of user/domain rules. Omission preserves the existing value; defaults to false on creation." + }, + "id": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "clientSecret": { + "type": "string", + "description": "Returned once for a new confidential application" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid application or permission catalog" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and settings access required" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/oauth/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "description", + "enabled", + "clientType", + "redirectUris", + "userIds", + "audience", + "scopes" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "enabled": { + "type": "boolean" + }, + "clientType": { + "enum": [ + "public", + "confidential" + ] + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 10 + }, + "userIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000 + }, + "accessMode": { + "enum": [ + "all", + "selected", + "domains" + ] + }, + "emailDomains": { + "type": "array", + "items": { + "type": "string" + } + }, + "audience": { + "type": "string", + "maxLength": 200 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + }, + "policyManaged": { + "type": "boolean", + "description": "Use assigned policies instead of user/domain rules. Omission preserves the existing value; defaults to false on creation." + } + } + } + } + } + } + } + }, + "/oauth-apps/{id}/secret": { + "post": { + "tags": [ + "oauth-apps" + ], + "operationId": "post__oauth_apps_id_secret", + "summary": "POST /oauth-apps/:id/secret", + "description": "Человек: oauth/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/routes.ts" + } + }, + "/oauth-apps/status": { + "get": { + "tags": [ + "oauth-apps" + ], + "operationId": "get__oauth_apps_status", + "summary": "GET /oauth-apps/status", + "description": "Человек: oauth/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/routes.ts" + } + }, + "/oauth-interactions/{uid}": { + "get": { + "tags": [ + "oauth-interactions" + ], + "operationId": "get__oauth_interactions_uid", + "summary": "GET /oauth-interactions/:uid", + "description": "Человек; UID, собственная сессия, правила приложения и согласие. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/interactions.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "uid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/interactions.ts" + }, + "post": { + "tags": [ + "oauth-interactions" + ], + "operationId": "post__oauth_interactions_uid", + "summary": "POST /oauth-interactions/:uid", + "description": "Человек; UID, собственная сессия, правила приложения и согласие. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/interactions.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "uid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/interactions.ts" + } + }, + "/oauth/complete/{uid}": { + "post": { + "tags": [ + "oauth" + ], + "operationId": "post__oauth_complete_uid", + "summary": "POST /oauth/complete/:uid", + "description": "Текущий пользователь, подписанные cookies взаимодействия OAuth и совпадающий Origin; идентификатор пользователя должен совпадать с сессией. Источник: apps/core/src/oauth/interactions.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "uid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Решение принято; перейти на redirectTo", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "redirectTo": { + "type": "string" + } + }, + "required": [ + "redirectTo" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Некорректный запрос" + }, + "401": { + "description": "Данные входа или browser proof недействительны" + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "429": { + "description": "Лимит попыток" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/oauth/interactions.ts", + "servers": [ + { + "url": "http://localhost:3000", + "description": "Публичный origin без /api; callback сохраняет зарегистрированный путь" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "approve": { + "type": "boolean" + }, + "userId": { + "type": "string" + }, + "reuse": { + "type": "boolean" + }, + "serviceScopes": { + "type": "array", + "maxItems": 30, + "items": { + "type": "string", + "maxLength": 160 + }, + "description": "Displayed servicePermissions.scope values. Required for explicit approval of a policy-managed application. A changed permission set returns 409 and requires a new sign-in." + } + }, + "required": [ + "approve", + "userId" + ] + } + } + } + } + } + }, + "/permissions": { + "get": { + "tags": [ + "permissions" + ], + "operationId": "get__permissions", + "summary": "GET /permissions", + "description": "Человек: policies/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + }, + "post": { + "tags": [ + "permissions" + ], + "operationId": "post__permissions", + "summary": "POST /permissions", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + } + }, + "/permissions/{id}": { + "delete": { + "tags": [ + "permissions" + ], + "operationId": "delete__permissions_id", + "summary": "DELETE /permissions/:id", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + }, + "get": { + "tags": [ + "permissions" + ], + "operationId": "get__permissions_id", + "summary": "GET /permissions/:id", + "description": "Человек: policies/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + }, + "patch": { + "tags": [ + "permissions" + ], + "operationId": "patch__permissions_id", + "summary": "PATCH /permissions/:id", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + } + }, + "/permissions/me": { + "get": { + "tags": [ + "permissions" + ], + "operationId": "get__permissions_me", + "summary": "GET /permissions/me", + "description": "Активный principal; только собственные права. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/permissions/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/permissions/routes.ts" + } + }, + "/policies": { + "get": { + "tags": [ + "policies" + ], + "operationId": "get__policies", + "summary": "GET /policies", + "description": "Человек: policies/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + }, + "post": { + "tags": [ + "policies" + ], + "operationId": "post__policies", + "summary": "POST /policies", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + } + }, + "/policies/{id}": { + "delete": { + "tags": [ + "policies" + ], + "operationId": "delete__policies_id", + "summary": "DELETE /policies/:id", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + }, + "get": { + "tags": [ + "policies" + ], + "operationId": "get__policies_id", + "summary": "GET /policies/:id", + "description": "Человек: policies/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + }, + "patch": { + "tags": [ + "policies" + ], + "operationId": "patch__policies_id", + "summary": "PATCH /policies/:id", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + } + }, + "/policies/{id}/permissions/{permissionId}": { + "delete": { + "tags": [ + "policies" + ], + "operationId": "delete__policies_id_permissions_permissionId", + "summary": "DELETE /policies/:id/permissions/:permissionId", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "permissionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + }, + "put": { + "tags": [ + "policies" + ], + "operationId": "put__policies_id_permissions_permissionId", + "summary": "PUT /policies/:id/permissions/:permissionId", + "description": "Только человек-superuser: изменение состава политик и permissions. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "permissionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + } + }, + "/policies/{id}/users": { + "put": { + "tags": [ + "policies" + ], + "operationId": "put__policies_id_users", + "summary": "PUT /policies/:id/users", + "description": "Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; атомарная замена назначений без изменения себя и superuser. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Назначения заменены атомарно. Менеджер не меняет собственные назначения или назначения superuser." + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/policies/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userIds": { + "type": "array", + "maxItems": 1000, + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + } + }, + "required": [ + "userIds" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/policies/{id}/users/{userId}": { + "delete": { + "tags": [ + "policies" + ], + "operationId": "delete__policies_id_users_userId", + "summary": "DELETE /policies/:id/users/:userId", + "description": "Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + }, + "put": { + "tags": [ + "policies" + ], + "operationId": "put__policies_id_users_userId", + "summary": "PUT /policies/:id/users/:userId", + "description": "Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/policies/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/policies/routes.ts" + } + }, + "/policies/applications": { + "get": { + "tags": [ + "policies" + ], + "operationId": "get__policies_applications", + "summary": "GET /policies/applications", + "description": "Catalog of policy-managed OAuth applications, including disabled applications. Requires policies/read or policies/update. Contains no client secrets. Catalog permissions do not grant access by themselves.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "enabled", + "audience", + "scopes", + "scopeLabels" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "audience": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 160 + }, + "maxItems": 30 + }, + "scopeLabels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 120 + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid application or permission catalog" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and settings access required" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/policies/routes.ts" + } + }, + "/presence": { + "post": { + "tags": [ + "presence" + ], + "operationId": "post__presence", + "summary": "POST /presence", + "description": "Активная человеческая сессия; каждый heartbeat проверяет read коллекции/строки или доступ раздела. /files требует files/read либо update у человека, или superuser. Возвращает только ID, имя, аватар и число окон; без email/сессий/значений записи. Источник: apps/core/src/presence/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Продление присутствия на 30 секунд и доступные участники. До 32 активных окон на сессию. Текущий пользователь идёт первым.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "minimum": 0, + "description": "Количество разных пользователей, включая текущего." + }, + "participants": { + "type": "array", + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "displayName": { + "type": "string" + }, + "pictureUrl": { + "type": [ + "string", + "null" + ] + }, + "views": { + "type": "integer", + "minimum": 1 + }, + "self": { + "type": "boolean" + } + }, + "required": [ + "id", + "displayName", + "pictureUrl", + "views", + "self" + ] + } + } + }, + "required": [ + "total", + "participants" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/presence/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "clientId": { + "type": "string", + "format": "uuid", + "description": "Случайный UUID одной открытой страницы; пользователь определяется только по Bearer-сессии." + }, + "scope": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "page" + }, + "page": { + "enum": [ + "/", + "/admin/collections", + "/files", + "/search", + "/settings", + "/access", + "/services", + "/oauth-apps", + "/admin/settings", + "/admin/settings/users", + "/admin/settings/policies", + "/admin/settings/services", + "/admin/settings/assistant", + "/admin/settings/plugins", + "/admin/settings/terms", + "/admin/settings/oauth", + "/admin/settings/integrations", + "/system-settings", + "/system-settings/users", + "/system-settings/policies", + "/system-settings/services", + "/system-settings/assistant", + "/system-settings/plugins", + "/system-settings/terms", + "/system-settings/oauth" + ] + } + }, + "required": [ + "kind", + "page" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "collection" + }, + "collection": { + "type": "string" + } + }, + "required": [ + "kind", + "collection" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "record" + }, + "collection": { + "type": "string" + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + }, + "required": [ + "kind", + "collection", + "id" + ] + } + ] + } + }, + "required": [ + "clientId", + "scope" + ] + } + } + } + } + } + }, + "/presence/{clientId}": { + "delete": { + "tags": [ + "presence" + ], + "operationId": "delete__presence_clientId", + "summary": "DELETE /presence/:clientId", + "description": "Активная человеческая сессия; удаляет только собственное окно этой сессии, без требования сохраняющегося read. Источник: apps/core/src/presence/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "clientId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Собственное окно удалено; повтор безопасен. Другие сессии не затрагиваются." + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/presence/routes.ts" + } + }, + "/public/files/{id}/content": { + "get": { + "tags": [ + "public" + ], + "operationId": "get__public_files_id_content", + "summary": "GET /public/files/:id/content", + "description": "Без авторизации: только явно опубликованный ready-файл (visibility=public). Приватный/удалённый файл — 404; no-store/CSP/nosniff. Без списка и метаданных.. Источник: apps/core/src/files/public-routes.ts", + "security": [], + "parameters": [ + { + "name": "preview", + "in": "query", + "schema": { + "enum": [ + "1" + ] + }, + "description": "Показать только поддержанное растровое превью" + } + ], + "responses": { + "200": { + "description": "Байты явно опубликованного ready-файла; no-store, nosniff и ограничительная CSP. Attachment по умолчанию.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/files/public-routes.ts" + } + }, + "/ready": { + "get": { + "tags": [ + "ready" + ], + "operationId": "get__ready", + "summary": "GET /ready", + "description": "Публичная проверка готовности, без секретов. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/health/readiness.ts", + "security": [], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/health/readiness.ts" + } + }, + "/realtime/locks": { + "delete": { + "tags": [ + "realtime" + ], + "operationId": "delete__realtime_locks", + "summary": "DELETE /realtime/locks", + "description": "Активная человеческая сессия; освобождает только собственную lease с совпадающим clientId, даже после потери read/update. Источник: apps/core/src/realtime/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "204": { + "description": "Собственная lease освобождена или уже отсутствует" + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/realtime/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "collection": { + "type": "string" + }, + "recordId": { + "type": "string" + }, + "field": { + "type": "string" + }, + "clientId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "collection", + "recordId", + "field", + "clientId" + ] + } + } + } + } + }, + "post": { + "tags": [ + "realtime" + ], + "operationId": "post__realtime_locks", + "summary": "POST /realtime/locks", + "description": "Активная человеческая сессия; read и update указанного поля и строки. Атомарная 30-секундная lease, конфликт 409 FIELD_LOCKED. Источник: apps/core/src/realtime/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Получена или продлена lease поля", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/realtime/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "collection": { + "type": "string" + }, + "recordId": { + "type": "string" + }, + "field": { + "type": "string" + }, + "clientId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "collection", + "recordId", + "field", + "clientId" + ] + } + } + } + } + } + }, + "/realtime/stream": { + "get": { + "tags": [ + "realtime" + ], + "operationId": "get__realtime_stream", + "summary": "GET /realtime/stream", + "description": "Активная человеческая сессия через Bearer token или браузерную HttpOnly cookie; scope проверяется теми же collection/row read правилами, что GET /items. Поток повторно проверяет доступ и закрывается при его потере. Presence только для этой сессии. Источник: apps/core/src/realtime/stream.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "clientId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "ID открытого окна" + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string", + "maxLength": 1024 + }, + "description": "JSON scope из PresenceInput" + } + ], + "responses": { + "200": { + "description": "SSE envelope; heartbeat-комментарии, событие presence.changed при подключении и изменении присутствия", + "content": { + "text/event-stream": { + "schema": { + "type": "string" + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/realtime/stream.ts" + } + }, + "/schema": { + "get": { + "tags": [ + "schema" + ], + "operationId": "get__schema", + "summary": "GET /schema", + "description": "Access-scoped collection and generated plugin-model contracts for SDK type generation. Accepts ordinary API authentication or a CLI schema:read credential. No records, defaults, secrets or permission conditions. Alias relations and untyped plugin actions are excluded. Query parameters are rejected.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Accessible collection schema", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "hash", + "collections" + ], + "properties": { + "version": { + "type": "integer", + "enum": [ + 1 + ] + }, + "hash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "methods": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "namespace", + "id", + "path", + "inputSchema", + "outputSchema" + ], + "properties": { + "namespace": { + "type": "string" + }, + "id": { + "type": "string" + }, + "path": { + "type": "string" + }, + "inputSchema": { + "type": "object", + "description": "Kit's bounded, closed JSON model-input schema" + }, + "outputSchema": { + "type": "object", + "description": "Kit's bounded, closed JSON model-output schema" + } + } + } + }, + "collections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "mode", + "primaryKey", + "actions", + "fields" + ], + "properties": { + "sourceKind": { + "enum": [ + "table", + "materialized-view" + ] + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "mode": { + "enum": [ + "single", + "multiple" + ] + }, + "primaryKey": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "type": { + "enum": [ + "uuid", + "serial", + "bigserial", + "text" + ] + } + } + }, + "actions": { + "type": "object", + "additionalProperties": false, + "required": [ + "read", + "create", + "update", + "delete" + ], + "properties": { + "read": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "update": { + "type": "boolean" + }, + "delete": { + "type": "boolean" + } + } + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type", + "nullable", + "read", + "create", + "update", + "requiredOnCreate" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "type": { + "enum": [ + "string", + "number", + "boolean", + "json", + "strings" + ] + }, + "nullable": { + "type": "boolean" + }, + "read": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "update": { + "type": "boolean" + }, + "requiredOnCreate": { + "type": "boolean" + }, + "enum": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "relationKey": { + "enum": [ + "uuid", + "serial", + "bigserial", + "text" + ] + }, + "filterKind": { + "enum": [ + "text", + "ordered", + "scalar", + "none" + ] + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Unexpected query parameters" + }, + "401": { + "description": "Authentication required" + }, + "503": { + "description": "Database unavailable" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/schema/routes.ts" + } + }, + "/search": { + "get": { + "tags": [ + "search" + ], + "operationId": "get__search", + "summary": "GET /search", + "description": "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/items/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/items/routes.ts" + } + }, + "/service-accounts": { + "get": { + "tags": [ + "service-accounts" + ], + "operationId": "get__service_accounts", + "summary": "GET /service-accounts", + "description": "Человек: services/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/routes.ts" + }, + "post": { + "tags": [ + "service-accounts" + ], + "operationId": "post__service_accounts", + "summary": "POST /service-accounts", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/routes.ts" + } + }, + "/service-accounts/{id}": { + "get": { + "tags": [ + "service-accounts" + ], + "operationId": "get__service_accounts_id", + "summary": "GET /service-accounts/:id", + "description": "Человек: services/read ИЛИ update; superuser bypass. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "id", + "name", + "description", + "status", + "createdAt", + "policyIds", + "keys", + "federations" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "enum": [ + "active", + "disabled" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "policyIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "keys": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "prefix", + "createdAt", + "expiresAt", + "lastUsedAt", + "lastActivityAt", + "requestCount", + "revokedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Последний успешный обмен ключа на токен" + }, + "lastActivityAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Последний успешный обмен или аутентификация HTTP-запроса токеном этого ключа" + }, + "requestCount": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Точный bigint строкой: HTTP-запросы с проверенным токеном, включая последующий отказ в правах. Обмены, неверные и истёкшие токены не учитываются; история до включения статистики не восстанавливается." + }, + "revokedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + } + }, + "federations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "projectPath": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "audience": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "revokedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + } + } + } + } + } + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/services/routes.ts" + }, + "put": { + "tags": [ + "service-accounts" + ], + "operationId": "put__service_accounts_id", + "summary": "PUT /service-accounts/:id", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/routes.ts" + } + }, + "/service-accounts/{id}/federations": { + "post": { + "tags": [ + "service-accounts" + ], + "operationId": "post__service_accounts_id_federations", + "summary": "POST /service-accounts/:id/federations", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/federation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/federation-routes.ts" + } + }, + "/service-accounts/{id}/federations/{federationId}": { + "delete": { + "tags": [ + "service-accounts" + ], + "operationId": "delete__service_accounts_id_federations_federationId", + "summary": "DELETE /service-accounts/:id/federations/:federationId", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/federation-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "federationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/federation-routes.ts" + } + }, + "/service-accounts/{id}/keys": { + "post": { + "tags": [ + "service-accounts" + ], + "operationId": "post__service_accounts_id_keys", + "summary": "POST /service-accounts/:id/keys", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "prefix", + "createdAt", + "expiresAt", + "lastUsedAt", + "lastActivityAt", + "requestCount", + "revokedAt", + "secret" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Последний успешный обмен ключа на токен" + }, + "lastActivityAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Последний успешный обмен или аутентификация HTTP-запроса токеном этого ключа" + }, + "requestCount": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Точный bigint строкой: HTTP-запросы с проверенным токеном, включая последующий отказ в правах. Обмены, неверные и истёкшие токены не учитываются; история до включения статистики не восстанавливается." + }, + "revokedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "secret": { + "type": "string", + "description": "Одноразовое значение; больше не возвращается", + "pattern": "^asm_sk_[A-Za-z0-9_-]{43}$" + } + } + } + } + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/services/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "expiresInDays": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "default": 90 + } + } + } + } + } + } + } + }, + "/service-accounts/{id}/keys/{keyId}": { + "delete": { + "tags": [ + "service-accounts" + ], + "operationId": "delete__service_accounts_id_keys_keyId", + "summary": "DELETE /service-accounts/:id/keys/:keyId", + "description": "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/services/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "keyId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/services/routes.ts" + } + }, + "/settings/access": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_access", + "summary": "GET /settings/access", + "description": "Активный человек; разделы, режим изменения и разрешённый набор назначаемых политик из актуальной БД. Источник: apps/core/src/settings/access-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "sections": { + "type": "array", + "items": { + "enum": [ + "users", + "policies", + "plugins", + "assistant", + "terms", + "services", + "oauth", + "files" + ] + } + }, + "editableSections": { + "type": "array", + "items": { + "enum": [ + "users", + "policies", + "plugins", + "assistant", + "terms", + "services", + "oauth", + "files" + ] + } + }, + "canManagePolicies": { + "type": "boolean", + "description": "Только человек-superuser может менять состав политик и permissions." + }, + "delegatablePolicyIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Явный набор готовых политик для назначения. Не даёт прав на соответствующий раздел и не назначает их пользователю. Superuser не ограничен этим списком." + } + }, + "required": [ + "sections", + "editableSections", + "canManagePolicies", + "delegatablePolicyIds" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/settings/access-routes.ts" + } + }, + "/settings/assistant": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_assistant", + "summary": "GET /settings/assistant", + "description": "Человек: assistant/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/routes.ts" + }, + "put": { + "tags": [ + "settings" + ], + "operationId": "put__settings_assistant", + "summary": "PUT /settings/assistant", + "description": "Человек: assistant/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/routes.ts" + } + }, + "/settings/assistant/telemetry": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_assistant_telemetry", + "summary": "GET /settings/assistant/telemetry", + "description": "Человек: assistant/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/routes.ts" + } + }, + "/settings/integrations": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_integrations", + "summary": "GET /settings/integrations", + "description": "Только активная человеческая сессия superuser; безопасные параметры и наличие ключей, без значений секретов. Источник: apps/core/src/integrations/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "monitoring", + "google", + "revision", + "storage", + "assistant", + "encryption", + "bootstrap", + "savedSecretCount" + ], + "properties": { + "monitoring": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "environment", + "release", + "errorsCore", + "errorsBrowser", + "performanceCore", + "performanceBrowser", + "databaseSpans", + "tracesSampleRate" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Без пробелов, слешей и зарезервированного имени None." + }, + "release": { + "type": "string", + "maxLength": 120 + }, + "errorsCore": { + "type": "boolean" + }, + "errorsBrowser": { + "type": "boolean" + }, + "performanceCore": { + "type": "boolean" + }, + "performanceBrowser": { + "type": "boolean" + }, + "databaseSpans": { + "type": "boolean" + }, + "tracesSampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "serverDsn", + "browserDsn" + ], + "properties": { + "serverDsn": { + "type": "boolean" + }, + "browserDsn": { + "type": "boolean" + } + } + } + } + }, + "google": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "clientId", + "redirectUri" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientId": { + "type": "string", + "maxLength": 256 + }, + "redirectUri": { + "type": "string", + "maxLength": 2048 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "clientSecret" + ], + "properties": { + "clientSecret": { + "type": "boolean" + } + } + } + } + }, + "revision": { + "type": "string", + "format": "uuid" + }, + "storage": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "provider", + "bucket", + "region", + "endpoint", + "serviceAccountId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "provider": { + "enum": [ + "s3", + "yandex" + ] + }, + "bucket": { + "type": "string", + "maxLength": 63 + }, + "region": { + "type": "string", + "maxLength": 120 + }, + "endpoint": { + "type": "string", + "maxLength": 2048 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "accessKeyId", + "secretAccessKey", + "sessionToken" + ], + "properties": { + "accessKeyId": { + "type": "boolean" + }, + "secretAccessKey": { + "type": "boolean" + }, + "sessionToken": { + "type": "boolean" + } + } + } + } + }, + "assistant": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "baseURL", + "model", + "api", + "timeoutMs", + "maxOutputTokens", + "effort", + "thinking" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "baseURL": { + "type": "string", + "maxLength": 2048 + }, + "model": { + "type": "string", + "maxLength": 120 + }, + "api": { + "enum": [ + "responses", + "chat-completions" + ] + }, + "timeoutMs": { + "type": "integer", + "minimum": 1000, + "maximum": 120000 + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 128, + "maximum": 16384 + }, + "effort": { + "enum": [ + "", + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "apiKey" + ], + "properties": { + "apiKey": { + "type": "boolean" + } + } + } + } + }, + "encryption": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "keyId", + "serviceAccountId" + ], + "properties": { + "provider": { + "enum": [ + "local", + "yandex-kms" + ] + }, + "keyId": { + "type": "string", + "maxLength": 120 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": {} + } + } + }, + "bootstrap": { + "type": "object", + "additionalProperties": false, + "required": [ + "localKey", + "workloadIdentity" + ], + "properties": { + "localKey": { + "type": "boolean" + }, + "workloadIdentity": { + "type": "boolean" + } + } + }, + "savedSecretCount": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } + } + }, + "400": { + "description": "Неверные параметры" + }, + "401": { + "description": "Нужна человеческая сессия" + }, + "403": { + "description": "Нужен superuser" + }, + "503": { + "description": "Провайдер или ключ шифрования недоступен; секреты не возвращаются" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/integrations/routes.ts" + } + }, + "/settings/integrations/{section}": { + "put": { + "tags": [ + "settings" + ], + "operationId": "put__settings_integrations_section", + "summary": "PUT /settings/integrations/:section", + "description": "Только superuser; полный запрет изменения env-группы, revision, атомарное перешифрование при смене защиты. Источник: apps/core/src/integrations/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "section", + "in": "path", + "required": true, + "schema": { + "enum": [ + "storage", + "assistant", + "encryption", + "google", + "monitoring" + ] + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "monitoring", + "google", + "revision", + "storage", + "assistant", + "encryption", + "bootstrap", + "savedSecretCount" + ], + "properties": { + "monitoring": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "environment", + "release", + "errorsCore", + "errorsBrowser", + "performanceCore", + "performanceBrowser", + "databaseSpans", + "tracesSampleRate" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Без пробелов, слешей и зарезервированного имени None." + }, + "release": { + "type": "string", + "maxLength": 120 + }, + "errorsCore": { + "type": "boolean" + }, + "errorsBrowser": { + "type": "boolean" + }, + "performanceCore": { + "type": "boolean" + }, + "performanceBrowser": { + "type": "boolean" + }, + "databaseSpans": { + "type": "boolean" + }, + "tracesSampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "serverDsn", + "browserDsn" + ], + "properties": { + "serverDsn": { + "type": "boolean" + }, + "browserDsn": { + "type": "boolean" + } + } + } + } + }, + "google": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "clientId", + "redirectUri" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientId": { + "type": "string", + "maxLength": 256 + }, + "redirectUri": { + "type": "string", + "maxLength": 2048 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "clientSecret" + ], + "properties": { + "clientSecret": { + "type": "boolean" + } + } + } + } + }, + "revision": { + "type": "string", + "format": "uuid" + }, + "storage": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "provider", + "bucket", + "region", + "endpoint", + "serviceAccountId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "provider": { + "enum": [ + "s3", + "yandex" + ] + }, + "bucket": { + "type": "string", + "maxLength": 63 + }, + "region": { + "type": "string", + "maxLength": 120 + }, + "endpoint": { + "type": "string", + "maxLength": 2048 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "accessKeyId", + "secretAccessKey", + "sessionToken" + ], + "properties": { + "accessKeyId": { + "type": "boolean" + }, + "secretAccessKey": { + "type": "boolean" + }, + "sessionToken": { + "type": "boolean" + } + } + } + } + }, + "assistant": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "baseURL", + "model", + "api", + "timeoutMs", + "maxOutputTokens", + "effort", + "thinking" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "baseURL": { + "type": "string", + "maxLength": 2048 + }, + "model": { + "type": "string", + "maxLength": 120 + }, + "api": { + "enum": [ + "responses", + "chat-completions" + ] + }, + "timeoutMs": { + "type": "integer", + "minimum": 1000, + "maximum": 120000 + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 128, + "maximum": 16384 + }, + "effort": { + "enum": [ + "", + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "apiKey" + ], + "properties": { + "apiKey": { + "type": "boolean" + } + } + } + } + }, + "encryption": { + "type": "object", + "additionalProperties": false, + "required": [ + "source", + "readOnly", + "value", + "secrets" + ], + "properties": { + "source": { + "enum": [ + "environment", + "admin" + ] + }, + "readOnly": { + "type": "boolean" + }, + "value": { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "keyId", + "serviceAccountId" + ], + "properties": { + "provider": { + "enum": [ + "local", + "yandex-kms" + ] + }, + "keyId": { + "type": "string", + "maxLength": 120 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": {} + } + } + }, + "bootstrap": { + "type": "object", + "additionalProperties": false, + "required": [ + "localKey", + "workloadIdentity" + ], + "properties": { + "localKey": { + "type": "boolean" + }, + "workloadIdentity": { + "type": "boolean" + } + } + }, + "savedSecretCount": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } + } + }, + "400": { + "description": "Неверные параметры" + }, + "401": { + "description": "Нужна человеческая сессия" + }, + "403": { + "description": "Нужен superuser" + }, + "409": { + "description": "Настройки изменились, группа заблокирована env, подключение выключено или хранилище содержит файлы" + }, + "503": { + "description": "Провайдер или ключ шифрования недоступен; секреты не возвращаются" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/integrations/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "revision", + "value" + ], + "properties": { + "revision": { + "type": "string", + "format": "uuid" + }, + "value": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "provider", + "bucket", + "region", + "endpoint", + "serviceAccountId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "provider": { + "enum": [ + "s3", + "yandex" + ] + }, + "bucket": { + "type": "string", + "maxLength": 63 + }, + "region": { + "type": "string", + "maxLength": 120 + }, + "endpoint": { + "type": "string", + "maxLength": 2048 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "baseURL", + "model", + "api", + "timeoutMs", + "maxOutputTokens", + "effort", + "thinking" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "baseURL": { + "type": "string", + "maxLength": 2048 + }, + "model": { + "type": "string", + "maxLength": 120 + }, + "api": { + "enum": [ + "responses", + "chat-completions" + ] + }, + "timeoutMs": { + "type": "integer", + "minimum": 1000, + "maximum": 120000 + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 128, + "maximum": 16384 + }, + "effort": { + "enum": [ + "", + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": [ + "boolean", + "null" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "keyId", + "serviceAccountId" + ], + "properties": { + "provider": { + "enum": [ + "local", + "yandex-kms" + ] + }, + "keyId": { + "type": "string", + "maxLength": 120 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "clientId", + "redirectUri" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientId": { + "type": "string", + "maxLength": 256 + }, + "redirectUri": { + "type": "string", + "maxLength": 2048 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "environment", + "release", + "errorsCore", + "errorsBrowser", + "performanceCore", + "performanceBrowser", + "databaseSpans", + "tracesSampleRate" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Без пробелов, слешей и зарезервированного имени None." + }, + "release": { + "type": "string", + "maxLength": 120 + }, + "errorsCore": { + "type": "boolean" + }, + "errorsBrowser": { + "type": "boolean" + }, + "performanceCore": { + "type": "boolean" + }, + "performanceBrowser": { + "type": "boolean" + }, + "databaseSpans": { + "type": "boolean" + }, + "tracesSampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + ] + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "if": { + "required": [ + "dsn" + ] + }, + "then": { + "maxProperties": 1 + } + } + ], + "properties": { + "accessKeyId": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "secretAccessKey": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "sessionToken": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "apiKey": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "clientSecret": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "dsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "Совместимый общий DSN мониторинга. Допускается отдельно от других ключей; задаёт общий DSN и очищает отдельные. null удаляет все DSN. Используйте serverDsn и browserDsn для независимой настройки.", + "deprecated": true + }, + "serverDsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "browserDsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + } + } + } + } + } + } + } + } + } + }, + "/settings/integrations/{section}/test": { + "post": { + "tags": [ + "settings" + ], + "operationId": "post__settings_integrations_section_test", + "summary": "POST /settings/integrations/:section/test", + "description": "Только superuser; credential rate limit, проверка соединения без сохранения настроек. Источник: apps/core/src/integrations/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "section", + "in": "path", + "required": true, + "schema": { + "enum": [ + "storage", + "assistant", + "encryption", + "google", + "monitoring" + ] + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "ok" + ], + "properties": { + "ok": { + "const": true + } + } + } + } + } + } + } + }, + "400": { + "description": "Неверные параметры" + }, + "401": { + "description": "Нужна человеческая сессия" + }, + "403": { + "description": "Нужен superuser" + }, + "409": { + "description": "Настройки изменились, группа заблокирована env, подключение выключено или хранилище содержит файлы" + }, + "429": { + "description": "Слишком много проверок" + }, + "503": { + "description": "Провайдер или ключ шифрования недоступен; секреты не возвращаются" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/integrations/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "revision", + "value" + ], + "properties": { + "revision": { + "type": "string", + "format": "uuid" + }, + "value": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "provider", + "bucket", + "region", + "endpoint", + "serviceAccountId" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "provider": { + "enum": [ + "s3", + "yandex" + ] + }, + "bucket": { + "type": "string", + "maxLength": 63 + }, + "region": { + "type": "string", + "maxLength": 120 + }, + "endpoint": { + "type": "string", + "maxLength": 2048 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "baseURL", + "model", + "api", + "timeoutMs", + "maxOutputTokens", + "effort", + "thinking" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "baseURL": { + "type": "string", + "maxLength": 2048 + }, + "model": { + "type": "string", + "maxLength": 120 + }, + "api": { + "enum": [ + "responses", + "chat-completions" + ] + }, + "timeoutMs": { + "type": "integer", + "minimum": 1000, + "maximum": 120000 + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 128, + "maximum": 16384 + }, + "effort": { + "enum": [ + "", + "low", + "medium", + "high", + "max" + ] + }, + "thinking": { + "type": [ + "boolean", + "null" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "keyId", + "serviceAccountId" + ], + "properties": { + "provider": { + "enum": [ + "local", + "yandex-kms" + ] + }, + "keyId": { + "type": "string", + "maxLength": 120 + }, + "serviceAccountId": { + "type": "string", + "maxLength": 120 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "clientId", + "redirectUri" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientId": { + "type": "string", + "maxLength": 256 + }, + "redirectUri": { + "type": "string", + "maxLength": 2048 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "environment", + "release", + "errorsCore", + "errorsBrowser", + "performanceCore", + "performanceBrowser", + "databaseSpans", + "tracesSampleRate" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Без пробелов, слешей и зарезервированного имени None." + }, + "release": { + "type": "string", + "maxLength": 120 + }, + "errorsCore": { + "type": "boolean" + }, + "errorsBrowser": { + "type": "boolean" + }, + "performanceCore": { + "type": "boolean" + }, + "performanceBrowser": { + "type": "boolean" + }, + "databaseSpans": { + "type": "boolean" + }, + "tracesSampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + ] + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "if": { + "required": [ + "dsn" + ] + }, + "then": { + "maxProperties": 1 + } + } + ], + "properties": { + "accessKeyId": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "secretAccessKey": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "sessionToken": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "apiKey": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "clientSecret": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "dsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "Совместимый общий DSN мониторинга. Допускается отдельно от других ключей; задаёт общий DSN и очищает отдельные. null удаляет все DSN. Используйте serverDsn и browserDsn для независимой настройки.", + "deprecated": true + }, + "serverDsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + }, + "browserDsn": { + "type": [ + "string", + "null" + ], + "writeOnly": true, + "maxLength": 8000, + "description": "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена." + } + } + } + } + }, + { + "type": "object", + "maxProperties": 0, + "description": "Для подключения из env сервер проверяет текущую конфигурацию" + } + ] + } + } + } + } + } + }, + "/settings/monitoring": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_monitoring", + "summary": "GET /settings/monitoring", + "description": "Только human superuser; локальные метрики текущего экземпляра Core, без DSN и пользовательских данных. Источник: apps/core/src/monitoring/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех; private, no-store. Метрики только текущего экземпляра Core.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "issue", + "enabled", + "windowSeconds", + "maxSamplesPerRoute", + "maxRoutes", + "routesLimited", + "routes", + "runtime" + ], + "properties": { + "issue": { + "enum": [ + null, + "configuration_unavailable" + ] + }, + "enabled": { + "type": "boolean" + }, + "windowSeconds": { + "const": 900 + }, + "maxSamplesPerRoute": { + "const": 2000 + }, + "maxRoutes": { + "const": 128 + }, + "routesLimited": { + "type": "boolean" + }, + "routes": { + "type": "array", + "maxItems": 128, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "route", + "samples", + "limited", + "errors", + "p50Ms", + "p95Ms", + "p99Ms" + ], + "properties": { + "route": { + "type": "string" + }, + "samples": { + "type": "integer", + "minimum": 0, + "maximum": 2000 + }, + "limited": { + "type": "boolean" + }, + "errors": { + "type": "integer", + "minimum": 0 + }, + "p50Ms": { + "type": "number", + "minimum": 0 + }, + "p95Ms": { + "type": "number", + "minimum": 0 + }, + "p99Ms": { + "type": "number", + "minimum": 0 + } + } + } + }, + "runtime": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "rssMb", + "heapUsedMb", + "eventLoopP99Ms", + "poolUsed", + "poolFree", + "poolPending" + ], + "properties": { + "rssMb": { + "type": "integer", + "minimum": 0 + }, + "heapUsedMb": { + "type": "integer", + "minimum": 0 + }, + "eventLoopP99Ms": { + "type": "number", + "minimum": 0 + }, + "poolUsed": { + "type": "integer", + "minimum": 0 + }, + "poolFree": { + "type": "integer", + "minimum": 0 + }, + "poolPending": { + "type": "integer", + "minimum": 0 + } + } + } + ] + } + } + } + } + } + } + } + }, + "401": { + "description": "Нужна активная человеческая сессия; сервисные токены не допускаются." + }, + "403": { + "description": "Нужен superuser; право не делегируется." + }, + "503": { + "description": "База/конфигурация недоступна." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/monitoring/routes.ts" + } + }, + "/settings/options/collections": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_options_collections", + "summary": "GET /settings/options/collections", + "description": "Человек: policies; только проекция схемы без записей; read ИЛИ update соответствующего раздела. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/access-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/access-routes.ts" + } + }, + "/settings/options/policies": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_options_policies", + "summary": "GET /settings/options/policies", + "description": "Человек: users ИЛИ policies ИЛИ services; только ID/названия; read ИЛИ update соответствующего раздела. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/access-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/access-routes.ts" + } + }, + "/settings/options/users": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_options_users", + "summary": "GET /settings/options/users", + "description": "Человек: oauth; только ID/email; read ИЛИ update соответствующего раздела. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/settings/access-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/settings/access-routes.ts" + } + }, + "/settings/plugins": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_plugins", + "summary": "GET /settings/plugins", + "description": "Человек: plugins/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/plugins/settings-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/plugins/settings-routes.ts" + } + }, + "/settings/plugins/{namespace}": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_plugins_namespace", + "summary": "GET /settings/plugins/:namespace", + "description": "Человек: plugins/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/plugins/settings-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "namespace", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/plugins/settings-routes.ts" + }, + "put": { + "tags": [ + "settings" + ], + "operationId": "put__settings_plugins_namespace", + "summary": "PUT /settings/plugins/:namespace", + "description": "Человек: plugins/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/plugins/settings-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "namespace", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/plugins/settings-routes.ts" + } + }, + "/settings/terms": { + "get": { + "tags": [ + "settings" + ], + "operationId": "get__settings_terms", + "summary": "GET /settings/terms", + "description": "Человек: terms/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + }, + "post": { + "tags": [ + "settings" + ], + "operationId": "post__settings_terms", + "summary": "POST /settings/terms", + "description": "Человек: terms/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + } + }, + "/settings/terms/{id}": { + "put": { + "tags": [ + "settings" + ], + "operationId": "put__settings_terms_id", + "summary": "PUT /settings/terms/:id", + "description": "Человек: terms/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/terms/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/terms/routes.ts" + } + }, + "/sign/sso/{provider}": { + "post": { + "tags": [ + "sign" + ], + "operationId": "post__sign_sso_provider", + "summary": "POST /sign/sso/:provider", + "description": "Точный Origin AUTH_UI_URL; настроенный провайдер; для link необходима активная сессия.. Источник: apps/core/src/auth/browser/sso.ts", + "security": [], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "303": { + "description": "Перенаправление к провайдеру или на страницу ошибки; proof в HttpOnly cookie", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + }, + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Origin не совпадает с AUTH_UI_URL" + }, + "404": { + "description": "Провайдер не настроен" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/sso.ts", + "servers": [ + { + "url": "http://localhost:3000", + "description": "Публичный origin без /api; callback сохраняет зарегистрированный путь" + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "intent": { + "enum": [ + "login", + "link" + ] + }, + "next": { + "type": "string" + } + } + } + } + } + } + } + }, + "/sign/sso/{provider}/callback": { + "get": { + "tags": [ + "sign" + ], + "operationId": "get__sign_sso_provider_callback", + "summary": "GET /sign/sso/:provider/callback", + "description": "Одноразовые state/PKCE и browser proof; для link проверяется сессия владельца.. Источник: apps/core/src/auth/browser/sso.ts", + "security": [], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "code", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "error", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "303": { + "description": "Проверены одноразовое состояние и browser proof; результат в перенаправлении. Для linking требуется текущая сессия.", + "headers": { + "Set-Cookie": { + "description": "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.", + "schema": { + "type": "string" + } + }, + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/browser/sso.ts", + "servers": [ + { + "url": "http://localhost:3000", + "description": "Публичный origin без /api; callback сохраняет зарегистрированный путь" + } + ] + } + }, + "/system-collections": { + "get": { + "tags": [ + "system-collections" + ], + "operationId": "get__system_collections", + "summary": "GET /system-collections", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "enum": [ + "users", + "files", + "policies", + "workspaces", + "service_accounts" + ] + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "nullable": { + "type": "boolean" + }, + "defaultValue": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "presentation": { + "type": "object", + "description": "Standard field presentation (labels/translations, interface, choices, constraints, layout and display). Behavior rules, sensitive history and relation filters are rejected.", + "additionalProperties": true + }, + "relation": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "m2o" + }, + "collection": { + "type": "string" + }, + "primaryKey": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "serial", + "bigserial", + "uuid", + "text" + ] + } + }, + "required": [ + "name", + "type" + ] + }, + "onDelete": { + "const": "setNull" + } + }, + "required": [ + "kind", + "collection", + "primaryKey", + "onDelete" + ] + } + }, + "required": [ + "name", + "type", + "managed", + "required", + "nullable" + ] + } + } + }, + "required": [ + "name", + "fields" + ] + } + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts" + } + }, + "/system-collections/{name}/fields/{field}": { + "delete": { + "tags": [ + "system-collections" + ], + "operationId": "delete__system_collections_name_fields_field", + "summary": "DELETE /system-collections/:name/fields/:field", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Custom column and its values removed. Builtins and dependent views are protected." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts" + } + }, + "/system-collections/{name}/fields/{field}/configuration": { + "post": { + "tags": [ + "system-collections" + ], + "operationId": "post__system_collections_name_fields_field_configuration", + "summary": "POST /system-collections/:name/fields/:field/configuration", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "enum": [ + "users", + "files", + "policies", + "workspaces", + "service_accounts" + ] + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "nullable": { + "type": "boolean" + }, + "defaultValue": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "presentation": { + "type": "object", + "description": "Standard field presentation (labels/translations, interface, choices, constraints, layout and display). Behavior rules, sensitive history and relation filters are rejected.", + "additionalProperties": true + }, + "relation": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "m2o" + }, + "collection": { + "type": "string" + }, + "primaryKey": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "serial", + "bigserial", + "uuid", + "text" + ] + } + }, + "required": [ + "name", + "type" + ] + }, + "onDelete": { + "const": "setNull" + } + }, + "required": [ + "kind", + "collection", + "primaryKey", + "onDelete" + ] + } + }, + "required": [ + "name", + "type", + "managed", + "required", + "nullable" + ] + } + } + }, + "required": [ + "name", + "fields" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "field": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "type": { + "enum": [ + "text", + "integer", + "bigint", + "boolean", + "datetime", + "date", + "email", + "decimal", + "json", + "uuid", + "file", + "files" + ] + }, + "required": { + "const": false + }, + "nullable": { + "const": true + }, + "defaultValue": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + } + }, + "required": [ + "name", + "type" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + }, + "type": { + "const": "relation" + }, + "targetCollection": { + "type": "string" + }, + "required": { + "const": false + }, + "nullable": { + "const": true + } + }, + "required": [ + "name", + "type", + "targetCollection" + ] + } + ] + }, + "presentation": { + "type": "object", + "description": "Standard field presentation (labels/translations, interface, choices, constraints, layout and display). Behavior rules, sensitive history and relation filters are rejected.", + "additionalProperties": true + } + }, + "required": [ + "field" + ], + "minProperties": 1, + "description": "Custom M2O fields target ordinary registered tables. They remain optional, use ON DELETE SET NULL and have no default or reverse field. Existing relation targets and key types are immutable; only presentation can be changed." + } + } + } + } + }, + "put": { + "tags": [ + "system-collections" + ], + "operationId": "put__system_collections_name_fields_field_configuration", + "summary": "PUT /system-collections/:name/fields/:field/configuration", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "field", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "enum": [ + "users", + "files", + "policies", + "workspaces", + "service_accounts" + ] + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "nullable": { + "type": "boolean" + }, + "defaultValue": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "presentation": { + "type": "object", + "description": "Standard field presentation (labels/translations, interface, choices, constraints, layout and display). Behavior rules, sensitive history and relation filters are rejected.", + "additionalProperties": true + }, + "relation": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "const": "m2o" + }, + "collection": { + "type": "string" + }, + "primaryKey": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "serial", + "bigserial", + "uuid", + "text" + ] + } + }, + "required": [ + "name", + "type" + ] + }, + "onDelete": { + "const": "setNull" + } + }, + "required": [ + "kind", + "collection", + "primaryKey", + "onDelete" + ] + } + }, + "required": [ + "name", + "type", + "managed", + "required", + "nullable" + ] + } + } + }, + "required": [ + "name", + "fields" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "field": { + "type": "object", + "additionalProperties": false, + "properties": { + "required": { + "const": false + }, + "nullable": { + "const": true + }, + "defaultValue": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + } + }, + "required": [] + }, + "presentation": { + "type": "object", + "description": "Standard field presentation (labels/translations, interface, choices, constraints, layout and display). Behavior rules, sensitive history and relation filters are rejected.", + "additionalProperties": true + } + }, + "required": [], + "minProperties": 1, + "description": "Custom M2O fields target ordinary registered tables. They remain optional, use ON DELETE SET NULL and have no default or reverse field. Existing relation targets and key types are immutable; only presentation can be changed." + } + } + } + } + } + }, + "/system-collections/{name}/records": { + "get": { + "tags": [ + "system-collections" + ], + "operationId": "get__system_collections_name_records", + "summary": "GET /system-collections/:name/records", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string", + "maxLength": 200 + }, + "description": "Search builtin record labels. Custom values are omitted from this selector." + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 1 + }, + "description": "25 records per page, stable order by label and ID." + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "description": "Only registered custom fields; builtin fields are always rejected on write and excluded on read." + } + }, + "required": [ + "id", + "label", + "values" + ] + } + }, + "page": { + "type": "integer" + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "records", + "page", + "hasMore" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts" + } + }, + "/system-collections/{name}/records/{id}": { + "get": { + "tags": [ + "system-collections" + ], + "operationId": "get__system_collections_name_records_id", + "summary": "GET /system-collections/:name/records/:id", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "description": "Only registered custom fields; builtin fields are always rejected on write and excluded on read." + } + }, + "required": [ + "id", + "label", + "values" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts" + }, + "patch": { + "tags": [ + "system-collections" + ], + "operationId": "patch__system_collections_name_records_id", + "summary": "PATCH /system-collections/:name/records/:id", + "description": "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены. Источник: apps/core/src/system-collections/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "description": "Only registered custom fields; builtin fields are always rejected on write and excluded on read." + } + }, + "required": [ + "id", + "label", + "values" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "401": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "403": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "404": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "409": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + }, + "default": { + "description": "Validation, authentication, authorization, missing entity or dependency conflict." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/system-collections/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "values": { + "type": "object", + "additionalProperties": { + "description": "JSON value; dates, datetime, decimal and bigint use strings." + }, + "description": "Only registered custom fields; builtin fields are always rejected on write and excluded on read.", + "minProperties": 1 + } + }, + "required": [ + "values" + ] + } + } + } + } + } + }, + "/table-views/{collection}": { + "get": { + "tags": [ + "table-views" + ], + "operationId": "get__table_views_collection", + "summary": "GET /table-views/:collection", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/preferences/table-view-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/preferences/table-view-routes.ts" + }, + "post": { + "tags": [ + "table-views" + ], + "operationId": "post__table_views_collection", + "summary": "POST /table-views/:collection", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/preferences/table-view-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/preferences/table-view-routes.ts" + } + }, + "/table-views/{collection}/{id}": { + "delete": { + "tags": [ + "table-views" + ], + "operationId": "delete__table_views_collection_id", + "summary": "DELETE /table-views/:collection/:id", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/preferences/table-view-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/preferences/table-view-routes.ts" + }, + "put": { + "tags": [ + "table-views" + ], + "operationId": "put__table_views_collection_id", + "summary": "PUT /table-views/:collection/:id", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/preferences/table-view-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/preferences/table-view-routes.ts" + } + }, + "/table-views/{collection}/default": { + "get": { + "tags": [ + "table-views" + ], + "operationId": "get__table_views_collection_default", + "summary": "GET /table-views/:collection/default", + "description": "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/preferences/table-view-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/preferences/table-view-routes.ts" + } + }, + "/translations": { + "get": { + "tags": [ + "translations" + ], + "operationId": "get__translations", + "summary": "GET /translations", + "description": "Активный пользователь или сервисный аккаунт; схема ограничена доступными вызывающему коллекциями и полями, статические каталоги активных плагинов, без записей и defaults. Источник: apps/core/src/translations/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "locale", + "in": "query", + "schema": { + "enum": [ + "ru", + "en" + ], + "default": "ru" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "version", + "locale", + "fallbackLocale", + "core", + "plugins", + "schema" + ], + "additionalProperties": false, + "properties": { + "version": { + "const": 1 + }, + "locale": { + "enum": [ + "ru", + "en" + ] + }, + "fallbackLocale": { + "const": "ru" + }, + "core": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "label", + "fields" + ], + "additionalProperties": false, + "properties": { + "label": { + "type": "string" + }, + "fields": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "placeholder": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "401": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "default": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/translations/routes.ts" + } + }, + "/users": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users", + "summary": "GET /users", + "description": "Человек: users/read или update ИЛИ policies/read или update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/user-routes.ts" + }, + "post": { + "tags": [ + "users" + ], + "operationId": "post__users", + "summary": "POST /users", + "description": "Человек: users/update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/user-routes.ts" + } + }, + "/users/{id}/access": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_id_access", + "summary": "GET /users/:id/access", + "description": "Человек: users/read ИЛИ update; superuser bypass. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/user-routes.ts" + } + }, + "/users/{id}/delegation": { + "put": { + "tags": [ + "users" + ], + "operationId": "put__users_id_delegation", + "summary": "PUT /users/:id/delegation", + "description": "Только человек-superuser: явный набор готовых политик, которые пользователь вправе назначать. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "policyIds": { + "type": "array", + "maxItems": 1000, + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + } + }, + "required": [ + "policyIds" + ] + } + }, + "required": [ + "data" + ] + } + } + } + }, + "default": { + "description": "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат." + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/user-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policyIds": { + "type": "array", + "maxItems": 1000, + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + } + }, + "required": [ + "policyIds" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/users/{id}/extension": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_id_extension", + "summary": "GET /users/:id/extension", + "description": "Активный пользователь с users/read и обычными read-правами коллекции. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "required": [ + "collection", + "userId", + "exists", + "data" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "exists": { + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true, + "description": "Consumer fields filtered by current row and field permissions. Null when absent or no longer readable." + }, + { + "type": "null" + } + ] + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_id_extension", + "summary": "PATCH /users/:id/extension", + "description": "Активный пользователь с users/update и обычными read/create/update-правами коллекции. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "required": [ + "collection", + "userId", + "exists", + "data" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "exists": { + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true, + "description": "Consumer fields filtered by current row and field permissions. Null when absent or no longer readable." + }, + { + "type": "null" + } + ] + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "collection", + "values" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true, + "description": "Schema-validated create/update values. Primary key and account fields cannot be supplied." + } + } + } + } + } + } + } + }, + "/users/{id}/invitation": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_id_invitation", + "summary": "POST /users/:id/invitation", + "description": "Человек: superuser ИЛИ users/update; повторное приглашение только в пределах разрешённого набора, без доступа к аккаунтам с собственным делегированием. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/user-routes.ts" + } + }, + "/users/{id}/profile": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_id_profile", + "summary": "GET /users/:id/profile", + "description": "Активный пользователь с users/read; публичные сведения профиля без credentials. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "email", + "superuser", + "hasPassword", + "displayName", + "firstName", + "lastName", + "description", + "pictureUrl", + "avatarId", + "createdAt", + "updatedAt", + "lastLoginAt", + "lastActiveAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "superuser": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + }, + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "lastActiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_id_profile", + "summary": "PATCH /users/:id/profile", + "description": "Активный пользователь с users/update; только поля профиля, без изменения статуса и привилегий. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "email", + "superuser", + "hasPassword", + "displayName", + "firstName", + "lastName", + "description", + "pictureUrl", + "avatarId", + "createdAt", + "updatedAt", + "lastLoginAt", + "lastActiveAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "superuser": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + }, + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "lastActiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "minProperties": 1, + "additionalProperties": false, + "properties": { + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "/users/{id}/profile-display": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_id_profile_display", + "summary": "GET /users/:id/profile-display", + "description": "Активный человек с users/read или superuser; настроенная проекция профиля целевого пользователя. Источник: apps/core/src/auth/profile-display-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "entries" + ], + "properties": { + "title": { + "type": "string" + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "type", + "value" + ], + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "type": { + "enum": [ + "text", + "email", + "integer", + "bigint", + "decimal", + "boolean", + "date", + "datetime", + "tags", + "user" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + } + } + } + }, + "default": { + "description": "Ошибка входа, доступа, проверки или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-display-routes.ts" + } + }, + "/users/{id}/recovery": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_id_recovery", + "summary": "POST /users/:id/recovery", + "description": "Свежий человек-superuser; только другой активный обычный пользователь. Одноразовая ссылка на 30 минут, аудит; не делегируется. Источник: apps/core/src/auth/user-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "invitationToken": { + "type": "string" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "invitationToken", + "expiresAt" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/user-routes.ts" + } + }, + "/users/me": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me", + "summary": "GET /users/me", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "email", + "superuser", + "hasPassword", + "displayName", + "firstName", + "lastName", + "description", + "pictureUrl", + "avatarId", + "createdAt", + "updatedAt", + "lastLoginAt", + "lastActiveAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "superuser": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + }, + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "lastActiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_me", + "summary": "PATCH /users/me", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "email", + "superuser", + "hasPassword", + "displayName", + "firstName", + "lastName", + "description", + "pictureUrl", + "avatarId", + "createdAt", + "updatedAt", + "lastLoginAt", + "lastActiveAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "superuser": { + "type": "boolean" + }, + "hasPassword": { + "type": "boolean" + }, + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "lastActiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "minProperties": 1, + "additionalProperties": false, + "properties": { + "displayName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "firstName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "lastName": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "pictureUrl": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048, + "format": "uri", + "description": "Public HTTPS URL, no credentials or fragment. Exported in OAuth profile claims." + }, + { + "type": "null" + } + ] + }, + "avatarId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "/users/me/avatar": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_me_avatar", + "summary": "POST /users/me/avatar", + "description": "Собственная человеческая сессия; приватное растровое изображение до 2 MiB, настроенное файловое хранилище и credential rate limit. Источник: apps/core/src/auth/profile-avatar-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "id" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "format": "uuid" + } + } + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "413": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "429": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "503": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-avatar-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary", + "description": "PNG, JPEG, GIF or WebP; maximum 2 MiB. Upload does not attach the avatar until profile save." + } + } + } + } + } + }, + "/users/me/extension": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_extension", + "summary": "GET /users/me/extension", + "description": "Собственная человеческая сессия и обычные read-права коллекции; скрытые поля и строки не раскрываются. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "required": [ + "collection", + "userId", + "exists", + "data" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "exists": { + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true, + "description": "Consumer fields filtered by current row and field permissions. Null when absent or no longer readable." + }, + { + "type": "null" + } + ] + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_me_extension", + "summary": "PATCH /users/me/extension", + "description": "Собственная человеческая сессия; read и create/update, обычные правила строк, полей, связей и файлов. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "required": [ + "collection", + "userId", + "exists", + "data" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "exists": { + "type": "boolean" + }, + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true, + "description": "Consumer fields filtered by current row and field permissions. Null when absent or no longer readable." + }, + { + "type": "null" + } + ] + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "collection", + "values" + ], + "additionalProperties": false, + "properties": { + "collection": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true, + "description": "Schema-validated create/update values. Primary key and account fields cannot be supplied." + } + } + } + } + } + } + } + }, + "/users/me/identities": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_identities", + "summary": "GET /users/me/identities", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/sso/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/sso/routes.ts" + } + }, + "/users/me/identities/{id}": { + "delete": { + "tags": [ + "users" + ], + "operationId": "delete__users_me_identities_id", + "summary": "DELETE /users/me/identities/:id", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/sso/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/sso/routes.ts" + } + }, + "/users/me/oauth-apps": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_oauth_apps", + "summary": "GET /users/me/oauth-apps", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/consent-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/consent-routes.ts" + } + }, + "/users/me/oauth-apps/{id}": { + "delete": { + "tags": [ + "users" + ], + "operationId": "delete__users_me_oauth_apps_id", + "summary": "DELETE /users/me/oauth-apps/:id", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/oauth/consent-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/oauth/consent-routes.ts" + } + }, + "/users/me/passkeys": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_passkeys", + "summary": "GET /users/me/passkeys", + "description": "Собственная активная человеческая сессия; только публичные сведения о ключах. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "createdAt", + "lastUsedAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts" + }, + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_me_passkeys", + "summary": "POST /users/me/passkeys", + "description": "Собственный session-bound challenge, свежий вход и WebAuthn verification; до десяти ключей. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "createdAt", + "lastUsedAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "WebAuthn verification failed or challenge used/expired" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "response": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "string" + }, + "type": { + "const": "public-key" + }, + "response": { + "type": "object", + "properties": { + "attestationObject": { + "type": "string" + }, + "clientDataJSON": { + "type": "string" + }, + "transports": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "attestationObject", + "clientDataJSON" + ], + "additionalProperties": false + }, + "clientExtensionResults": { + "type": "object", + "additionalProperties": true + }, + "authenticatorAttachment": { + "enum": [ + "platform", + "cross-platform" + ] + } + }, + "required": [ + "id", + "rawId", + "type", + "response", + "clientExtensionResults" + ], + "additionalProperties": false + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + } + }, + "required": [ + "challengeId", + "response" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/users/me/passkeys/{id}": { + "delete": { + "tags": [ + "users" + ], + "operationId": "delete__users_me_passkeys_id", + "summary": "DELETE /users/me/passkeys/:id", + "description": "Собственный ключ, свежий человеческий вход; нельзя удалить последний способ входа. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Собственный ключ удалён" + }, + "409": { + "description": "Последний способ входа нельзя удалить" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts" + } + }, + "/users/me/passkeys/options": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_me_passkeys_options", + "summary": "POST /users/me/passkeys/options", + "description": "Собственная активная человеческая сессия не старше пяти минут. Источник: apps/core/src/auth/passkeys/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "format": "uuid" + }, + "options": { + "type": "object", + "additionalProperties": true, + "properties": { + "challenge": { + "type": "string" + }, + "timeout": { + "type": "integer" + }, + "rp": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "displayName" + ], + "additionalProperties": false + }, + "pubKeyCredParams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "const": "public-key" + }, + "alg": { + "type": "integer" + } + }, + "required": [ + "type", + "alg" + ], + "additionalProperties": false + } + }, + "excludeCredentials": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "const": "public-key" + }, + "transports": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "type" + ], + "additionalProperties": false + } + }, + "authenticatorSelection": { + "type": "object", + "properties": { + "residentKey": { + "const": "required" + }, + "userVerification": { + "const": "required" + }, + "requireResidentKey": { + "type": "boolean" + } + }, + "required": [ + "residentKey", + "userVerification" + ], + "additionalProperties": false + } + }, + "required": [ + "challenge", + "rp", + "user", + "pubKeyCredParams" + ] + } + }, + "required": [ + "challengeId", + "options" + ], + "additionalProperties": false + } + } + } + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/passkeys/routes.ts" + } + }, + "/users/me/password": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_me_password", + "summary": "POST /users/me/password", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/profile-routes.ts" + } + }, + "/users/me/password/setup": { + "post": { + "tags": [ + "users" + ], + "operationId": "post__users_me_password_setup", + "summary": "POST /users/me/password/setup", + "description": "Собственная свежая человеческая сессия; только если пароля ещё нет. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "204": { + "description": "Первый пароль добавлен" + }, + "409": { + "description": "Пароль уже существует" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "minLength": 12, + "maxLength": 1024 + } + }, + "required": [ + "password" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/users/me/preferences": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_preferences", + "summary": "GET /users/me/preferences", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Источник: apps/core/src/preferences/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "theme", + "style", + "locale", + "timezone" + ], + "additionalProperties": false, + "properties": { + "theme": { + "anyOf": [ + { + "enum": [ + "light", + "dark", + "system" + ] + }, + { + "type": "null" + } + ] + }, + "style": { + "enum": [ + "neutral", + "ocean", + "coral" + ] + }, + "locale": { + "enum": [ + "ru", + "en" + ] + }, + "timezone": { + "anyOf": [ + { + "type": "string", + "maxLength": 100, + "description": "Valid IANA timezone; null uses the device timezone." + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "401": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "403": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "default": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/preferences/routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_me_preferences", + "summary": "PATCH /users/me/preferences", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Источник: apps/core/src/preferences/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "theme", + "style", + "locale", + "timezone" + ], + "additionalProperties": false, + "properties": { + "theme": { + "anyOf": [ + { + "enum": [ + "light", + "dark", + "system" + ] + }, + { + "type": "null" + } + ] + }, + "style": { + "enum": [ + "neutral", + "ocean", + "coral" + ] + }, + "locale": { + "enum": [ + "ru", + "en" + ] + }, + "timezone": { + "anyOf": [ + { + "type": "string", + "maxLength": 100, + "description": "Valid IANA timezone; null uses the device timezone." + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "401": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "403": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + }, + "default": { + "description": "Проверка входных данных, аутентификация или ошибка сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/preferences/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "minProperties": 1, + "additionalProperties": false, + "properties": { + "theme": { + "enum": [ + "light", + "dark", + "system" + ] + }, + "style": { + "enum": [ + "neutral", + "ocean", + "coral" + ] + }, + "locale": { + "enum": [ + "ru", + "en" + ] + }, + "timezone": { + "anyOf": [ + { + "type": "string", + "maxLength": 100, + "description": "Valid IANA timezone; null uses the device timezone." + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "/users/me/profile-display": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_profile_display", + "summary": "GET /users/me/profile-display", + "description": "Активный человек; только собственные значения заранее настроенных selfVisible путей, без права произвольного чтения коллекций. Источник: apps/core/src/auth/profile-display-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "entries" + ], + "properties": { + "title": { + "type": "string" + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "type", + "value" + ], + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "type": { + "enum": [ + "text", + "email", + "integer", + "bigint", + "decimal", + "boolean", + "date", + "datetime", + "tags", + "user" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + } + } + } + }, + "default": { + "description": "Ошибка входа, доступа, проверки или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-display-routes.ts" + } + }, + "/users/me/sessions": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_sessions", + "summary": "GET /users/me/sessions", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/profile-routes.ts" + } + }, + "/users/me/sessions/{id}": { + "delete": { + "tags": [ + "users" + ], + "operationId": "delete__users_me_sessions_id", + "summary": "DELETE /users/me/sessions/:id", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/auth/profile-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/auth/profile-routes.ts" + } + }, + "/users/me/table-preferences/{collection}": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_me_table_preferences_collection", + "summary": "GET /users/me/table-preferences/:collection", + "description": "Human caller with read access to the collection; only their own table preferences. PATCH merges supplied properties. Columns are reconciled against readable fields on GET; missing personal settings fall back to a visible default view. Resizing does not require record update permission.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Effective personal table preferences", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "collectionId", + "hasSaved", + "columns", + "pageSize", + "sort" + ], + "properties": { + "collectionId": { + "type": "string", + "format": "uuid" + }, + "hasSaved": { + "type": "boolean" + }, + "columns": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "hidden" + ], + "properties": { + "order": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "hidden": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "widths": { + "type": "object", + "maxProperties": 500, + "additionalProperties": { + "type": "integer", + "minimum": 80, + "maximum": 1200 + }, + "description": "Per-field widths in CSS pixels. Keys must be readable collection fields. Omitted keys use the default width; an empty map resets all widths." + } + } + }, + { + "type": "null" + } + ] + }, + "pageSize": { + "enum": [ + 10, + 25, + 50, + 100 + ] + }, + "sort": { + "type": "object", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string" + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid preferences or inaccessible column/width" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and collection read permission required" + }, + "404": { + "description": "Collection not found" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/preferences/routes.ts" + }, + "patch": { + "tags": [ + "users" + ], + "operationId": "patch__users_me_table_preferences_collection", + "summary": "PATCH /users/me/table-preferences/:collection", + "description": "Human caller with read access to the collection; only their own table preferences. PATCH merges supplied properties. Columns are reconciled against readable fields on GET; missing personal settings fall back to a visible default view. Resizing does not require record update permission.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "collection", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Effective personal table preferences", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "collectionId", + "hasSaved", + "columns", + "pageSize", + "sort" + ], + "properties": { + "collectionId": { + "type": "string", + "format": "uuid" + }, + "hasSaved": { + "type": "boolean" + }, + "columns": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "hidden" + ], + "properties": { + "order": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "hidden": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "widths": { + "type": "object", + "maxProperties": 500, + "additionalProperties": { + "type": "integer", + "minimum": 80, + "maximum": 1200 + }, + "description": "Per-field widths in CSS pixels. Keys must be readable collection fields. Omitted keys use the default width; an empty map resets all widths." + } + } + }, + { + "type": "null" + } + ] + }, + "pageSize": { + "enum": [ + 10, + 25, + 50, + 100 + ] + }, + "sort": { + "type": "object", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string" + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid preferences or inaccessible column/width" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "Human identity and collection read permission required" + }, + "404": { + "description": "Collection not found" + }, + "default": { + "description": "Request or server error" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/preferences/routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "columns": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "hidden" + ], + "properties": { + "order": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "hidden": { + "type": "array", + "maxItems": 500, + "items": { + "type": "string" + } + }, + "widths": { + "type": "object", + "maxProperties": 500, + "additionalProperties": { + "type": "integer", + "minimum": 80, + "maximum": 1200 + }, + "description": "Per-field widths in CSS pixels. Keys must be readable collection fields. Omitted keys use the default width; an empty map resets all widths." + } + } + }, + "pageSize": { + "enum": [ + 10, + 25, + 50, + 100 + ] + }, + "sort": { + "type": "object", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string" + }, + "direction": { + "enum": [ + "asc", + "desc" + ] + }, + "order": { + "enum": [ + "field", + "relevance" + ] + } + } + } + } + } + } + } + } + } + }, + "/users/me/workspace": { + "put": { + "tags": [ + "users" + ], + "operationId": "put__users_me_workspace", + "summary": "PUT /users/me/workspace", + "description": "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/workspaces/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/workspaces/routes.ts" + } + }, + "/users/profile-display": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_profile_display", + "summary": "GET /users/profile-display", + "description": "Только human superuser; конфигурация блока профиля. Источник: apps/core/src/auth/profile-display-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "entries" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 100 + }, + "entries": { + "type": "array", + "maxItems": 12, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "path", + "selfVisible" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "path": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + } + }, + "selfVisible": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "default": { + "description": "Ошибка входа, доступа, проверки или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-display-routes.ts" + }, + "put": { + "tags": [ + "users" + ], + "operationId": "put__users_profile_display", + "summary": "PUT /users/profile-display", + "description": "Только human superuser; явно разрешает ограниченную проекцию выбранных полей управляющим пользователями и при selfVisible владельцу профиля. Источник: apps/core/src/auth/profile-display-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "entries" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 100 + }, + "entries": { + "type": "array", + "maxItems": 12, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "path", + "selfVisible" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "path": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + } + }, + "selfVisible": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "default": { + "description": "Ошибка входа, доступа, проверки или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-display-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "entries" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 100 + }, + "entries": { + "type": "array", + "maxItems": 12, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "path", + "selfVisible" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "path": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,62}$" + } + }, + "selfVisible": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "/users/profile-display/sources": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_profile_display_sources", + "summary": "GET /users/profile-display/sources", + "description": "Только human superuser; разрешённые пользовательские поля и пути M2O, без данных записей. Источник: apps/core/src/auth/profile-display-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Префикс одиночных связей через точку; пустой — дополнительные поля пользователя." + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "label", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "type": { + "enum": [ + "text", + "email", + "integer", + "bigint", + "decimal", + "boolean", + "date", + "datetime", + "tags", + "user", + "relation" + ] + } + } + } + } + } + } + } + } + }, + "default": { + "description": "Ошибка входа, доступа, проверки или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-display-routes.ts" + } + }, + "/users/profile-extension": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_profile_extension", + "summary": "GET /users/profile-extension", + "description": "Активный пользователь с users/read; имя выбранной коллекции и ключа. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "collection", + "key" + ], + "properties": { + "collection": { + "type": "string" + }, + "key": { + "type": "string" + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts" + }, + "put": { + "tags": [ + "users" + ], + "operationId": "put__users_profile_extension", + "summary": "PUT /users/profile-extension", + "description": "Только human superuser; связывает обычную UUID-коллекцию, не выдаёт права и не удаляет данные. Источник: apps/core/src/auth/profile-extension-routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "collection", + "key" + ], + "properties": { + "collection": { + "type": "string" + }, + "key": { + "type": "string" + } + } + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "401": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "403": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "404": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "409": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + }, + "default": { + "description": "Ошибка проверки, входа, доступа, конфликта или сервера" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/profile-extension-routes.ts", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "collection" + ], + "additionalProperties": false, + "properties": { + "collection": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + }, + "/users/references": { + "get": { + "tags": [ + "users" + ], + "operationId": "get__users_references", + "summary": "GET /users/references", + "description": "Bounded system-user picker. Requires an authenticated human superuser or users section read/update permission. Returns only id and a display label (name/email); service accounts are denied. Search lists active users; explicit ids may resolve disabled users already referenced. This does not expose system tables through /items.", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "q", + "in": "query", + "schema": { + "type": "string", + "maxLength": 200 + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "ids", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Up to 100 comma-separated UUIDs; invalid IDs and unknown query parameters are rejected." + } + ], + "responses": { + "200": { + "description": "User reference page", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "data", + "labels", + "page" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + } + } + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "page": { + "type": "object", + "required": [ + "number", + "size", + "total", + "sort", + "direction" + ], + "properties": { + "number": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "total": { + "type": "string" + }, + "sort": { + "const": "label" + }, + "direction": { + "const": "asc" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid query" + }, + "401": { + "description": "Authentication required" + }, + "403": { + "description": "User directory access denied" + } + }, + "x-contract-level": "documented", + "x-source": "apps/core/src/auth/user-routes.ts" + } + }, + "/workspaces": { + "get": { + "tags": [ + "workspaces" + ], + "operationId": "get__workspaces", + "summary": "GET /workspaces", + "description": "Человек; только пространства с доступными коллекциями (superuser видит все). Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/workspaces/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/workspaces/routes.ts" + }, + "post": { + "tags": [ + "workspaces" + ], + "operationId": "post__workspaces", + "summary": "POST /workspaces", + "description": "Человек: superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/workspaces/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/workspaces/routes.ts" + } + }, + "/workspaces/{id}": { + "delete": { + "tags": [ + "workspaces" + ], + "operationId": "delete__workspaces_id", + "summary": "DELETE /workspaces/:id", + "description": "Человек: superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/workspaces/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/workspaces/routes.ts" + }, + "put": { + "tags": [ + "workspaces" + ], + "operationId": "put__workspaces_id", + "summary": "PUT /workspaces/:id", + "description": "Человек: superuser. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: apps/core/src/workspaces/routes.ts", + "security": [ + { + "bearerAuth": [] + }, + { + "browserSession": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа." + } + }, + "x-contract-level": "route-only", + "x-source": "apps/core/src/workspaces/routes.ts" + } + } + }, + "components": { + "schemas": { + "ItemCommitDraft": { + "type": "object", + "additionalProperties": false, + "description": "Одна транзакция; до 100 операций, глубина до 5. Существующая запись требует read. Каждая запись и поле проверяются по личным правам.", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "values": { + "type": "object", + "additionalProperties": true, + "default": {} + }, + "expectedValues": { + "type": "object", + "additionalProperties": true, + "description": "Исходные значения всех обновляемых полей, включая изменяемые FK. Требуют read. Несовпадение с актуальным значением возвращает ITEM_CHANGED (409), если новое значение ещё не применено. Пропуск сохраняет прежнее безусловное поведение." + }, + "references": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + }, + "records": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "collection": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "record": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + }, + "required": [ + "collection", + "record" + ] + } + }, + "relations": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ItemCommitRelation" + } + } + } + }, + "ItemCommitRelation": { + "type": "object", + "additionalProperties": false, + "properties": { + "attach": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "record": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + }, + "required": [ + "id" + ] + } + }, + "detach": { + "type": "array", + "maxItems": 100, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + }, + "create": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "record": { + "$ref": "#/components/schemas/ItemCommitDraft" + }, + "link": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + }, + "required": [ + "record" + ] + } + }, + "links": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "record": { + "$ref": "#/components/schemas/ItemCommitDraft" + } + }, + "required": [ + "id", + "record" + ] + } + } + } + } + }, + "securitySchemes": { + "browserSession": { + "type": "apiKey", + "in": "cookie", + "name": "asmblyr_session", + "description": "HttpOnly browser session; префикс меняется SESSION_COOKIE_PREFIX. Записывающие запросы требуют Origin = AUTH_UI_URL. Cookie не отменяет права конкретной операции." + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "Непрозрачный access token пользователя или сервиса. Разделы настроек доступны только человеку." + } + } + }, + "x-delegated-routes": [ + { + "method": "ALL", + "path": "/oauth/*", + "source": "apps/core/src/oauth/routes.ts", + "line": 111, + "access": "Протокольные endpoints oidc-provider: discovery/JWKS публичны, остальные используют проверки OAuth/OIDC", + "public": true + } + ] +} diff --git a/docs/reference/cli-guide.md b/docs/reference/cli-guide.md index 7da2aa2..f295b17 100644 --- a/docs/reference/cli-guide.md +++ b/docs/reference/cli-guide.md @@ -81,4 +81,4 @@ may still be private: commit generated files only when suitable for your project Use the same permission scope for generation and runtime requests. Core remains the authority on current access and row conditions. -See [SDK](./sdk-guide.md) and [package preparation](./packages.md). +See [SDK](sdk-guide.md) and [package preparation](packages.md). diff --git a/docs/reference/http.md b/docs/reference/http.md index b42daa9..871efe9 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -1,40 +1,34 @@ -# HTTP API и OpenAPI + -Публичный API: `http://localhost:3000/api`, обслуживается непосредственно Core. -Самостоятельный Core на `http://localhost:3001` поддерживает те же маршруты как -без префикса, так и с `/api`. Роут плагина: `/api//...` на общем домене. -Браузер использует HttpOnly-сессию; SDK и внешние приложения — Bearer-токены. +# HTTP API and OpenAPI -- Открыть интерактивный справочник -- [Скачать OpenAPI 3.1 JSON](/openapi.json) -- [Матрица маршрутов и доступа](./routes.md) +The public API at `http://localhost:3000/api` is served directly by Core. Standalone Core at `http://localhost:3001` supports the same routes with or without /api. Plugin routes use /api/<namespace>/… on the common domain. Browsers use HttpOnly sessions; SDK/external applications use Bearer tokens. + +- Open interactive reference +- [Download OpenAPI 3.1 JSON](/openapi.json) +- [Route/access matrix](./routes.md) - [HTTP SDK](./sdk-guide.md) -Для первого запроса и типизированного клиента начните с -[подключения SDK](./sdk-guide.md#подключение-клиента). Там же описаны -[фильтры и пагинация](./sdk-guide.md#реализованные-методы), -[сохранение связей](./sdk-guide.md#запись-и-сохранение-связей) и -[ошибки](./sdk-guide.md#ошибки-и-отмена). Поведение коллекций и пределы поиска -описаны в [данных](../features/data.md), права — в -[матрице доступа](../security/access-matrix.md). +Start with [client setup](./sdk-guide.md#connect-a-client), [filters/pagination](./sdk-guide.md#available-methods), [relationship saves](./sdk-guide.md#writes-and-relationships), and [errors](./sdk-guide.md#errors-and-cancellation). See [data](../features/data.md) for collection/search limits and [access matrix](../security/access-matrix.md) for authorization. + +The reference runs without a CDN. Request execution, external Swagger validation, and authorization persistence are disabled so browsing documentation does not mutate data. + + -Справочник работает без CDN. Выполнение запросов отключено, чтобы просмотр документации не менял данные. Внешний Swagger validator и сохранение авторизации отключены. +## Coverage -## Полнота +All static Core REST routes are listed. Key items/auth/settings operations include JSON contracts; others are explicitly x-contract-level:route-only, documenting method, path, path parameters, access, and source file without claiming full body/response schemas. Do not generate a complete SDK from this incomplete specification. -В спецификации перечислены все статические REST-маршруты Core. Для ключевых операций items/auth/settings описаны JSON-контракты; остальные помечены `x-contract-level: route-only` и содержат путь, метод, параметры пути, требования доступа и source file. Они не претендуют на полное описание тела/ответа. Генерировать полный SDK по этой спецификации пока рано. +ALL /oauth/\* is under x-delegated-routes. OIDC discovery supplies actual protocol endpoints. Dynamic plugin routes come from files/build indexes and are not automatically included in static Core OpenAPI. -`ALL /oauth/*` вынесен в `x-delegated-routes`: реальные protocol endpoints публикует discovery oidc-provider. Динамические маршруты установленных плагинов определяются их файлами и build index; в статический Core OpenAPI не добавляются автоматически. + -## Авторизация и ошибки +## Authentication and errors -Прямой API использует `Authorization: Bearer `. Основной access token непрозрачен; OAuth token внешнего приложения не подходит. Системные настройки требуют человека и соответствующий section grant. Bearer security scheme описывает передачу токена, а не достаточность прав. +Use Authorization: Bearer <accessToken>. Core access tokens are opaque; external-application OAuth tokens do not work here. Settings require a human and the relevant section grant. The Bearer security scheme describes transport, not sufficient permissions. -Обычно 400 — некорректный ввод, 401 — нет действительной учётной записи, 403 — недостаточно прав, 404 — ресурс не найден/не раскрывается, 409 — конфликт, 429 — лимит, 503 — зависимость недоступна. Не все endpoints имеют одинаковую обёртку ответа: auth token pair передаётся напрямую, items/users обычно через data. +Typical statuses: 400 invalid input, 401 unauthenticated, 403 denied, 404 missing/not disclosed, 409 conflict, 429 rate limit, and 503 dependency unavailable. Response envelopes vary: auth token pairs are direct; items/users usually use data. -Ошибки Core имеют единый JSON-формат `{ code, message, details, requestId }`. -`details` сейчас пустой объект; закрытые значения записи и внутренние ошибки в -него не попадают. Сохраняйте `requestId` при диагностике. Прокси UI может -сформировать собственную ошибку, если Core вообще недоступен. +Core errors use { code, message, details, requestId }. Details is currently empty and excludes protected values/internal errors. Preserve requestId for diagnostics. Infrastructure may return a different error if Core is unreachable. -Схема данных коллекции динамическая. Даже успешный create/update может вернуть `data: null` без read grant. Типы больших ID/total остаются строками. Подробный клиентский контракт — в SDK. +Collection schemas are dynamic. Successful create/update may return data:null without read grants. Large IDs/totals remain strings. SDK documentation describes the client contract. diff --git a/docs/reference/kit-capabilities.md b/docs/reference/kit-capabilities.md index bdf9d9d..00735c6 100644 --- a/docs/reference/kit-capabilities.md +++ b/docs/reference/kit-capabilities.md @@ -1,8 +1,10 @@ -# Возможности плагина + -Пакет перечисляет используемые возможности в `package.json`: +# Plugin capabilities + +Declare capabilities in the package's package.json: ```json { @@ -24,7 +26,7 @@ } ``` -Владелец проекта разрешает их по **имени npm-пакета** в корневом `package.json`: +The project owner approves them by npm package name in the root package.json: ```json { @@ -45,50 +47,31 @@ } ``` -Core сверяет все включённые пакеты до импорта их серверных модулей. -Неизвестные, повторяющиеся или неразрешённые возможности останавливают запуск. -Отсутствующий список означает отсутствие дополнительных возможностей. -Разрешение, которого нет в декларации самого пакета, не выдаётся. -При обновлении пакета новые возможности требуют изменения конфигурации проекта; -изменения деклараций и разрешений применяются после перезапуска Core. +Core checks all enabled packages before importing server modules. Unknown, duplicate, or unapproved capabilities stop startup. A missing list grants no additional capabilities. Project approval cannot grant something absent from the package declaration. New capabilities after an upgrade require project configuration changes; restart Core to apply declarations/approvals. + +| Capability | Access | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| identity.profile | Current human/service actor.displayName; no email, tokens, or other profiles | +| items.read | Items.list/get within caller permissions | +| items.write | Create/update/delete within caller permissions; without items.read, writes return data:null; commit requires both | +| collections.manage | Install/migrate own declared collections; no arbitrary SQL or foreign tables | +| storage.own | Own collections through storage; plugin enforces domain conditions | +| hooks.items | Items.create/update/delete hooks | +| hooks.collections | Collections.delete hooks | +| settings | Declare/read own package settings | +| notifications | Current human's record subscriptions and own-namespace inbox publish/update/remove; Core selects recipients/checks access; unavailable in model handlers | +| connections.google | Restricted personal Google broker for eligible model handlers; no credentials or automatic write confirmation | -| Возможность | Что разрешено | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identity.profile` | `actor.displayName` текущего пользователя или сервиса. Email, токены и чужие профили не выдаются. | -| `items.read` | `items.list/get` в пределах прав вызывающего пользователя. | -| `items.write` | `items.create/update/delete` в пределах прав пользователя. Без `items.read` ответ записи содержит `data: null`. `commit` требует обе возможности. | -| `collections.manage` | Установка деклараций и выполнение миграций собственных коллекций. Нет произвольного SQL или изменения чужих таблиц. | -| `storage.own` | Доступ к собственным коллекциям через `storage`. Предметные условия проверяет плагин. | -| `hooks.items` | Обработчики `items.create/update/delete`. | -| `hooks.collections` | Обработчик `collections.delete`. | -| `settings` | Декларация настроек и получение значений своего пакета. | -| `notifications` | Подписка текущего человека на запись; публикация, обновление и удаление inbox-событий своего namespace. Получателей и доступ проверяет Core. Model handlers этот контекст не получают. | +For discussions, declare and approve notifications together. See [notifications](https://github.com/Asmblyr/Collaborative/blob/main/docs/features/notifications.md). Await context.notifications.publish({ collection, item, eventId, panelId, targetId, preview }) inside context.withRecord beside the storage write. EventId deduplicates within retained inbox data. Preview is plain text up to 300 characters; arbitrary recipients/URLs are not accepted. -Для обсуждений добавьте `notifications` одновременно в manifest комментариев и -разрешения проекта. См. [обсуждения и уведомления](https://github.com/Asmblyr/Collaborative/blob/main/docs/features/notifications.md). -`context.notifications.publish({ collection, item, eventId, panelId, targetId, preview })` -вызывается внутри `context.withRecord`, рядом с записью события в storage, и должна -быть awaited. `eventId` устраняет повторную доставку в пределах сохранённого inbox; -`preview` — обычный текст до 300 символов. Произвольных получателей или URL нет. +Basic actor.id/kind are available to authenticated endpoints without extra capabilities for authorship. Capability checks also apply to superusers. Items.write does not grant the caller write permissions. HTTP/MCP share restrictions; MCP additionally checks publication/readOnly. Model handlers never receive privileged storage. -Базовые `actor.id` и `actor.kind` доступны аутентифицированному endpoint без -дополнительных возможностей: они нужны для авторства операций. -Проверка возможностей действует и для superuser. Возможность `items.write` -не добавляет пользователю права записи. HTTP и MCP используют те же ограничения; -MCP дополнительно учитывает публикацию коллекций и `readOnly` действия. -Привилегированный `storage` model handlers не получают. +Administrators view enabled packages and capabilities under Settings → Plugins. Approval remains project configuration, not an editable UI switch. -Администратор видит включённые пакеты и разрешения в **Система → Плагины**. -Эта страница не изменяет разрешения: они остаются частью конфигурации проекта. + -## Граница контроля +## Control boundary -Это контракт доверенных пакетов, исполняемых в процессе Core. Он контролирует -выданные Core возможности: Kit, установку схемы, hooks и настройки. Произвольный -Node-код пакета технически может импортировать сетевые и файловые библиотеки; -декларация не является песочницей. UI пакета также исполняется в контексте админки. -Устанавливайте только доверенные пакеты. Изолированное выполнение — отдельный этап. +Trusted packages run inside Core. Capabilities control services supplied by Core: Kit, schema installation, hooks, and settings. Arbitrary Node code can still import networking/filesystem libraries. This is not a sandbox. Plugin UI also runs in the admin context. Install trusted packages; isolated execution is not provided. -Секреты не хранятся в обычных настройках плагина. Для них нужен отдельный контракт -хранения и выдачи. Объекты запроса, `items`, `storage` и контекст hook нельзя -сохранять в глобальных переменных или использовать в фоновых задачах. +Ordinary plugin settings are not secret storage. Secrets need a separate storage/access contract. Never retain request, items, storage, or hook contexts globally or use them in background tasks. diff --git a/docs/reference/kit-fields.md b/docs/reference/kit-fields.md index e291c1e..f5cecd2 100644 --- a/docs/reference/kit-fields.md +++ b/docs/reference/kit-fields.md @@ -1,17 +1,17 @@ -# Редакторы полей из плагинов + -Первый этап поддерживает обычные поля хранения `text`. Плагин предоставляет -ввод, необязательное отображение значения и настройки каждого поля. Его -регистрация использует существующий браузерный export `./ui`: +# Plugin field editors + +The initial contract supports ordinary text storage fields. A plugin supplies an editor, optional value display, and per-field settings through its existing ./ui browser export. ```ts export default defineUiPlugin({ fieldInterfaces: [ { id: "picker", - title: "Цвет", + title: "Color", types: ["text"], editor: ColorEditor, display: ColorDisplay, @@ -21,44 +21,31 @@ export default defineUiPlugin({ }); ``` -Рабочий пример: [examples/plugins/color](./plugin-color.md). Серверный -`plugin.ts` остаётся `definePlugin({})`; таблицы и HTTP endpoints этому плагину -не нужны. Включение пакета, сборка, source/built discovery и общие компоненты -работают так же, как у страниц и вкладок. +See the working [color example](plugin-color.md). Server plugin.ts remains definePlugin({}); no tables or endpoints are needed. Enablement, build, source/built discovery, and shared components follow page/panel conventions. + + -## Контракт компонентов +## Component contract -Типы доступны из `@asmblyr-collaborative/kit/ui`: +Types from @asmblyr-collaborative/kit/ui: -- `FieldEditorProps`: `id`, `label`, `value`, `options`, `disabled`, `required`, - `placeholder`, `describedBy`, `onChange(value)`. -- `FieldDisplayProps`: `value: string | null`, `options`. -- `FieldInterfaceSettingsProps`: `options`, `disabled`, `onChange(options)`. +- FieldEditorProps: id, label, value, options, disabled, required, placeholder, describedBy, onChange(value). +- FieldDisplayProps: value:string|null, options. +- FieldInterfaceSettingsProps: options, disabled, onChange(options). -`options` — JSON-объект. Плагин должен читать пустые, старые и неизвестные -настройки с подходящими значениями по умолчанию. Компонент настроек сохраняет -изменения через callback; настройки принадлежат конкретному полю. +Options is a JSON object. Handle empty, older, and unknown options with suitable defaults. Settings changes use the callback and belong to that field. -Редактор получает строковый черновик, пустая строка означает отсутствие ввода. -Преобразование в `null`, пропуск default при создании, проверка обязательности -и отправка в API принадлежат форме Core. Нельзя сохранять запись из редактора -или подменять callback собственным хранилищем. Плагин вызывает `onChange` только -в ответ на действие пользователя, учитывает `disabled` и не меняет значение -при монтировании. Права на чтение и изменение полей проверяются хостом и API. +Editors receive string drafts; empty string means no input. Core's form owns null conversion, omission for create defaults, required validation, and API submission. Do not save records from editors or replace callbacks with separate storage. Call onChange only after user action, respect disabled, and never mutate values on mount. Host/API enforce field reads/writes. -Для проверок ввода используйте обычные `required`, `pattern`, `setCustomValidity` -на Input/Textarea. Форма проверяет их перед сохранением изменённых полей и -показывает ошибку у нужного поля. Серверные ограничения остаются обязательными: -UI-проверки не являются ограничением API или способом авторизации. +Use native required, pattern, and setCustomValidity on Input/Textarea. The form checks changed fields before save and shows errors at the field. Server validation remains essential; UI constraints are not API authorization. -Компоненты shadcn импортируются из `@asmblyr-collaborative/kit/ui/`. Хост передаёт -контейнер нативного диалога через `PortalContainerContext`: вложенные Select -остаются внутри окна. Компоненты должны поддерживать серверный рендер React; -доступ к DOM допустим в обработчиках и effects, а не при импорте/рендере. +Import shadcn components from @asmblyr-collaborative/kit/ui/<component>. PortalContainerContext keeps nested Select portals inside the native dialog. Components must support React SSR: access DOM in handlers/effects, not imports or render. -## Хранение и отключение + -В существующем JSONB presentation сохраняется: +## Persistence and disabling + +Existing presentation JSONB stores: ```json { @@ -70,27 +57,17 @@ UI-проверки не являются ограничением API или с } ``` -ID формируется как `:`. Core проверяет формат, совместимый -тип и ограниченный JSON: до 8 КиБ, 8 уровней, 1000 узлов. URL/импорты из -метаданных не исполняются. Настройки видны читателям поля и не подходят для -секретов. Новая миграция базы не требуется. - -Настройки сохраняются и для отсутствующего пакета. UI использует только -собранные расширения, включённые в Core. Если расширение отсутствует, ввод -возвращается к стандартному полю с уведомлением, отображение — к обычному -тексту. Ошибка клиентского рендера компонента также включает fallback. Черновик -остаётся в хосте. Повторное включение пакета возвращает оформление. - -Смена редактора в настройках — явное действие: она удаляет ссылку на предыдущий -редактор и его options, не меняя записанные значения. Настройка плагина имеет -приоритет над встроенным display; при её выборе UI сбрасывает старый display. - -## Границы этапа - -- Один редактор может включать свой display; независимый выбор display плагина - пока не реализован. -- Расширение не определяет SQL-тип, серверную валидацию или отдельные права. -- Цвет поддерживает HEX `#RRGGBB`. `allowCustom` управляет выбором в UI; - через API поле остаётся строкой с обычными ограничениями Core. -- События, внешние справочники, редакторы связей и асинхронные операции - редактора относятся к следующим этапам. +IDs use namespace:local-id. Core validates format, compatible type, and bounded JSON: 8 KiB, depth 8, 1000 nodes. Metadata URLs/imports are never executed. Field readers see options, so they must contain no secrets. No new database migration is required. + +Missing packages retain settings. UI loads only built extensions enabled in Core. If absent, editors fall back to the standard input with a notice, and displays to plain text. Client render errors also trigger fallback. The host retains the draft. Re-enabling restores presentation. + +Changing editor explicitly removes the prior extension/options without changing stored values. Plugin presentation takes precedence over built-in display; selecting it clears the old display. + + + +## Limits + +- An editor may include its own display; independent plugin-display selection is unsupported. +- Extensions define no SQL type, server validator, or separate permissions. +- Color supports HEX #RRGGBB. AllowCustom controls UI choices; API values remain strings under ordinary Core rules. +- Events, external catalogs, relationship editors, and asynchronous editor operations are not part of this contract. diff --git a/docs/reference/kit-guide.md b/docs/reference/kit-guide.md index 1bad55c..cd77c74 100644 --- a/docs/reference/kit-guide.md +++ b/docs/reference/kit-guide.md @@ -2,19 +2,15 @@ # @asmblyr-collaborative/kit -Типизированный API для описания плагинов Asmblyr. +Typed API for Collaborative plugins. -[Возможности и разрешения](./kit-capabilities.md) · [Hooks и настройки](./kit-hooks.md) +[Capabilities and permissions](kit-capabilities.md) · [Hooks and settings](kit-hooks.md) -Плагины также могут предоставлять [редакторы текстовых полей](./kit-fields.md) -с настройками и отображением в таблице. Пример — `examples/plugins/color`. +Plugins can also supply [text-field editors](kit-fields.md), field settings, and table displays. See examples/plugins/color. -`@asmblyr-collaborative/kit` содержит серверный контекст, H3-обработчики и сборщик плагинов. -HTTP-клиент для браузера и Node находится в отдельном [@asmblyr-collaborative/sdk](./sdk-guide.md). -Общие типы запросов и ответов принадлежат `@asmblyr-collaborative/contracts`; -kit переэкспортирует типы, необходимые авторам обработчиков. +Kit contains server context, H3 handlers, and the plugin builder. The browser/Node HTTP client is the separate [SDK](sdk-guide.md). Shared request/response types belong to @asmblyr-collaborative/contracts; Kit re-exports those needed by handler authors. -Обязательный `plugin.ts` в корне пакета пока остаётся пустым описанием: +The required root plugin.ts currently stays minimal: ```ts import { definePlugin } from "@asmblyr-collaborative/kit"; @@ -22,17 +18,13 @@ import { definePlugin } from "@asmblyr-collaborative/kit"; export default definePlugin({}); ``` -Маршруты обнаруживаются по файлам в `server/api`. Перечислять их в `plugin.ts` -не нужно. Дополнительные возможности этого файла определим отдельно. +Routes are discovered under server/api and are not listed manually in plugin.ts. The same principle applies to the assistant/MCP: defineModelContext<Input> connects an existing H3 handler to defineModelAnnotation. The builder derives schemas from TypeScript. See [plugin actions](../development/plugin-actions.md). -Тот же принцип действует для ассистента и MCP: `defineModelContext` связывает -существующий h3-обработчик с `defineModelAnnotation`. Сборщик выводит схемы из типов -TypeScript; описание хранится рядом с обработчиком. Контракт, пример и правила проверки — -в [действиях плагинов](../development/plugin-actions.md). + -## Обработчик +## Handlers -Файл `server/api/comments/status.get.ts`: +server/api/comments/status.get.ts: ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -44,40 +36,21 @@ export default defineHandler((event) => { }); ``` -- `defineHandler` — реэкспорт настоящего обработчика H3 **2.0.1-rc.32**. На входе - `H3Event`; HTTP-утилиты можно импортировать из `h3`, добавив ту же закреплённую - версию в зависимости своего пакета. H3 v1 с этим контрактом несовместим. -- `useAsmblyr(event)` возвращает проверенный контекст: `actor` (`id`, `kind`, - необязательный `displayName`), `requestId`, `logger`, `items` и разрешённые возможности пакета. - `storage` требует namespace и `storage.own`; `displayName` — `identity.profile`. Вызов без контекста Core завершается ошибкой. - Возможность `notifications` добавляет подписки и публикацию событий своего - namespace в личные входящие. Core выбирает получателей и проверяет права на запись. - См. [контракт возможностей](./kit-capabilities.md). -- Core проверяет access token пользователя или сервисного аккаунта до чтения тела - и запуска обработчика. Анонимных endpoints нет. `Authorization` в `event.req` - не передаётся; Core также удаляет cookies сессии и провайдеров из запроса к плагину. -- Параметры и query читаются функциями H3: `getRouterParam`, `getQuery`, - `getValidatedQuery`. Для декодирования параметра используйте `{ decode: true }` - один раз. Тело читается через `event.req.json()`, `.text()`, `.formData()` или - `readValidatedBody`. Generic-параметр сам по себе не проверяет входные данные. -- Можно возвращать объект, строку, бинарные данные, `Response`, поток или Promise. - Статус и заголовки задаются через `event.res` либо в `Response`. - Строка теперь отправляется как текст; для JSON-строки нужен `Response.json(value)`. - `null`/`undefined` означают пустое тело. Объекты остаются JSON. -- `HTTPError` из H3 поддерживается. Сохранён и `EndpointError` для ошибок с кодом - Asmblyr. Ошибки имеют общий формат `{ code, message, requestId }`; подробности - серверных ошибок остаются в журнале. Клиентские HTTPError сохраняют свои headers. -- Вход не означает доступ к данным коллекций. `items` проверяет право нужного - действия на коллекцию и доступ к её полям через общую с `/items` логику Core. -- `useItems(event)` — общий доступ к этому сервису в обычном endpoint и в model - handler. В model handler `useActionContext(event)` также даёт actor, superuser и - signal; полного контекста `useAsmblyr` и привилегированного storage там нет. - `AccessGate.authenticated` разрешает вход, а права данных проверяет каждый метод - items. У MCP дополнительно учитывается публикация всех затронутых коллекций. - -Это API доверенных серверных пакетов, выполняемых в процессе Core, без песочницы. - -## Чтение данных +- `defineHandler` re-exports H3 2.0.1-rc.32 and receives H3Event. Import HTTP utilities from h3 with the same pinned dependency. H3 v1 is incompatible. +- `useAsmblyr`(event) returns validated actor (id, kind, optional displayName), requestId, logger, items, and approved capabilities. Storage needs namespace/storage.own; displayName needs identity.profile. Calls outside Core context fail. +- Notifications capability adds subscriptions and inbox publication within the plugin namespace. Core selects recipients and checks record access. See [capabilities](kit-capabilities.md). +- Core authenticates humans/services before reading the body or invoking handlers. There are no anonymous endpoints. Authorization and session/provider cookies are removed from plugin requests. +- Read parameters/query with H3 getRouterParam, getQuery, or getValidatedQuery. Decode a parameter once with decode:true. Read bodies with event.req.json/text/formData or readValidatedBody. Generics do not validate input. +- Return objects, strings, bytes, Response, streams, or Promises. Set status/headers through event.res or Response. Strings are text; JSON strings require Response.json(value). Null/undefined mean an empty body; objects remain JSON. +- H3 HTTPError and Collaborative EndpointError are supported. Errors share code/message/requestId; server details stay in logs. Client HTTPErrors retain headers. +- Authentication grants no collection access. Items checks actions/fields through Core's shared /items logic. +- UseItems(event) works in ordinary endpoints and model handlers. `useActionContext` adds actor, superuser, and signal, but excludes full useAsmblyr and privileged storage. AccessGate authenticates; each items method authorizes data. MCP additionally checks publication of all affected collections. + +These are trusted in-process server packages, not sandboxed code. + + + +## Reading data ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -94,107 +67,66 @@ export default defineHandler(async (event) => { }); ``` -Одна запись: `await items.get("articles", 4, { fields: ["title"] })`. -`items` из kit вызывает общий сервис Core внутри процесса, без дополнительного HTTP-запроса. - -| Метод | Результат | -| ------------------------------------- | ------------------------------------------------------------------ | -| `items.list(collection, options?)` | `{ data, labels, page: { number, size, total, sort, direction } }` | -| `items.get(collection, id, options?)` | `{ data, label }`; отсутствующая запись — ошибка 404 | - -Контракты `ItemsReader`, `ItemListOptions`, `ItemReadOptions`, `ItemListResult`, -`ItemResult`, `ItemRecord` и типы фильтров экспортируются из `@asmblyr-collaborative/kit`. - -- `collection` и `fields` используют технические имена. Выбор полей относится - к физическим столбцам, включая внешние ключи и системные даты. Разворачивания - связанных записей и выбора виртуальных полей через `fields` пока нет. -- Без `fields` возвращаются все доступные поля. `fields: []` возвращает только - первичный ключ; ключ всегда включён. `*` указывать не нужно. Недоступное поле — - ошибка 403, неизвестное — 400. Проекция выполняется в SQL. -- Сортировка, поиск и фильтры проверяются по полному набору разрешённых полей, - независимо от `fields`. Например, можно вернуть только `id`, сортируя по - доступному `title`. `labels`/`label` тоже могут использовать доступные поля вне - проекции, но не закрытые поля своей или связанной коллекции. -- `page` начинается с 1, `limit` — от 1 до 100 (по умолчанию 100), `total` — строка. - Это те же ограничения, что у HTTP API. Без `sort` используется первичный ключ. -- `id` — строка или безопасное целое JS. Большие `bigserial` передавайте строками; - в результате они также остаются строками. Поля имеют тип `unknown`, поскольку - схемы динамические. Календарный `date` всегда остаётся строкой `YYYY-MM-DD`, - обычный `bigint` — точной десятичной строкой. Внутри обработчика `datetime` может быть `Date`; - при JSON-ответе они сериализуются в ISO-строки, как в `/items`. -- Поддерживаются `q` и объект `filter` с теми же операторами и проверками, что у - HTTP API. Значения условий — строки или массивы строк, включая числа и boolean. - Фильтры по связям ограничены одним переходом и требуют прав на обе стороны. - Действуют общие лимиты: 8192 символа JSON, 3 уровня групп, 20 условий, 30 узлов. -- Права загружаются один раз при входе в endpoint. Изменение политик и отзыв - сессии/сервисного ключа учитываются в следующем запросе. Не сохраняйте `items` - в глобальной переменной и не передавайте в фоновые задачи: это контекст запроса. -- Переопределение actor, `superuser` или прав не предусмотрено. Неизвестные опции - чтения отклоняются. В kit нет доступа к Knex, токенам или системным - таблицам через `items`. Код плагина остаётся доверенным кодом процесса. -- Условия permissions проверяются Core: до count/пагинации, отдельно от фильтра - вызывающей стороны. Поля проецируются по сработавшим веткам; изменения проверяют - исходную и итоговую запись. См. [права и границы первой версии](../../docs/features/access.md). - Пользовательский `filter` сужает выдачу и не заменяет такую политику. - Статусы записей автоматически не отфильтровываются: как в `/items`, нужное - условие передаёт вызывающая сторона. - -Пример фильтра по доступному полю связанной коллекции: +Read one record with items.get("articles", 4, { fields: ["title"] }). Kit calls Core's in-process service without extra HTTP. + +| Method | Result | +| ----------------------------------- | ----------------------------------------------------------------------- | +| items.list(collection, options?) | { data, labels, page: { number, size, total, sort, direction, order } } | +| items.get(collection, id, options?) | { data, label }; missing records return 404 | + +Kit exports ItemsReader, ItemListOptions, ItemReadOptions, ItemListResult, ItemResult, ItemRecord, and filter types. + +- Collection/fields use technical names. Selection covers physical columns, foreign keys, and timestamps, without relation expansion or virtual fields. +- Omitted fields returns all permitted fields. An empty list returns only the primary key, which is always included. No "\*" is needed. Closed fields yield 403; unknown fields 400. SQL performs projection. +- Sorting/search/filtering use all permitted fields independently of projection. You can return id while sorting by accessible title. Labels may use permitted fields outside selection, never closed fields in source/related collections. +- Page starts at 1; limit is 1–100, default 100; total is a string. Without search or explicit sorting, the primary key orders results. Search relevance follows Core's order option. +- IDs are strings or safe JS integers. Large bigserial IDs remain strings. Dynamic fields have unknown type. Date stays YYYY-MM-DD, bigint stays an exact decimal string. Datetime may be Date inside handlers, becoming ISO on JSON serialization. +- Q/filter use HTTP operators and checks. Values are strings/string arrays, including numbers/booleans. Related filters allow one hop with access to both sides. Limits: 8192 JSON characters, 3 group levels, 20 conditions, 30 nodes. +- Grants load at endpoint entry. Policy/session/key revocation applies on the next request. Never store items globally or use it in background tasks. +- Actors, superuser, and grants cannot be overridden. Unknown read options are rejected. Items exposes no Knex, tokens, or system tables; plugin code remains trusted. +- Core applies row permissions before count/pagination, separately from caller filters. Matching rules control projection and original/final write checks. See [access boundaries](../features/access.md). Record statuses are not implicitly filtered: callers supply the condition as with /items. + +Example with a readable related field: ```ts const result = await items.list("articles", { fields: ["title"], filter: { logic: "and", - children: [{ field: "author_id.name", op: "eq", value: "Анна" }], + children: [{ field: "author_id.name", op: "eq", value: "Anna" }], }, }); ``` -Ошибки чтения проходят через общую обработку Core: 400 — некорректный запрос, -403 — нет разрешения, 404 — коллекция или запись не найдена. До запуска endpoint -невалидный access token даёт 401. Ошибка не превращается в пустую выдачу. +Errors remain errors: 400 invalid request, 403 denied, 404 missing collection/record, and 401 invalid token before invocation. They never become empty results. -HTTP API использует ту же проекцию: `GET /items/articles?fields=id,title` и -`GET /items/articles/4?fields=title`. Без параметра формат ответа сохранён. +HTTP uses the same projection: GET /items/articles?fields=id,title and GET /items/articles/4?fields=title. Omitting fields preserves its response format. -## Запись данных + + +## Writing data ```ts const { items } = useAsmblyr(event); -const result = await items.create("articles", { title: "Статья" }); -await items.update("articles", 4, { title: "Новое название" }); +const result = await items.create("articles", { title: "Article" }); +await items.update("articles", 4, { title: "New title" }); await items.delete("articles", 4); ``` -Контекст предоставляет `ItemsService`, включающий чтение и запись. `ItemsReader` -остаётся отдельным типом для функций, которым требуется только чтение. -Входные значения — JSON-объекты; правила полей и отношений проверяет Core. - -- `create`/`update` возвращают `{ data }` только с читаемыми полями. Если права - чтения нет, успешный результат — `{ data: null }`. `delete` возвращает `void`. -- Core использует общую с HTTP API логику в `items/writer.ts`: права, проверку - связанной коллекции, валидацию, запись и проекцию результата. -- Каждая операция и её история выполняются в транзакции. Автор истории берётся - из текущей сессии пользователя или сервисного аккаунта. ID операции истории - генерирует Core; это UUID, отдельный от HTTP `requestId`. -- Несколько вызовов не образуют одну транзакцию. Для записи со связями есть - `items.commit(collection, draft)`, использующий существующий механизм черновиков. - Возвращает `{ data: { id } }`. Сбой откатывает весь черновик, включая историю. -- `ItemCommitDraft` содержит values/references/relations/records, до 100 изменений - и глубины 5. Для изменения существующей записи через commit нужно и чтение. - `expectedValues` в каждой изменяемой записи включает исходные значения всех - обновляемых полей, включая FK. Core проверяет их под блокировкой строки; - несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. - Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. - Полный формат общий с [HTTP SDK](./sdk-guide.md#запись-и-сохранение-связей). -- Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, - 409 — конфликт связи/ключа или вторая запись в single-коллекции. - -Собственные коллекции плагина устанавливаются Core при первом запуске. -Формат описания и ограничения обновления приведены ниже. - -### Пример POST с проверкой тела +ItemsService includes reads/writes; ItemsReader remains a separate type for read-only consumers. Inputs are JSON objects validated by Core's field/relation rules. + +- Create/update return readable data, or data:null on successful write without read access. Delete returns void. +- Shared items/writer.ts performs authorization, related-collection checks, validation, storage, and projection. +- Each operation/history is transactional. The caller supplies authorship; Core generates a separate UUID history-operation ID, distinct from HTTP requestId. +- Multiple calls do not share a transaction. Items.commit(collection, draft) atomically saves linked drafts, returning the root ID and rolling back all changes/history on failure. +- ItemCommitDraft contains values/references/relations/records, capped at 100 changes/depth 5. Existing records in commits also require reads. ExpectedValues on each modified record includes baselines for updated fields/FKs; Core checks under row locks. Mismatch returns ITEM_CHANGED (409) and rolls back everything. Compared fields need read/update; omission preserves legacy behavior. See [SDK writes](sdk-guide.md#writes-and-relationships). +- Errors: 400 invalid values/fields, 403 access, 404 missing record, 409 relationship/key conflict or a second singleton record. + +Core installs plugin-owned collections on first startup; declaration/update rules follow below. + + + +### POST body validation ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -210,58 +142,42 @@ export default defineHandler(async (event) => { }); ``` -### Граница HTTP + + +### HTTP boundary -Fastify продолжает маршрутизировать запросы. Изолированный parser читает исходные -байты тела с лимитом **1 МиБ**; H3 разбирает их один раз. Multipart поддерживается -в этом лимите, потоковая загрузка больших файлов пока не реализована. -Ответы идут потоком напрямую через Fastify без преобразования в JSON и без полной -буферизации. Отключение клиента передаётся в `event.req.signal`. +Fastify routes requests. An isolated parser reads raw bodies up to 1 MiB; H3 parses once. Multipart fits within that limit; large streaming uploads are unsupported. Responses stream through Fastify without JSON conversion/full buffering. Disconnect reaches event.req.signal. -Core сохраняет content-type, статусы, cookies плагина и end-to-end headers. -Имена cookies с префиксом `asmblyr_` и настроенным `SESSION_COOKIE_PREFIX` -зарезервированы; плагин не читает и не заменяет cookies сессии или провайдеров. -Редирект передаётся браузеру без серверного перехода с токеном Core. -В `Location` следует использовать публичный адрес, например `/api/comments/status`. -Промежуточного UI-прокси и его таймаута нет; клиент управляет отменой запроса. -Все ответы плагинов имеют `Cache-Control: no-store`. +Core preserves content type, status, plugin cookies, and end-to-end headers. Cookie names prefixed asmblyr\_ or SESSION_COOKIE_PREFIX are reserved; plugins cannot read/replace session/provider cookies. Redirects go to the browser without a server-side follow carrying Core credentials. Use public Location values such as /api/comments/status. -Это интеграция обработчиков H3, а не полноценный Nitro-сервер. `event.app`, -сырой Node req/res, WebSocket upgrades и фоновый lifecycle пока не предоставляются. -`plugin.ts` по-прежнему описывает пакет Asmblyr; `definePlugin` из `h3` — другое API. +There is no intermediate UI proxy timeout; clients control cancellation. All plugin responses use Cache-Control:no-store. -## Адреса +This integrates H3 handlers, not a full Nitro server. Event.app, raw Node req/res, WebSocket upgrades, and background lifecycle are unavailable. H3's definePlugin is a different API from Collaborative's package declaration. -Путь определяется относительно `server/api`, метод — суффиксом файла: + -| Файл внутри `server/api` | Публичный маршрут на адресе админки | -| ------------------------------------ | ----------------------------------- | -| `comments/index.get.ts` | `GET /api/comments` | -| `comments/index.post.ts` | `POST /api/comments` | -| `comments/[id].delete.ts` | `DELETE /api/comments/:id` | -| `comments/[id]/replies/index.get.ts` | `GET /api/comments/:id/replies` | +## Addresses -Core использует те же пути без общего префикса `/api`. Имя npm-пакета в URL не -подставляется. Поддерживаются `.get.ts`, `.post.ts`, `.put.ts`, `.patch.ts` и -`.delete.ts`. Файл без суффикса метода, например `comments/status.ts`, обслуживает -все пять методов. Каждый файл должен экспортировать обработчик по умолчанию. +Paths are relative to server/api; suffixes determine methods. -Вспомогательные функции размещаются вне `server/api`. Файлы `.d.ts` не считаются -маршрутами. Необязательные параметры, catch-all, составные параметры в сегменте -и отдельные HEAD/OPTIONS handlers пока не поддерживаются. +| File under server/api | Public admin-domain route | +| ---------------------------------- | ----------------------------- | +| comments/index.get.ts | GET /api/comments | +| comments/index.post.ts | POST /api/comments | +| comments/[id].delete.ts | DELETE /api/comments/:id | +| comments/[id]/replies/index.get.ts | GET /api/comments/:id/replies | -Первый сегмент пути принадлежит плагину. Core отклоняет пересечения с собственными -разделами, другими плагинами и дубли маршрутов. Сборка также отклоняет неоднозначные -файлы, например `[id].get.ts` и `[key].get.ts` в одном каталоге. -UI автоматически проксирует пути плагинов, используя -свою сессию и существующую проверку Origin для изменений. +Core also uses these paths without /api. Npm package names are not inserted in URLs. Supported suffixes: get/post/put/patch/delete.ts. A file without a method suffix serves all five. Every route default-exports a handler. -## Коллекции плагина +Keep helpers outside server/api. Declaration files are not routes. Optional parameters, catch-all, compound path segments, and separate HEAD/OPTIONS handlers are unsupported. -В `server/collections/entries.ts` плагина comments есть пример с -`defineCollection()` из kit. Core проверяет описание и при первом запуске создаёт -таблицу вместе с метаданными. Файлы лежат непосредственно в `server/collections`, -имя файла совпадает с локальным именем коллекции. Подкаталоги и симлинки запрещены. +The first path segment belongs to the plugin. Core rejects collisions with built-in sections, other plugins, and duplicate routes. Builds reject ambiguous files such as [id].get.ts and [key].get.ts in one directory. Browser calls use the public API with the current session and the existing mutation Origin checks. + + + +## Plugin collections + +Comments' server/collections/entries.ts demonstrates defineCollection. Core validates declarations and creates tables/metadata on initial startup. Files sit directly under server/collections and match local collection names. Subdirectories/symlinks are forbidden. ```ts import { defineCollection } from "@asmblyr-collaborative/kit"; @@ -270,82 +186,51 @@ export default defineCollection({ name: "entries", primaryKey: { name: "id", type: "uuid" }, timestamps: { createdAt: true, updatedAt: true }, - presentation: { displayName: "Комментарии", hidden: true }, + presentation: { displayName: "Comments", hidden: true }, fields: { body: { type: "text", required: true, nullable: false, - presentation: { label: "Комментарий", interface: "textarea" }, + presentation: { label: "Comment", interface: "textarea" }, }, }, }); ``` -- `name` — локальное имя внутри плагина. Namespace задаётся в манифесте, - а Core формирует имя таблицы: `plugin__`, например `plugin_comments_entries`. -- `primaryKey` и `timestamps` используют общие с Core типы из contracts. - Ключ и системные даты не нужно повторять в `fields`. -- Ключи объекта `fields` — технические имена полей. Порядок объявления задаёт - порядок создания столбцов. Метаданные полей сохраняются при установке. -- `required` — обязательность непустого значения в API; `nullable` — допустимость - SQL NULL в БД. Оба параметра явные и независимые: `true` / `true` допустимо. -- Типы полей соответствуют Core. Тип значения `defaultValue` зависит от поля; - `date` и `bigint` используют строки, включая `CollectionRow` и входы storage. - для `file` / `files` дефолт не поддерживается. `searchable` доступен для `text` - и `email`; создание индекса это свойство не описывает. -- `presentation` поля использует существующий `FieldPresentation` из contracts; - можно указать только нужные настройки. У коллекции пока доступны `displayName` - и `hidden`. Скрытие из навигации не заменяет права или защиту структуры. -- `defineCollection` возвращает описание без побочных действий. Это не runtime- - валидатор: допустимость имён, UUID/дат/чисел, сочетаний настроек интерфейса и - конфликтов полей проверяет загрузчик Core через общие валидаторы коллекций. - -Пакет с коллекциями задаёт `asmblyr.manifest.namespace`, например `comments`, -и экспортирует `"./collections": "./dist/collections.json"`. Namespace — стабильное -имя из 1–31 строчных латинских букв, цифр и подчёркиваний, начиная с буквы. -Он уникален между установленными пакетами; `asmblyr` и префиксы `asmblyr_` / `plugin_` -зарезервированы. Полное имя таблицы должно умещаться в 63 символа PostgreSQL. -Сборщик создаёт индекс деклараций. В dev Core сканирует исходники локального пакета, -в production читает индекс и скомпилированные модули. `plugin.ts` остаётся пустым. - -Core хранит namespace и npm-владельца в `asmblyr_plugins`, а локальное имя, -физическое имя и нормализованное описание — в `asmblyr_plugin_collections`. -Реестр создаётся обычной миграцией Core (`pnpm db:migrate`). При старте установка -всех включённых коллекций проходит в одной транзакции под общей блокировкой. -Чужие таблицы и метаданные не присваиваются. Повторный запуск сохраняет данные. - -Изменения установленного описания должны быть воспроизведены явными миграциями -из `server/migrations`. Несовпадение итогового описания останавливает запуск. -Новые коллекции добавляются автоматически. Исчезнувшая -таблица не создаётся заново поверх реестра. Прямые изменения структуры через SQL -не синхронизируются автоматически. Отключение плагина сохраняет таблицы и владельца. - -Префикс `plugin_` зарезервирован в редакторе структуры и API независимо от того, -включён ли пакет. Обычные пользователи и superuser не меняют структуру и настройки -таких коллекций. Строки доступны через `/items` и kit по обычным правам, включая -историю изменений; отключение кода плагина само по себе эти права не отзывает. -MCP при установке выключен. Связи между коллекциями плагина пока не поддержаны -декларациями. Формат пока экспериментальный. - -## Миграции, хранилище и UI - -Полный контракт и ограничения описаны в [жизненном цикле плагина](./kit-lifecycle.md). -Пример со всеми тремя возможностями — `packages/plugin-comments`. - -## Пакет и подключение - -Имя, версия и зависимости плагина остаются в его `package.json`. -`asmblyr.manifest.version` задаёт версию формата манифеста, а не версию kit. - -Пакет плагина экспортирует собранный модуль по `exports["."].default`, типы по -`exports["."].types`, метаданные по `exports["./package.json"]` и сгенерированный -`dist/routes.json` по `exports["./routes"]`. Пакеты с namespace также экспортируют -`dist/collections.json` по `exports["./collections"]`. -Пример настройки находится в `packages/plugin-comments/package.json`. - -Пакеты устанавливаются как зависимости корневого проекта и явно включаются -в его `package.json`: +- Name is local to the plugin. Manifest namespace produces plugin*<namespace>*<name>, such as plugin_comments_entries. +- PrimaryKey/timestamps use shared Contracts types; do not duplicate them in fields. +- Field keys are technical names; declaration order determines column creation order. Installation persists metadata. +- Required means nonempty API input; nullable permits SQL NULL. Both are explicit/independent, including true/true. +- Types match Core. DefaultValue depends on type; date/bigint use strings, including CollectionRow/storage input. File/files have no defaults. Searchable supports text/email and does not itself declare an index. +- Field presentation uses FieldPresentation with optional settings. Collection presentation currently supports displayName/hidden. Navigation hiding is not authorization or structural protection. +- DefineCollection is side-effect-free and not a runtime validator. Core uses shared validators for names, values, interface combinations, and field conflicts. + +Packages with collections specify a namespace and export ./collections as ./dist/collections.json. Namespace is 1–31 lowercase Latin letters/digits/underscores, beginning with a letter, unique among installed packages. Asmblyr and asmblyr*/plugin* prefixes are reserved. Full table names must fit PostgreSQL's 63-character limit. + +The builder generates the declaration index. Development scans local source; production loads indexes/compiled modules. `plugin.ts` remains empty. + +Core stores namespace/npm ownership in asmblyr_plugins and local/physical names plus normalized declarations in asmblyr_plugin_collections. Normal Core migrations create the registry. Startup installs all enabled collections in one transaction under a shared lock. Foreign tables/metadata are never adopted; restarting preserves data. + +Installed declaration changes require explicit server/migrations reproducing the new state. Mismatch stops startup. New collections install automatically. Missing registered tables are not silently recreated. Direct SQL structure edits do not synchronize automatically. Disabling a plugin preserves tables/ownership. + +Plugin\_ remains reserved in UI/API whether the package is enabled or not. Neither ordinary users nor superusers edit these collections' structure/settings. Rows use normal /items/Kit grants and history; disabling plugin code does not revoke those grants. MCP starts disabled. Declaration-based relations between plugin collections are unsupported; the format remains experimental. + + + +## Migrations, storage, and UI + +See [plugin lifecycle](kit-lifecycle.md). Packages/plugin-comments demonstrates all three. + + + +## Package and installation + +Name, version, and dependencies remain in package.json. Asmblyr.manifest.version is the manifest format version, not Kit's version. + +Export built modules through exports["."].default, types through exports["."].types, metadata through ./package.json, and generated dist/routes.json through ./routes. Namespaced packages also export dist/collections.json through ./collections. See packages/plugin-comments/package.json. + +Install dependencies at the project root and explicitly enable them: ```json { @@ -355,53 +240,32 @@ MCP при установке выключен. Связи между колле } ``` -Core проверяет имена, версии манифестов и наличие обработчиков всех включённых -пакетов перед импортом. Ошибки включённого плагина останавливают запуск. -Сканирование происходит только внутри включённых пакетов; установка сама по себе -не включает плагин. +Core validates enabled package names, manifest versions, and handlers before imports. Enabled-plugin failures stop startup. Discovery scans enabled packages only; installing a dependency alone does not enable it. -## Разработка и сборка + -В разработке `pnpm dev` использует исходники включённых локальных пакетов из -workspace-пакетов внутри проекта. Изменение, добавление и удаление `.ts`-файлов автоматически перезапускает -Core и обновляет маршруты. Это перезапуск процесса, а не замена обработчика на лету. +## Development and build -Сборка плагина выполняется командой kit `asmblyr-plugin build` в каталоге пакета; -в comments она задана в script `build`. TypeScript использует `rootDir: "."`, -`outDir: "dist"`, включая `plugin.ts` и `server/**/*.ts`. -Для типов Web API добавлены `DOM` и `DOM.Iterable` в `lib`. В kit и примере включён -`skipLibCheck`: декларации выбранной RC содержат несовместимости с TypeScript 5.9 -и ссылки на опциональные зависимости. Проверка собственного кода остаётся строгой; -при обновлении H3 это ограничение нужно пересмотреть. -Сборщик проверяет имена маршрутов, default exports и типы, очищает собственный -`dist`, создаёт JavaScript, декларации типов и список маршрутов `dist/routes.json`. +Pnpm dev uses enabled local workspace sources. Adding/changing/removing TypeScript files restarts Core and rebuilds route discovery; it is a process restart, not hot handler replacement. -В production и для установленных npm-пакетов Core читает готовый список и загружает -JavaScript. Сканирования исходников в этом режиме нет. Удалённые исходные маршруты -не сохраняются в следующей сборке. +Run asmblyr-plugin build in the package directory, as the Comments build script does. TypeScript uses rootDir:"." and outDir:"dist", including plugin.ts and server/\*_/_.ts. DOM/DOM.Iterable provide Web API types. Kit/examples use skipLibCheck because the pinned H3 RC declarations conflict with TypeScript 5.9 and refer to optional dependencies; project code stays strictly checked. Revisit this when upgrading H3. -В репозитории Kit и comments связаны как workspace-пакеты. `pnpm build:plugins` собирает оба; -`pnpm dev` выполняет начальную сборку автоматически. `pnpm build` сначала собирает -пакеты, затем Core и UI. Публикация пакетов остаётся отдельным шагом. +The builder checks route names, default exports, and types, then generates JavaScript, declarations, and dist/routes.json in its own dist. Production/npm packages load the index and JavaScript without source scanning. Deleted routes disappear from the next build. -Проверка kit и первого плагина из корня репозитория: +Workspace builds link Kit and bundled plugins. Pnpm build:plugins builds them; pnpm dev performs initial generation. Pnpm build builds packages before Core/UI. Publishing remains separate. ```sh pnpm build:plugins pnpm --filter @asmblyr-collaborative/kit --filter @asmblyr-collaborative/plugin-comments typecheck ``` -## Локализация расширения + -Необязательные плоские JSON-каталоги `locales/ru.json` и `locales/en.json` -располагаются рядом с `package.json`. Включайте `locales/` в опубликованный пакет -вместе с `dist/`. Core читает их одинаково в source и built режиме и добавляет -в защищённый `GET /translations`. Каталоги содержат только публичные подписи: не -добавляйте в них настройки, ключи или пользовательские данные. +## Plugin localization -UI использует тот же каталог через `defineUiPlugin({ translations: { ru, en }, ... })`. -Для JSON-импортов включите `resolveJsonModule` в tsconfig. У страниц, панелей -и интерфейсов поля можно указать `titleKey`; `title` остаётся запасной подписью. +Optional flat locales/ru.json and locales/en.json sit beside package.json. Publish locales alongside dist. Core reads them in source/built modes and includes them in protected GET /translations. Catalogs contain public labels only, never settings, keys, or user data. + +UI uses defineUiPlugin({ translations: { ru, en }, ... }). Enable resolveJsonModule for JSON imports. Pages, record panels, and field interfaces support titleKey with title fallback. ```tsx import { usePluginTranslations } from "@asmblyr-collaborative/kit/ui/i18n"; @@ -412,32 +276,24 @@ function Panel() { } ``` -Хост предоставляет изолированный i18next provider на текущем языке профиля, -пространство имён `plugin.` и русский fallback. React -экранирует текст при отображении. Не импортируйте серверный entry point в UI. -Ключи `collection..label` и -`field...label|description|placeholder` задают подписи -собственных таблиц в UI и API. Для настроек используются `settings.title`, -`settings.description` и `settings..label|description`. -Имена коллекций/полей, значения записей и HTTP-контракты не переводятся. +The host supplies isolated i18next context using the profile locale, plugin.<manifest.namespace>, and Russian fallback. React escapes text. Never import the server entry in UI. + +Collection.<local-name>.label and field.<local-name>.<field>.label|description|placeholder supply owned-table UI/API labels. Settings use settings.title, settings.description, and settings.<field>.label|description. Technical names, record values, and HTTP contracts are not translated. + + + +## Personal external connections -## Личные внешние подключения +Model annotation connection:"google" makes a handler visible only with the current human's active connection. Declare connections.google and obtain explicit project approval. -Model handler с `connection: "google"` в `defineModelAnnotation` виден ассистенту только при активном подключении текущего человека. Пакет запрашивает capability `connections.google`, установка явно одобряет её в `asmblyr.pluginPermissions`. +`useActionContext`(event).connections?.google exposes owner-bound list, readText, sheet, cells, and proposeWrite. Kit exports PersonalConnections, GoogleWriteInput, GoogleFileList, GoogleText, GoogleSheet, and GoogleCells. There are no raw tokens, arbitrary URLs, or confirmation methods. `proposeWrite` saves a proposal; humans confirm actual writes in Core UI. HTTP/MCP share handlers. See packages/plugin-google-workspace and [Google Workspace](../features/google-workspace.md). -`useActionContext(event).connections?.google` предоставляет owner-bound операции `list`, `readText`, `sheet`, `cells`, `proposeWrite`. Контракты `PersonalConnections`, `GoogleWriteInput`, `GoogleFileList`, `GoogleText`, `GoogleSheet`, `GoogleCells` экспортируются Kit. Нет raw token, произвольного URL или метода подтверждения. `proposeWrite` только сохраняет предложение; реальную запись подтверждает человек в UI Core. HTTP и внутренний MCP используют одинаковый handler. Пример — `packages/plugin-google-workspace`; ограничения и настройка — [Google Workspace](../../docs/features/google-workspace.md). + ## SDK consumer contracts -Built defineModelContext handlers include generated JSON input/output schemas. -Core exports accessible handlers through GET /schema, using their existing -AccessGate and original HTTP address. The generated SDK provides -`client.plugins.namespace[methodId](input)` with inferred output. Method IDs join -route segments with hyphens. No extra SDK handler or registry is needed. +Built defineModelContext handlers include generated input/output JSON schemas. GET /schema exports accessible handlers using AccessGate and original HTTP routes. Generated SDK methods use client.plugins.namespace[methodId](https://github.com/Asmblyr/Collaborative/blob/main/packages/kit/input) with inferred output. IDs join route segments with hyphens; no extra registry is needed. -Contracts describe Kit's bounded JSON model subset. They do not grant data access -or imply that a personal OAuth connection is available. Execution still runs the -original gate and current Core permissions. Legacy defineAction handlers without -a generated outputSchema do not appear in the typed consumer API. +The bounded JSON contract grants no data access and implies no personal OAuth connection. Execution rechecks the original gate/current Core permissions. Legacy defineAction handlers without generated outputSchema are omitted. -[Package checks and release preparation](./packages.md). +See [package checks and release preparation](packages.md). diff --git a/docs/reference/kit-hooks.md b/docs/reference/kit-hooks.md index ff149b0..b95fdab 100644 --- a/docs/reference/kit-hooks.md +++ b/docs/reference/kit-hooks.md @@ -1,13 +1,16 @@ -# Hooks и настройки + -`plugin.ts` остаётся `definePlugin({})`. Core обнаруживает дополнительные файлы -автоматически. Возможности объявляются и разрешаются по [контракту capabilities](./kit-capabilities.md). +# Hooks and settings -## Транзакционные hooks +`plugin.ts` remains definePlugin({}). Core discovers additional files automatically. Declare and approve [capabilities](kit-capabilities.md). -Каждый файл непосредственно в `server/hooks/` экспортирует один обработчик: + + +## Transactional hooks + +Each direct server/hooks file exports one handler: ```ts import { defineHook } from "@asmblyr-collaborative/kit"; @@ -20,91 +23,63 @@ export default defineHook("items.delete", async (event, context) => { }); ``` -Пример очистки с пагинацией: `packages/plugin-comments/server/services/cleanup.ts`. -Не создавайте реестр hooks в `plugin.ts`. - -| Событие | Данные | -| -------------------- | -------------------------------------- | -| `items.create` | `collection`, `collectionId`, `itemId` | -| `items.update` | `collection`, `collectionId`, `itemId` | -| `items.delete` | `collection`, `collectionId`, `itemId` | -| `collections.delete` | `collection`, `collectionId` | - -`collectionId` — стабильный ID экземпляра коллекции; имя может впоследствии -принадлежать другой коллекции. `itemId` — строковое представление сохранённого ключа. -Скрытые значения полей в событие не попадают. Контекст предоставляет actor, -requestId, logger, reader `items`, разрешённый `storage` и настройки пакета. -Reader использует права исходного пользователя; удалённую запись уже не прочитать. - -Событие вызывается после мутации и записи истории, **до commit**. Hook выполняется -в той же транзакции, включая свою работу с `storage`. Ошибка прерывает операцию и -откатывает данные, историю и изменения предыдущих hooks. Обработчики выполняются -последовательно: порядок пакетов в конфигурации, затем имена файлов по порядку -кодовых единиц строки. Source и production используют один порядок. -Нельзя рассчитывать, что внешний HTTP-вызов или отправка уведомления откатятся -вместе с БД. Такие эффекты и гарантированная доставка после commit пока не поддержаны. -Каждый вызов сервиса обязательно нужно `await`-ить. - -HTTP items, Kit, MCP, массовые изменения, операции связей и сохранение черновика -используют общую границу мутаций. Нет событий для update без изменений, прямого -SQL, каскадных действий PostgreSQL и записей в `asmblyr_`/`plugin_`. Исключение -внутренних коллекций предотвращает рекурсию при очистке собственного хранилища. -Отключённый плагин не получает события; его данные сохраняются. - -Production export: `"./hooks": "./dist/hooks.json"`. -Сборщик создаёт индекс; вручную поддерживать его не нужно. -В `server/hooks` допустимы только непосредственные `.ts`-файлы без symlinks; -вспомогательный код размещается в `server/services` или другой соседней папке. - -## Работа с данными, привязанными к записи - -В обычном endpoint `context.withRecord(collection, id, async context => ...)` -проверяет право чтения, удерживает разделяемую блокировку записи и выполняет callback -в транзакции. Используйте **контекст callback** для своего `storage`: он участвует -в той же транзакции. Это позволяет сохранять комментарий и исключить одновременное -удаление его целевой записи. Метод требует `items.read`; в model handlers его нет. -В callback не выполняйте длительную внешнюю работу и не сохраняйте контекст наружу. - -## Настройки - -Файл `server/settings.ts`: +See packages/plugin-comments/server/services/cleanup.ts for paginated cleanup. Do not create a hook registry in plugin.ts. + +| Event | Data | +| ------------------ | -------------------------------- | +| items.create | collection, collectionId, itemId | +| items.update | collection, collectionId, itemId | +| items.delete | collection, collectionId, itemId | +| collections.delete | collection, collectionId | + +`collectionId` identifies the collection instance; the name may later be reused. `itemId` is the saved key as a string. Hidden field values are excluded. Context includes actor, requestId, logger, items reader, approved storage, and package settings. Reads use the original caller; deleted rows cannot be reread. + +Hooks run after mutation/history but before commit, in the same transaction, including storage operations. Failure rolls back data, history, and earlier hooks. Execution is sequential: configured package order, then filename order by string code units, identical in source/production. + +External HTTP calls or notifications cannot be rolled back with PostgreSQL. External effects and guaranteed post-commit delivery are unsupported. Await every service call. + +HTTP items, Kit, MCP, bulk, relation changes, and draft saves share this boundary. No events fire for unchanged updates, direct SQL, PostgreSQL cascades, or asmblyr*/plugin* writes. Internal-collection exclusion prevents recursive cleanup. Disabled plugins receive no events and retain data. + +Production export: "./hooks": "./dist/hooks.json". The builder maintains the index. Only direct .ts files without symlinks belong under server/hooks; put helpers in server/services or a sibling directory. + + + +## Record-bound data + +In ordinary endpoints, context.withRecord(collection, id, async context => ...) checks reads, holds a shared record lock, and runs the callback transactionally. Use the callback's context for storage in that same transaction. This allows comment saves while preventing concurrent target deletion. + +It requires items.read and is unavailable in model handlers. Do not perform slow external work inside the callback or retain its context outside. + + + +## Settings + +`server/settings.ts`: ```ts import { defineSettings } from "@asmblyr-collaborative/kit"; export default defineSettings({ - title: "Комментарии", + title: "Comments", fields: { allowNewComments: { type: "boolean", - label: "Разрешить новые комментарии", + label: "Allow new comments", default: true, }, }, }); ``` -Использование в endpoint, model handler или hook: +Use in endpoints, model handlers, or hooks: ```ts const options = useSettings(context, settings); -// options.allowNewComments имеет тип boolean. +// options.allowNewComments is boolean. ``` -Поддержаны boolean, строка с maxLength, число с min/max/integer, select со списком -вариантов. Core проверяет декларацию, default и каждое сохраняемое значение. -Неизвестные поля, пропущенные значения и неявное приведение типов не допускаются. -До 40 полей и 32 000 символов JSON значений. Это обычные настройки, не секреты. - -Production export: `"./settings": "./dist/server/settings.js"`. -Суперпользователь управляет настройками через `/settings/plugins/:namespace` -и форму **Система → Плагины**. PUT принимает `{ values, revision }`; устаревшая -revision возвращает 409, сохраняя черновик в UI. `null` обозначает ещё не сохранённые -значения по умолчанию. Кнопка «По умолчанию» меняет черновик до явного сохранения. - -Значения хранятся в `asmblyr_settings` под ключом `plugin:`. -Изменения действуют для новых запросов без перезапуска; в журнале безопасности -сохраняются автор, namespace и имена изменённых настроек, без копирования значений. -Новые поля получают default, удалённые игнорируются. Несовместимое изменение типа -или диапазона существующей настройки требует предварительного обновления её значения: -Core отклоняет неверное сохранённое значение и не исправляет его молча. +Supported types: boolean, string with maxLength, number with min/max/integer, and select options. Core validates declaration, defaults, and saved values. Unknown fields, missing values, and implicit coercion are rejected. Limits: 40 fields and 32000 JSON characters. These are ordinary settings, not secrets. + +Production export: `"./settings": "./dist/server/settings.js"`. Manage them through `/settings/plugins/:namespace` and Settings → Extensions. Reads require `plugins/read` (or `plugins/update`); writes require `plugins/update`. Superusers also have access. PUT accepts `{ values, revision }`; stale revision returns 409 and preserves the UI draft. A null revision represents unsaved defaults. Restore defaults changes the draft until explicitly saved. + +Values live in asmblyr_settings under plugin:<namespace>. New requests see changes without restart. Security audit stores actor, namespace, and changed setting names, not values. New fields receive defaults; removed fields are ignored. Incompatible type/range changes require updating stored values first: Core rejects invalid saved values rather than silently repairing them. diff --git a/docs/reference/kit-lifecycle.md b/docs/reference/kit-lifecycle.md index b580615..83b9066 100644 --- a/docs/reference/kit-lifecycle.md +++ b/docs/reference/kit-lifecycle.md @@ -1,11 +1,14 @@ -# Миграции, хранилище и UI плагина + -## Миграции +# Plugin migrations, storage, and UI -Файлы `server/migrations/YYYYMMDDHHmmss_name.ts` экспортируют `defineMigration()`. -Нужен export `"./migrations": "./dist/migrations.json"` в package.json. + + +## Migrations + +`server/migrations/YYYYMMDDHHmmss_name.ts` files export defineMigration. `package.json` needs "./migrations": "./dist/migrations.json". ```ts import { defineMigration } from "@asmblyr-collaborative/kit"; @@ -28,47 +31,27 @@ export default defineMigration({ }); ``` -Одновременно обновите актуальное описание в `server/collections`. Миграции содержат -неизменяемые JSON-планы: контрольная сумма не зависит от компиляции TS в JS. -Не импортируйте изменяемое актуальное описание в старую миграцию: её план должен -оставаться прежним. Есть `addField` и обычный составной B-tree `addIndex`. -SQL, `drop`, смены типов, переименований, backfill и автоматического down пока нет. -NOT NULL без default на заполненной таблице завершится ошибкой PostgreSQL и откатом. +Update the current server/collections declaration too. Migrations are immutable JSON plans; checksums do not depend on TS-to-JS compilation. Never import mutable current declarations into old migrations. Supported operations are addField and ordinary composite B-tree addIndex. SQL, drop, type changes, renames, backfills, and automatic down are unsupported. Adding NOT NULL without a default to populated data fails in PostgreSQL and rolls back. + +Core applies new plans by filename under a shared lock. Operations accept only local names of owned collections. Asmblyr_plugin_migrations records name, SHA-256 plan hash, timestamp, and baseline flag. Editing/removing applied plans or inserting a plan before an applied one is forbidden. Operation failures or final-declaration mismatch roll back the full installation: DDL, metadata, and journal. -Core под общей блокировкой запускает новые планы по имени файла. Операции принимают -только локальные имена собственных коллекций. Реестр `asmblyr_plugin_migrations` -хранит имя, SHA-256 плана, время и признак baseline. Изменение/удаление выполненного -плана и добавление плана раньше уже применённого запрещены. Ошибка операции или -несовпадение итоговой декларации откатывает всю установку: DDL, метаданные и журнал. +New installations create current declarations, validate addField, create indexes, and baseline plans. New tables in existing plugins also use current schemas. Removing declarations is unsupported. Disabling never deletes data; registry down is blocked while history exists. Direct SQL changes do not synchronize automatically. -При новой установке таблица создаётся по актуальному описанию, addField проверяется, -индексы создаются, планы отмечаются как baseline. Новая таблица в существующем -плагине также получает текущую схему. Удаление деклараций пока не поддержано. -Данные не удаляются при отключении. Down реестра запрещён при наличии истории. -Прямые изменения схемы через SQL автоматически не согласуются с декларациями. + -## Собственное хранилище +## Own storage -`storage` доступен внутри аутентифицированного обработчика пакета с namespace -и разрешённой возможностью `storage.own`. -Методы `list/get/create/update/delete` имеют формат `items`, но принимают локальное -имя, например `storage.list("entries")`. Область доступа ограничена коллекциями -этого пакета. Нет SQL, Knex, `commit`, смены actor или получения чужого namespace. -Применяются валидация Core, история с реальным автором и транзакции записи. +Authenticated namespaced handlers with storage.own receive storage. List/get/create/update/delete follow items shapes but accept local names, such as storage.list("entries"). Access is restricted to that package. No SQL, Knex, commit, actor switching, or foreign namespace access. Core validation, actual-actor history, and write transactions apply. -Это **право доверенного серверного плагина на своё хранилище**. Обычный `items` -по-прежнему использует права пользователя. Плагин обязан проверить предметные -условия до `storage`: comments сначала читает обсуждаемую запись через `items.get`, -потом обращается к своим комментариям. Grants на таблицу комментариев не нужны. +This is privileged access by a trusted plugin to its own storage. Ordinary items keeps user permissions. Plugins must enforce domain rules first: Comments reads the target through items.get before accessing comments storage, so users need no grants on comment tables. -Прямой `/items/plugin_…` остаётся административным путём с обычными grants; выдача -таких grants разрешает работу с таблицей в обход специализированного endpoint. -Для comments выдавать их участникам не нужно. Код плагина доверенный и выполняется -в процессе Core, без песочницы. Пользовательские таблицы через storage недоступны. +Direct /items/plugin\_… remains an administrative path under ordinary grants. Granting it bypasses specialized endpoint rules and is unnecessary for discussion participants. Plugin code is trusted in Core, without a sandbox. Storage cannot access user collections. -## Типизированное хранилище + -Для типизированного доступа к собственной коллекции используйте её декларацию: +## Typed storage + +Use the declaration for typed access: ```ts import { useStorage } from "@asmblyr-collaborative/kit"; @@ -76,109 +59,70 @@ import entries from "./server/collections/entries.js"; const comments = useStorage(context, entries); const row = await comments.get(id); -// row.body, row.author_id и остальные поля выведены из entries. +// row.body, row.author_id, and other fields are inferred from entries. ``` -`get/create/update` возвращают саму строку, `list` — `{ data, labels, page }`. -Имена и типы полей, обязательность при создании, nullable и ручной первичный ключ -выводятся из `defineCollection`. Дефолты позволяют пропускать поле при создании. -Генерируемые ключи и системные даты не принимаются в типизированной записи. -Даты нормализуются в ISO-строки, decimal/bigint сохраняются строками. Несовпадение -типов прочитанной строки с декларацией даёт ошибку вместо неявного преобразования. -Проверки записи и область доступа остаются у Core. Этот API читает полные строки; -для проекций используйте исходный `context.storage`. Не сохраняйте его между запросами. +Get/create/update return the row itself; list returns { data, labels, page }. DefineCollection infers names/types, required create fields, nullability, and manual primary keys. Defaults allow omission on create. Generated keys/timestamps cannot be supplied to typed writes. + +Dates normalize to ISO strings; decimal/bigint remain strings. Read values inconsistent with declarations throw instead of coercing. Core still owns validation/scope. This API reads full rows; use context.storage for projections. Never retain it across requests. -## Вкладки редактора записи + -`ui/index.ts` экспортирует `defineUiPlugin({ recordPanels: [...] })` из -**`@asmblyr-collaborative/kit/ui`**. Package.json экспортирует его сборку как `./ui`. -Корневой entry kit и `plugin.ts` в браузер не импортируются. -У вкладки есть `id`, `title`, React `component` и необязательный `supports(record)`. +## Record-editor tabs -Host передаёт `RecordPanelProps`: +Ui/index.ts exports defineUiPlugin({ recordPanels: [...] }) from @asmblyr-collaborative/kit/ui, with its build exported as ./ui. Do not import root Kit/plugin.ts into browsers. Panels define id, title, React component, and optional supports(record). -- `record`: техническое имя коллекции, сохранённый ID и отображаемое имя; -- `request(path, init)`: запрос через сессию UI в `/api//…`; -- `onStateChange({ dirty, busy })`: защита текста и выполняющейся записи от закрытия. +RecordPanelProps includes: -Компоненты shadcn импортируются напрямую из `@asmblyr-collaborative/kit/ui/`. -Админка и плагины используют одну реализацию из Kit, включая полные props и ref. -Дополнительный provider или prop `components` не требуется. -Подробнее: [общие компоненты UI](./kit-ui.md). +- Record: technical collection name, saved ID, display name. +- Request<T>(path, init): authenticated namespace request under /api/<namespace>/…. +- OnStateChange({ dirty, busy }): protects unsaved text and active writes from closing. -Вкладка монтируется при первом открытии и сохраняется при смене вкладок. -Сбой рендера изолируется от формы записи. Новой несохранённой записи вкладки не -предоставляются. Активные пакеты сверяются с аутентифицированным `/extensions`. +Import shadcn directly from kit/ui/<component>. Admin/plugins share full props/refs, without extra providers or components props. See [shared UI](kit-ui.md). -Next при запуске/сборке создаёт статические импорты из exports включённых пакетов -и источники Tailwind для их UI. Локальная разработка читает `ui/index.ts`, production -и npm — собранный `./ui`. Изменение списка пакетов требует перезапуска dev-сервера; -production требует пересборки UI. Изменения TSX используют обычный Fast Refresh. -React — peer dependency. Для UI используйте `.ts`/`.tsx` в относительных импортах -и `rewriteRelativeImportExtensions: true` в tsconfig: TypeScript заменяет их на -`.js` при сборке. Добавьте `ui/**/*` и `shared/**/*` в include. +Panels mount on first open and persist across tab changes. Render failures are isolated from the record form. New unsaved records have no panels. Active packages are checked through authenticated /extensions. -Первый полный пример — `packages/plugin-comments`. Серверные пакеты нельзя импортировать в UI. +Next generates static imports and Tailwind sources from enabled exports at startup/build. Development reads ui/index.ts; production/npm use built ./ui. Package-list changes require dev restart or production UI rebuild; TSX uses Fast Refresh. React is a peer dependency. Use .ts/.tsx relative imports with rewriteRelativeImportExtensions:true, and include ui/**/\* and shared/**/\*. -## Отдельные страницы +Packages/plugin-comments is the complete example. Server packages must never enter UI imports. -Второй пример — `examples/plugins/overview`. Браузерный entry может экспортировать -страницы, вкладки записи или оба вида расширений: + + +## Standalone pages + +Examples/plugins/overview demonstrates pages, which can coexist with record panels. ```ts import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; import { OverviewPage } from "./pages/overview-page.tsx"; export default defineUiPlugin({ - pages: [{ id: "home", title: "Обзор", component: OverviewPage }], + pages: [{ id: "home", title: "Overview", component: OverviewPage }], }); ``` -Маршрут: `/extensions//`. Host добавляет страницу в группу -«Приложения» бокового меню, отображает её название в шапке и использует текущий -layout. ID проверяются на допустимость в URL и уникальность внутри каждого вида -расширений. Страница и вкладка одного пакета могут иметь одинаковый ID. - -`PluginPageProps` предоставляет `request: PluginRequest` — тот же ограниченный -namespace HTTP-клиент, что у вкладок. `Button` и `Textarea` доступны из kit. -Общая оболочка изолирует ошибки отрисовки от остального интерфейса. -Список активных пакетов загружается для layout через аутентифицированный -`/extensions` и используется страницами, меню и вкладками записи. - -Пользователь должен войти и иметь доступ к админке по её существующим правилам. -Отдельные разрешения на UI-страницы пока не реализованы; API каждого плагина обязан -сам проверять предметные права. Отсутствующий или отключённый плагин не отображается. -Включение пакета требует сборки/перезапуска UI и Core. - -Пока поддержаны фиксированные страницы без вложенных и динамических маршрутов. -Страница получает также `preparedAction` и `onStateChange`. -`useAction(contract, props, { path, initialInput })` использует обычный `request` -и связывает схему формы с серверным действием, подготовленным результатом -и признаками dirty/busy для host. Обзор только читает собственный контекст пользователя. -Редакторы полей описаны в [FIELDS.md](./kit-fields.md). - -## Действия и ассистент - -Первый пример — `examples/plugins/calculator`. TypeScript-интерфейсы описывают поля и результат. -H3 handler в `server/api/calculator/calculate.post.ts` обёрнут в -`defineModelContext(defineHandler(...), annotate)`. -`defineModelAnnotation` описывает назначение и `middleware: AccessGate.authenticated`. -Сборщик и source loader создают `.asmblyr/models` для runtime-валидации и UI; -для импорта JSON формой нужен `resolveJsonModule: true` в tsconfig. -Сканер подключает HTTP-маршрут по файлу; Core публикует явно отмеченные действия -во внутреннем MCP. `plugin.ts` остаётся `definePlugin({})`. -HTTP и MCP вызывают один handler с одним ограниченным контекстом: -`useActionContext(event)` предоставляет actor, superuser, signal и items. -`useItems(event)` работает и в обычных endpoints: проверяет права действия и полей. -Для MCP также проверяется доступность коллекций и связей в MCP. Привилегированного -storage в model handler нет. `readOnly: true` запрещает все методы записи items. -Форма использует `useAction(generatedModel, props, options)`; -схемы браузера и сервера получены из одних типов, серверный handler в UI не попадает. -Подробности и текущие ограничения — [действия плагинов](../development/plugin-actions.md). - -## Возможности, hooks и настройки - -Пакет объявляет capabilities в manifest; проект разрешает их через -`asmblyr.pluginPermissions`. Правила и ограничения: [CAPABILITIES.md](./kit-capabilities.md). -Транзакционные обработчики в `server/hooks` и настройки в `server/settings.ts` -описаны в [HOOKS.md](./kit-hooks.md). Регистрация в `plugin.ts` не требуется. +Routes use /extensions/<namespace>/<id>. The host adds pages under Applications, shows their title, and uses the current layout. IDs must be URL-safe and unique within each extension kind; a page/panel may share an ID. + +PluginPageProps.request is the same namespace-limited HTTP client. Shared Button/Textarea come from Kit. The shell isolates render errors. Authenticated /extensions supplies enabled packages to layout, menu, pages, and panels. + +Users must satisfy existing admin admission rules. Separate page permissions are unsupported; each plugin API enforces its domain rights. Missing/disabled plugins are hidden. Enabling requires rebuilding/restarting UI and Core. + +Only fixed pages are supported, without nested/dynamic routes. Pages also receive preparedAction/onStateChange. UseAction(contract, props, { path, initialInput }) connects form schema, ordinary requests, prepared results, and dirty/busy host state. Overview reads only the current caller's context. See [field editors](kit-fields.md). + + + +## Actions and assistant + +Examples/plugins/calculator declares input/output in TypeScript. Its static POST handler uses defineModelContext<CalculationInput>(defineHandler(...), annotate), with defineModelAnnotation and AccessGate.authenticated. + +Builder/source loader produce .asmblyr/models for runtime validation/UI; enable resolveJsonModule for JSON imports. File discovery registers HTTP; Core exposes annotated handlers to internal MCP. `plugin.ts` stays definePlugin({}). + +HTTP/MCP share one restricted handler context. `useActionContext` exposes actor, superuser, signal, and items. UseItems works in ordinary endpoints too, checking actions/fields. MCP additionally checks publication of collections/relations. Model handlers receive no privileged storage. ReadOnly:true blocks all items writes. + +Forms use useAction<Input, Output>(generatedModel, props, options), deriving browser/server schemas from the same types without importing server handlers. See [action contracts](../development/plugin-actions.md). + + + +## Capabilities, hooks, and settings + +Declare manifest capabilities and approve them in asmblyr.pluginPermissions. See [capabilities](kit-capabilities.md). Transactional server/hooks and server/settings.ts are covered in [hooks/settings](kit-hooks.md). No registration in plugin.ts is required. diff --git a/docs/reference/kit-ui.md b/docs/reference/kit-ui.md index da8347d..0872d3b 100644 --- a/docs/reference/kit-ui.md +++ b/docs/reference/kit-ui.md @@ -1,13 +1,12 @@ -# Общие компоненты UI + -Регистрация собственных редакторов полей и их настроек описана в -[FIELDS.md](./kit-fields.md). Они используют эти же компоненты и тему хоста. +# Shared UI components -Kit содержит исходники общих shadcn-компонентов в `src/ui`. Админка и плагины -импортируют одну реализацию. Компоненты работают без `PluginUiProvider`; -обычные props, события, ref, `asChild` и варианты shadcn доступны напрямую. +[Field editors](kit-fields.md) use these same components and host theme. + +Kit owns shared shadcn sources under src/ui. Admin and plugins import one implementation. Components need no PluginUiProvider; ordinary props, events, refs, asChild, and shadcn variants work directly. ```tsx import { Button } from "@asmblyr-collaborative/kit/ui/button"; @@ -29,34 +28,27 @@ import { } from "@asmblyr-collaborative/kit/ui/select"; ``` -`buttonVariants` экспортируется из `@asmblyr-collaborative/kit/ui/button` для ссылок и других -элементов, которым нужен стиль кнопки. `@asmblyr-collaborative/kit/ui` содержит регистрацию -плагина и контракты его UI. Серверный entry `@asmblyr-collaborative/kit` в браузер не импортируется. -React и ReactDOM — совместимые peer dependencies: плагины используют экземпляры -хоста. Серверные потребители Kit не обязаны импортировать UI. +`buttonVariants` from kit/ui/button styles links and other elements. `kit/ui` contains plugin registration/UI contracts. Never import the server root into browsers. React/ReactDOM are compatible peer dependencies shared with the host. Server consumers need not import UI. + + -## Стили и сборка +## Styles and build -Хост использует Tailwind 4, анимации shadcn и общие переменные темы. В его глобальном -CSS уже подключено: +The host uses Tailwind 4, shadcn animations, and shared theme variables. Global CSS already imports: ```css @import "@asmblyr-collaborative/kit/ui/styles.css"; ``` -Этот entry указывает Tailwind на собранные компоненты внутри пакета, поэтому их -классы доступны и из установленного пакета. Плагину повторять импорт не нужно; -его собственные классы хост сканирует через сгенерированный `plugin-ui.css`. -Цвета, радиусы и светлая/тёмная тема принадлежат хосту. +This entry points Tailwind at built package components. Plugins need not repeat it; generated plugin-ui.css includes their own classes. Colors, radii, and light/dark themes belong to the host. -После изменения Kit выполните из корня `pnpm --filter @asmblyr-collaborative/kit build`. -`pnpm dev` и `pnpm build` также собирают Kit перед приложениями. +After Kit changes run pnpm --filter @asmblyr-collaborative/kit build. Pnpm dev/build also builds Kit first. -## Выпадающие списки внутри нативного dialog + -Radix-портал должен находиться в том же диалоге, чтобы не попасть под его backdrop. -В `SelectContent` можно передать `container={dialogElement}`. Для вложенных контролов -есть общий контекст из `@asmblyr-collaborative/kit/ui/portal-container`: +## Dropdowns inside native dialogs + +Radix portals must stay inside the dialog to appear above its backdrop. Pass container={dialogElement} to SelectContent or use the shared kit/ui/portal-container context: ```tsx @@ -64,17 +56,12 @@ Radix-портал должен находиться в том же диалог ``` -`SelectContent` использует явно указанный контейнер или унаследованный контекст. -Popover админки использует этот же контекст. За пределами диалога контейнер не нужен. +SelectContent uses an explicit container or inherited context. Admin popovers share it. No container is needed outside dialogs. + + -## Изменение и добавление компонентов +## Changing or adding components -Общие Button, Input, Textarea, Select, Checkbox и Tabs меняются в Kit. -Сохраняйте `"use client"` на интерактивных модулях и проверяйте компоненты внутри -нативного диалога. Button и `buttonVariants` остаются доступными серверным компонентам. +Change shared Button, Input, Textarea, Select, Checkbox, and Tabs in Kit. Keep "use client" on interactive modules and check native-dialog behavior. Button/buttonVariants remain usable from server components. -`apps/ui/components.json` пока обслуживает остальные локальные компоненты админки. -Не запускайте `shadcn add --overwrite` для уже перенесённых компонентов в `apps/ui`: -это создаст вторую реализацию. При переносе следующего компонента добавьте явный -export в Kit, перенесите необходимые зависимости и замените импорты потребителей. -Импорты внутри Kit не должны зависеть от `@/` или файлов админки. +Apps/ui/components.json still manages other local components. Do not run shadcn add --overwrite for components already moved to Kit: it creates duplicates. When moving another component, add an explicit Kit export, move dependencies, and replace consumer imports. Kit imports must not depend on @/ aliases or admin files. diff --git a/docs/reference/packages.md b/docs/reference/packages.md index 00f7235..c21d6f7 100644 --- a/docs/reference/packages.md +++ b/docs/reference/packages.md @@ -1,3 +1,5 @@ + + # SDK packages Four MIT packages are available on npm at version `0.1.0-beta.1`: @@ -43,6 +45,8 @@ For local package development, use workspace commands or the checked archives. S and the [SDK guide](sdk-guide.md) for runtime clients. Generated code needs ordinary TypeScript; it does not require a custom compiler or build plugin. + + ## Release procedure Publication is a separate maintainer action. Run repository checks and builds, diff --git a/docs/reference/plugin-color.md b/docs/reference/plugin-color.md index 596e8d9..2a481be 100644 --- a/docs/reference/plugin-color.md +++ b/docs/reference/plugin-color.md @@ -1,27 +1,22 @@ -# Цвет + -Пример расширения редактора обычного текстового поля. +# Color -1. Откройте настройки текстового поля → **Отображение**. -2. В списке **Редактор** выберите **Цвет · расширение**. -3. Настройте палитру и возможность вводить произвольный цвет. -4. Сохраните поле. В карточке появится палитра, в таблице — цвет и HEX. +An example editor extension for ordinary text fields. -Каждое поле хранит свою палитру. Поддержаны 1–24 цвета в формате `#RRGGBB`, -очистка значения, обязательность и блокировка на время сохранения. Старое -значение вне палитры сохраняется, пока пользователь сам его не заменит. -Некорректная строка отображается как текст, без выполнения HTML или CSS. +1. Open a text field's Presentation settings. +2. Select Color · extension in Editor. +3. Configure its palette and whether custom colors are allowed. +4. Save. Record cards show the palette; tables show color and HEX. -`plugin.ts` — пустой серверный контракт. `ui/index.ts` регистрирует вклад -`color:picker`; editor, display, settings и чтение options находятся рядом -в отдельных небольших файлах. Импортов из Core или UI нет. +Each field stores its own palette of 1–24 #RRGGBB colors. Clearing, required validation, and disabling during save are supported. Existing values outside the palette remain until explicitly replaced. Invalid strings display as text without executing HTML/CSS. -Сборка: `pnpm --filter @asmblyr-collaborative/plugin-color build`. Пакет не включён по умолчанию; подключение описано в [руководстве примеров](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/README.md). После изменения состава пакетов перезапустите -Core и UI. При отключении плагина поле остаётся доступно как обычный текст. +`plugin.ts` is the empty server contract. Ui/index.ts registers color:picker; editor, display, settings, and option parsing live in separate small files. There are no Core/UI imports. -Настройка палитры не ограничивает HTTP API: формат и палитра являются UI. -Сервер применяет обычные права и ограничения текстового поля. +Build: pnpm --filter @asmblyr-collaborative/plugin-color build. It is disabled by default; see [example setup](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/README.md). Restart Core/UI after changing packages. Disabling falls back to ordinary text. -Полный контракт: [Kit / Редакторы полей](./kit-fields.md). +Palette settings constrain UI only, not HTTP API. The server applies normal text-field permissions/constraints. + +Full contract: [Kit field editors](kit-fields.md). diff --git a/docs/reference/routes.md b/docs/reference/routes.md index be31fd3..a6c24ec 100644 --- a/docs/reference/routes.md +++ b/docs/reference/routes.md @@ -1,234 +1,234 @@ -# Матрица HTTP-маршрутов +# HTTP route matrix -Сгенерировано из Core и проверенного каталога доступа. 226 деклараций. +Generated from Core and the reviewed access catalog. 226 declarations. -Это описание границ; их исполнение проверяют интеграционные тесты. Динамические маршруты плагинов и внутренние endpoints oidc-provider не перечисляются отдельно. +This describes access boundaries; integration tests verify enforcement. Dynamic plugin routes and internal oidc-provider endpoints are not listed individually. -| Метод и путь | Доступ | Источник в репозитории | +| Method and path | Access | Source in the repository | | --- | --- | --- | -| `GET /assistant/conversations` | Активный человек с доступом к чату; только собственные сессии | `apps/core/src/assistant/history-routes.ts` | -| `POST /assistant/conversations` | Активный человек с доступом к чату; создаёт собственную сессию с пустым контекстом | `apps/core/src/assistant/history-routes.ts` | -| `DELETE /assistant/conversations/:id` | Активный человек с доступом к чату; только собственная неактивная сессия | `apps/core/src/assistant/history-routes.ts` | -| `GET /assistant/conversations/:id` | Активный человек с доступом к чату; только собственная история, включая superuser | `apps/core/src/assistant/history-routes.ts` | -| `POST /assistant/filter/validate` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | -| `POST /assistant/messages` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | -| `POST /assistant/messages/:id/cancel` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | -| `POST /assistant/selection/validate` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | -| `GET /assistant/status` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | -| `POST /auth/browser/google/start` | Активный пользователь, точный Origin AUTH_UI_URL; proof в HttpOnly cookie. | `apps/core/src/auth/browser/google.ts` | -| `POST /auth/browser/invitations/claim` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | -| `POST /auth/browser/login` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | -| `POST /auth/browser/logout` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | -| `POST /auth/browser/passkeys/login` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | -| `POST /auth/browser/passkeys/options` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `GET /assistant/conversations` | Active human with chat access; own conversations only | `apps/core/src/assistant/history-routes.ts` | +| `POST /assistant/conversations` | Active human with chat access; creates an own conversation with empty context | `apps/core/src/assistant/history-routes.ts` | +| `DELETE /assistant/conversations/:id` | Active human with chat access; own idle conversation only | `apps/core/src/assistant/history-routes.ts` | +| `GET /assistant/conversations/:id` | Active human with chat access; own history only, including for superusers | `apps/core/src/assistant/history-routes.ts` | +| `POST /assistant/filter/validate` | Human: superuser OR at least one read/create/update grant; tools authorize separately | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/messages` | Human: superuser OR at least one read/create/update grant; tools authorize separately | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/messages/:id/cancel` | Human: superuser OR at least one read/create/update grant; tools authorize separately | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/selection/validate` | Human: superuser OR at least one read/create/update grant; tools authorize separately | `apps/core/src/assistant/routes.ts` | +| `GET /assistant/status` | Human: superuser OR at least one read/create/update grant; tools authorize separately | `apps/core/src/assistant/routes.ts` | +| `POST /auth/browser/google/start` | Active user, exact AUTH_UI_URL Origin; proof in an HttpOnly cookie. | `apps/core/src/auth/browser/google.ts` | +| `POST /auth/browser/invitations/claim` | Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/login` | Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/logout` | Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/passkeys/login` | Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/passkeys/options` | Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session. | `apps/core/src/auth/browser/routes.ts` | | `POST /auth/cli/authorize` | Active human session approves a 60-second one-use PKCE S256 code for an exact loopback callback. | `apps/core/src/auth/cli/routes.ts` | | `GET /auth/cli/config` | Public CLI discovery; configured admin consent URL only. No credential. | `apps/core/src/auth/cli/routes.ts` | | `POST /auth/cli/token` | One-use code + PKCE verifier + exact callback. Issues 10-minute schema:read access only; rate-limited. | `apps/core/src/auth/cli/routes.ts` | -| `POST /auth/federation-token` | Подписанный GitLab CI assertion и активная федерация | `apps/core/src/services/federation-routes.ts` | -| `POST /auth/invitations/accept` | Одноразовый invitation token и новый пароль | `apps/core/src/auth/routes.ts` | -| `POST /auth/invitations/claim` | Одноразовый invitation/recovery token; создаёт человеческую сессию. Recovery отзывает прежние сеансы и способы входа | `apps/core/src/auth/routes.ts` | -| `POST /auth/login` | Проверка email и пароля; ограничение частоты | `apps/core/src/auth/routes.ts` | -| `POST /auth/logout` | Refresh token в теле либо Bearer сессии | `apps/core/src/auth/routes.ts` | -| `GET /auth/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/routes.ts` | -| `POST /auth/passkeys/login` | Одноразовый challenge, подпись сохранённого passkey, точный origin/RP ID и user verification; активный пользователь | `apps/core/src/auth/passkeys/routes.ts` | -| `POST /auth/passkeys/options` | Публичный WebAuthn challenge, пять минут; общий credential rate limit | `apps/core/src/auth/passkeys/routes.ts` | -| `GET /auth/providers` | Публичные ID и подписи провайдеров | `apps/core/src/auth/sso/routes.ts` | -| `POST /auth/refresh` | Действительный refresh token; ротация и защита от повторного использования | `apps/core/src/auth/routes.ts` | -| `POST /auth/service-token` | Действительный сервисный ключ | `apps/core/src/services/routes.ts` | -| `POST /auth/setup` | Setup secret и отсутствие созданных пользователей; ограничение частоты | `apps/core/src/auth/routes.ts` | -| `GET /auth/setup/status` | Публичный статус первой настройки | `apps/core/src/auth/routes.ts` | -| `POST /auth/sso/:provider/callback` | Проверка SSO flow, state/PKCE и ответа провайдера | `apps/core/src/auth/sso/routes.ts` | -| `POST /auth/sso/:provider/start` | Начало SSO; привязка требует действующей пользовательской сессии | `apps/core/src/auth/sso/routes.ts` | -| `GET /collections` | Активный principal; только доступные коллекции и поля | `apps/core/src/collections/routes.ts` | -| `POST /collections` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `DELETE /collections/:name` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | -| `PUT /collections/:name/display` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `POST /collections/:name/fields` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `DELETE /collections/:name/fields/:field` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | -| `PATCH /collections/:name/fields/:field` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `POST /collections/:name/fields/:field/configuration` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PUT /collections/:name/fields/:field/configuration` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `GET /collections/:name/fields/:field/impact` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | -| `PUT /collections/:name/fields/:field/presentation` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PUT /collections/:name/fields/:field/search` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PATCH /collections/:name/folder` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PUT /collections/:name/form` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `GET /collections/:name/impact` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | -| `DELETE /collections/:name/materialized-view` | Человек-superuser; отключает MV из каталога с проверкой зависимостей, удаляет метаданные/права; сохраняет PostgreSQL объект и данные | `apps/core/src/collections/materialized-routes.ts` | -| `PATCH /collections/:name/navigation` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `POST /collections/:name/relations` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PUT /collections/:name/relations/:field/search` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PATCH /collections/:name/settings` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `GET /collections/:name/terms` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | -| `PUT /collections/:name/terms` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | -| `DELETE /collections/:name/terms/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | -| `PUT /collections/:name/terms/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | -| `DELETE /connections/google` | Активная человеческая сессия; удаляет собственные токены, flows и предложения, пытается отозвать Google grant | `apps/core/src/connections/routes.ts` | -| `GET /connections/google` | Активная человеческая сессия; состояние только собственного подключения, без токенов | `apps/core/src/connections/routes.ts` | -| `GET /connections/google/callback` | Редирект результата; подключение требует активную сессию, одноразовые state и browser proof, совпадение владельца. | `apps/core/src/auth/browser/google.ts` | -| `POST /connections/google/callback` | Активная человеческая сессия; собственный flow, browser proof, state/nonce/PKCE и та же конфигурация клиента | `apps/core/src/connections/routes.ts` | -| `POST /connections/google/start` | Активная человеческая сессия; одноразовый OAuth flow владельца с browser proof и PKCE | `apps/core/src/connections/routes.ts` | -| `DELETE /connections/google/writes/:id` | Активная человеческая сессия; отмена собственного ожидающего предложения | `apps/core/src/connections/routes.ts` | -| `GET /connections/google/writes/:id` | Активная человеческая сессия; собственное неистекшее предложение и то же активное подключение | `apps/core/src/connections/routes.ts` | -| `POST /connections/google/writes/:id/confirm` | Активная человеческая сессия; однократное подтверждение собственного предложения, перепроверка подключения и исходных данных; не MCP-инструмент | `apps/core/src/connections/routes.ts` | -| `GET /extensions` | Активный principal; метаданные включённых UI-плагинов | `apps/core/src/plugins/ui-routes.ts` | -| `GET /extensions/:namespace/drafts/:id` | Активный principal; подготовленный черновик только своего principal и namespace, с TTL | `apps/core/src/plugins/action-draft-routes.ts` | -| `GET /files` | Человек + files/read либо update; superuser bypass. Общая библиотека команды | `apps/core/src/files/routes.ts` | -| `POST /files` | Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок | `apps/core/src/files/routes.ts` | -| `DELETE /files/:id` | Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок | `apps/core/src/files/routes.ts` | -| `GET /files/:id` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | -| `PATCH /files/:id` | Человек + files/update; superuser bypass. Изменение title/description и visibility private/public одного ready-файла; публикация и отзыв ссылки записываются в историю. | `apps/core/src/files/routes.ts` | -| `GET /files/:id/content` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | -| `GET /files/:id/events` | Человек + files/read либо update; superuser bypass. Общая библиотека команды | `apps/core/src/files/routes.ts` | -| `GET /files/resolve` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | -| `GET /filter-presets/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | -| `POST /filter-presets/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | -| `DELETE /filter-presets/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | -| `PUT /filter-presets/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | -| `POST /folders` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `DELETE /folders/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PATCH /folders/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `PATCH /folders/:id/order` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | -| `GET /health` | Публичная проверка процесса | `apps/core/src/app.ts` | -| `GET /item-events/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `GET /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `PATCH /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `POST /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `DELETE /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `GET /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `PATCH /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `GET /items/:collection/:id/related` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `GET /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | -| `PATCH /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | -| `POST /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | -| `GET /items/:collection/:id/relations/:field/candidates` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | -| `GET /items/:collection/:id/relations/:field/links/:linkId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | -| `PATCH /items/:collection/:id/relations/:field/links/:linkId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | -| `POST /items/:collection/:id/relations/:field/links/to/:targetId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | -| `POST /items/:collection/:id/relations/:field/records` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | -| `POST /items/:collection/commit` | Активный principal; личные read/create/update и поля всех затронутых записей. expectedValues требует чтения обновляемых полей; конфликт откатывает весь commit | `apps/core/src/items/relation-routes.ts` | -| `GET /materialized-views` | Человек-superuser; обнаружение существующих materialized views в public без регистрации/REFRESH; Core/plugin объекты исключены | `apps/core/src/collections/materialized-routes.ts` | -| `POST /materialized-views` | Человек-superuser; атомарное подключение заполненного public MV с поддерживаемыми полями и устойчивым уникальным ключом. Только метаданные отображения/расположения, без SQL/DDL/REFRESH; права другим пользователям не выдаются | `apps/core/src/collections/materialized-routes.ts` | -| `GET /monitoring/browser` | Активная человеческая сессия; только публичный браузерный DSN и разрешённые флаги, без серверного DSN. Сервисные токены не допускаются | `apps/core/src/monitoring/routes.ts` | -| `GET /notifications` | Активная человеческая сессия; собственные уведомления включённых плагинов, доступ к каждой записи проверяется заново | `apps/core/src/notifications/routes.ts` | -| `POST /notifications/:id/read` | Активная человеческая сессия; собственное уведомление и текущий доступ к записи | `apps/core/src/notifications/routes.ts` | -| `POST /notifications/read-all` | Активная человеческая сессия; только собственные уведомления до границы снимка списка | `apps/core/src/notifications/routes.ts` | -| `GET /oauth-apps` | Человек: oauth/read ИЛИ update; superuser bypass | `apps/core/src/oauth/routes.ts` | -| `POST /oauth-apps` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | -| `PUT /oauth-apps/:id` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | -| `POST /oauth-apps/:id/secret` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | -| `GET /oauth-apps/status` | Человек: oauth/read ИЛИ update; superuser bypass | `apps/core/src/oauth/routes.ts` | -| `GET /oauth-interactions/:uid` | Человек; UID, собственная сессия, правила приложения и согласие | `apps/core/src/oauth/interactions.ts` | -| `POST /oauth-interactions/:uid` | Человек; UID, собственная сессия, правила приложения и согласие | `apps/core/src/oauth/interactions.ts` | -| `ALL /oauth/*` | Протокольные endpoints oidc-provider: discovery/JWKS публичны, остальные используют проверки OAuth/OIDC | `apps/core/src/oauth/routes.ts` | -| `POST /oauth/complete/:uid` | Текущий пользователь, подписанные cookies взаимодействия OAuth и совпадающий Origin; идентификатор пользователя должен совпадать с сессией | `apps/core/src/oauth/interactions.ts` | -| `GET /permissions` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/permissions/routes.ts` | -| `POST /permissions` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | -| `DELETE /permissions/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | -| `GET /permissions/:id` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/permissions/routes.ts` | -| `PATCH /permissions/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | -| `GET /permissions/me` | Активный principal; только собственные права | `apps/core/src/permissions/routes.ts` | -| `GET /policies` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/policies/routes.ts` | -| `POST /policies` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | -| `DELETE /policies/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | -| `GET /policies/:id` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/policies/routes.ts` | -| `PATCH /policies/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | -| `DELETE /policies/:id/permissions/:permissionId` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | -| `PUT /policies/:id/permissions/:permissionId` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | -| `PUT /policies/:id/users` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; атомарная замена назначений без изменения себя и superuser | `apps/core/src/policies/routes.ts` | -| `DELETE /policies/:id/users/:userId` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser | `apps/core/src/policies/routes.ts` | -| `PUT /policies/:id/users/:userId` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser | `apps/core/src/policies/routes.ts` | -| `GET /policies/applications` | Человек: policies/read ИЛИ update; каталог приложений с управлением через политики, без секретов | `apps/core/src/policies/routes.ts` | -| `POST /presence` | Активная человеческая сессия; каждый heartbeat проверяет read коллекции/строки или доступ раздела. /files требует files/read либо update у человека, или superuser. Возвращает только ID, имя, аватар и число окон; без email/сессий/значений записи | `apps/core/src/presence/routes.ts` | -| `DELETE /presence/:clientId` | Активная человеческая сессия; удаляет только собственное окно этой сессии, без требования сохраняющегося read | `apps/core/src/presence/routes.ts` | -| `GET /public/files/:id/content` | Без авторизации: только явно опубликованный ready-файл (visibility=public). Приватный/удалённый файл — 404; no-store/CSP/nosniff. Без списка и метаданных. | `apps/core/src/files/public-routes.ts` | -| `GET /ready` | Публичная проверка готовности, без секретов | `apps/core/src/health/readiness.ts` | -| `DELETE /realtime/locks` | Активная человеческая сессия; освобождает только собственную lease с совпадающим clientId, даже после потери read/update | `apps/core/src/realtime/routes.ts` | -| `POST /realtime/locks` | Активная человеческая сессия; read и update указанного поля и строки. Атомарная 30-секундная lease, конфликт 409 FIELD_LOCKED | `apps/core/src/realtime/routes.ts` | -| `GET /realtime/stream` | Активная человеческая сессия через Bearer token или браузерную HttpOnly cookie; scope проверяется теми же collection/row read правилами, что GET /items. Поток повторно проверяет доступ и закрывается при его потере. Presence только для этой сессии | `apps/core/src/realtime/stream.ts` | -| `GET /schema` | Активный пользователь или сервисный аккаунт: доступные коллекции, wire-типы и разрешения на поля. Без записей, defaults и условий политик; private/no-store; accepts scoped CLI schema:read access and includes permission-gated generated plugin model contracts | `apps/core/src/schema/routes.ts` | -| `GET /search` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | -| `GET /service-accounts` | Человек: services/read ИЛИ update; superuser bypass | `apps/core/src/services/routes.ts` | -| `POST /service-accounts` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | -| `GET /service-accounts/:id` | Человек: services/read ИЛИ update; superuser bypass | `apps/core/src/services/routes.ts` | -| `PUT /service-accounts/:id` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | -| `POST /service-accounts/:id/federations` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/federation-routes.ts` | -| `DELETE /service-accounts/:id/federations/:federationId` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/federation-routes.ts` | -| `POST /service-accounts/:id/keys` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | -| `DELETE /service-accounts/:id/keys/:keyId` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | -| `GET /settings/access` | Активный человек; разделы, режим изменения и разрешённый набор назначаемых политик из актуальной БД | `apps/core/src/settings/access-routes.ts` | -| `GET /settings/assistant` | Человек: assistant/read ИЛИ update; superuser bypass | `apps/core/src/settings/routes.ts` | -| `PUT /settings/assistant` | Человек: assistant/update; superuser bypass | `apps/core/src/settings/routes.ts` | -| `GET /settings/assistant/telemetry` | Человек: assistant/read ИЛИ update; superuser bypass | `apps/core/src/settings/routes.ts` | -| `GET /settings/integrations` | Только активная человеческая сессия superuser; безопасные параметры и наличие ключей, без значений секретов | `apps/core/src/integrations/routes.ts` | -| `PUT /settings/integrations/:section` | Только superuser; полный запрет изменения env-группы, revision, атомарное перешифрование при смене защиты | `apps/core/src/integrations/routes.ts` | -| `POST /settings/integrations/:section/test` | Только superuser; credential rate limit, проверка соединения без сохранения настроек | `apps/core/src/integrations/routes.ts` | -| `GET /settings/monitoring` | Только human superuser; локальные метрики текущего экземпляра Core, без DSN и пользовательских данных | `apps/core/src/monitoring/routes.ts` | -| `GET /settings/options/collections` | Человек: policies; только проекция схемы без записей; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | -| `GET /settings/options/policies` | Человек: users ИЛИ policies ИЛИ services; только ID/названия; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | -| `GET /settings/options/users` | Человек: oauth; только ID/email; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | -| `GET /settings/plugins` | Человек: plugins/read ИЛИ update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | -| `GET /settings/plugins/:namespace` | Человек: plugins/read ИЛИ update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | -| `PUT /settings/plugins/:namespace` | Человек: plugins/update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | -| `GET /settings/terms` | Человек: terms/read ИЛИ update; superuser bypass | `apps/core/src/terms/routes.ts` | -| `POST /settings/terms` | Человек: terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | -| `PUT /settings/terms/:id` | Человек: terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | -| `POST /sign/sso/:provider` | Точный Origin AUTH_UI_URL; настроенный провайдер; для link необходима активная сессия. | `apps/core/src/auth/browser/sso.ts` | -| `GET /sign/sso/:provider/callback` | Одноразовые state/PKCE и browser proof; для link проверяется сессия владельца. | `apps/core/src/auth/browser/sso.ts` | -| `GET /system-collections` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `DELETE /system-collections/:name/fields/:field` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `POST /system-collections/:name/fields/:field/configuration` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `PUT /system-collections/:name/fields/:field/configuration` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `GET /system-collections/:name/records` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `GET /system-collections/:name/records/:id` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `PATCH /system-collections/:name/records/:id` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | -| `GET /table-views/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | -| `POST /table-views/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | -| `DELETE /table-views/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | -| `PUT /table-views/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | -| `GET /table-views/:collection/default` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | -| `GET /translations` | Активный пользователь или сервисный аккаунт; схема ограничена доступными вызывающему коллекциями и полями, статические каталоги активных плагинов, без записей и defaults | `apps/core/src/translations/routes.ts` | -| `GET /users` | Человек: users/read или update ИЛИ policies/read или update; superuser bypass | `apps/core/src/auth/user-routes.ts` | -| `POST /users` | Человек: users/update; superuser bypass | `apps/core/src/auth/user-routes.ts` | -| `GET /users/:id/access` | Человек: users/read ИЛИ update; superuser bypass | `apps/core/src/auth/user-routes.ts` | -| `PUT /users/:id/delegation` | Только человек-superuser: явный набор готовых политик, которые пользователь вправе назначать | `apps/core/src/auth/user-routes.ts` | -| `GET /users/:id/extension` | Активный пользователь с users/read и обычными read-правами коллекции | `apps/core/src/auth/profile-extension-routes.ts` | -| `PATCH /users/:id/extension` | Активный пользователь с users/update и обычными read/create/update-правами коллекции | `apps/core/src/auth/profile-extension-routes.ts` | -| `POST /users/:id/invitation` | Человек: superuser ИЛИ users/update; повторное приглашение только в пределах разрешённого набора, без доступа к аккаунтам с собственным делегированием | `apps/core/src/auth/user-routes.ts` | -| `GET /users/:id/profile` | Активный пользователь с users/read; публичные сведения профиля без credentials | `apps/core/src/auth/profile-extension-routes.ts` | -| `PATCH /users/:id/profile` | Активный пользователь с users/update; только поля профиля, без изменения статуса и привилегий | `apps/core/src/auth/profile-extension-routes.ts` | -| `GET /users/:id/profile-display` | Активный человек с users/read или superuser; настроенная проекция профиля целевого пользователя | `apps/core/src/auth/profile-display-routes.ts` | -| `POST /users/:id/recovery` | Свежий человек-superuser; только другой активный обычный пользователь. Одноразовая ссылка на 30 минут, аудит; не делегируется | `apps/core/src/auth/user-routes.ts` | -| `GET /users/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | -| `PATCH /users/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | -| `POST /users/me/avatar` | Собственная человеческая сессия; приватное растровое изображение до 2 MiB, настроенное файловое хранилище и credential rate limit | `apps/core/src/auth/profile-avatar-routes.ts` | -| `GET /users/me/extension` | Собственная человеческая сессия и обычные read-права коллекции; скрытые поля и строки не раскрываются | `apps/core/src/auth/profile-extension-routes.ts` | -| `PATCH /users/me/extension` | Собственная человеческая сессия; read и create/update, обычные правила строк, полей, связей и файлов | `apps/core/src/auth/profile-extension-routes.ts` | -| `GET /users/me/identities` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/sso/routes.ts` | -| `DELETE /users/me/identities/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/sso/routes.ts` | -| `GET /users/me/oauth-apps` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/oauth/consent-routes.ts` | -| `DELETE /users/me/oauth-apps/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/oauth/consent-routes.ts` | -| `GET /users/me/passkeys` | Собственная активная человеческая сессия; только публичные сведения о ключах | `apps/core/src/auth/passkeys/routes.ts` | -| `POST /users/me/passkeys` | Собственный session-bound challenge, свежий вход и WebAuthn verification; до десяти ключей | `apps/core/src/auth/passkeys/routes.ts` | -| `DELETE /users/me/passkeys/:id` | Собственный ключ, свежий человеческий вход; нельзя удалить последний способ входа | `apps/core/src/auth/passkeys/routes.ts` | -| `POST /users/me/passkeys/options` | Собственная активная человеческая сессия не старше пяти минут | `apps/core/src/auth/passkeys/routes.ts` | -| `POST /users/me/password` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | -| `POST /users/me/password/setup` | Собственная свежая человеческая сессия; только если пароля ещё нет | `apps/core/src/auth/profile-routes.ts` | -| `GET /users/me/preferences` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | -| `PATCH /users/me/preferences` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | -| `GET /users/me/profile-display` | Активный человек; только собственные значения заранее настроенных selfVisible путей, без права произвольного чтения коллекций | `apps/core/src/auth/profile-display-routes.ts` | -| `GET /users/me/sessions` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | -| `DELETE /users/me/sessions/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | -| `GET /users/me/table-preferences/:collection` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | -| `PATCH /users/me/table-preferences/:collection` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | -| `PUT /users/me/workspace` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/workspaces/routes.ts` | -| `GET /users/profile-display` | Только human superuser; конфигурация блока профиля | `apps/core/src/auth/profile-display-routes.ts` | -| `PUT /users/profile-display` | Только human superuser; явно разрешает ограниченную проекцию выбранных полей управляющим пользователями и при selfVisible владельцу профиля | `apps/core/src/auth/profile-display-routes.ts` | -| `GET /users/profile-display/sources` | Только human superuser; разрешённые пользовательские поля и пути M2O, без данных записей | `apps/core/src/auth/profile-display-routes.ts` | -| `GET /users/profile-extension` | Активный пользователь с users/read; имя выбранной коллекции и ключа | `apps/core/src/auth/profile-extension-routes.ts` | -| `PUT /users/profile-extension` | Только human superuser; связывает обычную UUID-коллекцию, не выдаёт права и не удаляет данные | `apps/core/src/auth/profile-extension-routes.ts` | -| `GET /users/references` | Человеческая сессия: superuser или чтение раздела users; только ID и подпись пользователя, ограниченные поиск и пагинация; сервисные ключи запрещены | `apps/core/src/auth/user-routes.ts` | -| `GET /workspaces` | Человек; только пространства с доступными коллекциями (superuser видит все) | `apps/core/src/workspaces/routes.ts` | -| `POST /workspaces` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | -| `DELETE /workspaces/:id` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | -| `PUT /workspaces/:id` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | +| `POST /auth/federation-token` | Signed GitLab CI assertion and active federation | `apps/core/src/services/federation-routes.ts` | +| `POST /auth/invitations/accept` | One-time invitation token and new password | `apps/core/src/auth/routes.ts` | +| `POST /auth/invitations/claim` | One-time invitation/recovery token creates a human session. Recovery revokes prior sessions and sign-in methods | `apps/core/src/auth/routes.ts` | +| `POST /auth/login` | Email/password verification with rate limiting | `apps/core/src/auth/routes.ts` | +| `POST /auth/logout` | Refresh token in body or session Bearer token | `apps/core/src/auth/routes.ts` | +| `GET /auth/me` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/routes.ts` | +| `POST /auth/passkeys/login` | One-time challenge, stored passkey signature, exact origin/RP ID, user verification, and active user | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /auth/passkeys/options` | Public five-minute WebAuthn challenge; shared credential rate limit | `apps/core/src/auth/passkeys/routes.ts` | +| `GET /auth/providers` | Public provider IDs and labels | `apps/core/src/auth/sso/routes.ts` | +| `POST /auth/refresh` | Valid refresh token; rotation and replay protection | `apps/core/src/auth/routes.ts` | +| `POST /auth/service-token` | Valid service key | `apps/core/src/services/routes.ts` | +| `POST /auth/setup` | Setup secret and no existing users; rate-limited | `apps/core/src/auth/routes.ts` | +| `GET /auth/setup/status` | Public initial-setup status | `apps/core/src/auth/routes.ts` | +| `POST /auth/sso/:provider/callback` | SSO flow, state/PKCE, and provider-response validation | `apps/core/src/auth/sso/routes.ts` | +| `POST /auth/sso/:provider/start` | Start SSO; linking requires an active user session | `apps/core/src/auth/sso/routes.ts` | +| `GET /collections` | Active principal; accessible collections and fields only | `apps/core/src/collections/routes.ts` | +| `POST /collections` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `DELETE /collections/:name` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/lifecycle-routes.ts` | +| `PUT /collections/:name/display` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/fields` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `DELETE /collections/:name/fields/:field` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/lifecycle-routes.ts` | +| `PATCH /collections/:name/fields/:field` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/fields/:field/configuration` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/fields/:field/configuration` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/fields/:field/impact` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/lifecycle-routes.ts` | +| `PUT /collections/:name/fields/:field/presentation` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/fields/:field/search` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PATCH /collections/:name/folder` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/form` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/impact` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/lifecycle-routes.ts` | +| `DELETE /collections/:name/materialized-view` | Human superuser; dependency-checked catalog disconnection removes metadata/grants and preserves the PostgreSQL object/data | `apps/core/src/collections/materialized-routes.ts` | +| `PATCH /collections/:name/navigation` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/relations` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/relations/:field/search` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PATCH /collections/:name/settings` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/terms` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/terms/routes.ts` | +| `PUT /collections/:name/terms` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/terms/routes.ts` | +| `DELETE /collections/:name/terms/:id` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/terms/routes.ts` | +| `PUT /collections/:name/terms/:id` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/terms/routes.ts` | +| `DELETE /connections/google` | Active human session; deletes own tokens, flows, and proposals and attempts Google grant revocation | `apps/core/src/connections/routes.ts` | +| `GET /connections/google` | Active human session; own connection status without tokens | `apps/core/src/connections/routes.ts` | +| `GET /connections/google/callback` | Result redirect; connection requires active session, one-time state/browser proof, and matching owner. | `apps/core/src/auth/browser/google.ts` | +| `POST /connections/google/callback` | Active human session; own flow, browser proof, state/nonce/PKCE, and unchanged client configuration | `apps/core/src/connections/routes.ts` | +| `POST /connections/google/start` | Active human session; owner's one-time OAuth flow with browser proof and PKCE | `apps/core/src/connections/routes.ts` | +| `DELETE /connections/google/writes/:id` | Active human session; cancel own pending proposal | `apps/core/src/connections/routes.ts` | +| `GET /connections/google/writes/:id` | Active human session; own unexpired proposal and the same active connection | `apps/core/src/connections/routes.ts` | +| `POST /connections/google/writes/:id/confirm` | Active human session; one-time confirmation of own proposal with connection/source revalidation; not an MCP tool | `apps/core/src/connections/routes.ts` | +| `GET /extensions` | Active principal; enabled UI-plugin metadata | `apps/core/src/plugins/ui-routes.ts` | +| `GET /extensions/:namespace/drafts/:id` | Active principal; prepared draft bound to its principal and namespace, with TTL | `apps/core/src/plugins/action-draft-routes.ts` | +| `GET /files` | Human with files/read or update; superuser bypass. Shared team library | `apps/core/src/files/routes.ts` | +| `POST /files` | Human with files/update; superuser bypass. Upload up to 25 MiB, metadata validation, and protection of used references | `apps/core/src/files/routes.ts` | +| `DELETE /files/:id` | Human with files/update; superuser bypass. Upload up to 25 MiB, metadata validation, and protection of used references | `apps/core/src/files/routes.ts` | +| `GET /files/:id` | Active principal: superuser, human with files/read/update, or accessible reference in a readable record field; other files are hidden | `apps/core/src/files/routes.ts` | +| `PATCH /files/:id` | Human with files/update; superuser bypass. Update title/description and private/public visibility of one ready file; publication/revocation is audited. | `apps/core/src/files/routes.ts` | +| `GET /files/:id/content` | Active principal: superuser, human with files/read/update, or accessible reference in a readable record field; other files are hidden | `apps/core/src/files/routes.ts` | +| `GET /files/:id/events` | Human with files/read or update; superuser bypass. Shared team library | `apps/core/src/files/routes.ts` | +| `GET /files/resolve` | Active principal: superuser, human with files/read/update, or accessible reference in a readable record field; other files are hidden | `apps/core/src/files/routes.ts` | +| `GET /filter-presets/:collection` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/items/filter-preset-routes.ts` | +| `POST /filter-presets/:collection` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/items/filter-preset-routes.ts` | +| `DELETE /filter-presets/:collection/:id` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/items/filter-preset-routes.ts` | +| `PUT /filter-presets/:collection/:id` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/items/filter-preset-routes.ts` | +| `POST /folders` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `DELETE /folders/:id` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PATCH /folders/:id` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `PATCH /folders/:id/order` | Human superuser; system and plugin-owned structure has additional protection | `apps/core/src/collections/routes.ts` | +| `GET /health` | Public process liveness check | `apps/core/src/app.ts` | +| `GET /item-events/:collection` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `GET /items/:collection` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `PATCH /items/:collection` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `POST /items/:collection` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `DELETE /items/:collection/:id` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `PATCH /items/:collection/:id` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id/related` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id/relations/:field` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-routes.ts` | +| `PATCH /items/:collection/:id/relations/:field` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-routes.ts` | +| `POST /items/:collection/:id/relations/:field` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-routes.ts` | +| `GET /items/:collection/:id/relations/:field/candidates` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-routes.ts` | +| `GET /items/:collection/:id/relations/:field/links/:linkId` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-link-routes.ts` | +| `PATCH /items/:collection/:id/relations/:field/links/:linkId` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/:id/relations/:field/links/to/:targetId` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/:id/relations/:field/records` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/commit` | Active principal; own read/create/update grants and fields of every affected record. expectedValues requires reading changed fields; conflict rolls back the whole commit | `apps/core/src/items/relation-routes.ts` | +| `GET /materialized-views` | Human superuser; discovers existing public materialized views without registration/REFRESH, excluding Core/plugin objects | `apps/core/src/collections/materialized-routes.ts` | +| `POST /materialized-views` | Human superuser; atomically imports a populated public materialized view with supported fields and a stable unique key. Presentation/location metadata only, no SQL/DDL/REFRESH or grants to others | `apps/core/src/collections/materialized-routes.ts` | +| `GET /monitoring/browser` | Active human session; public browser DSN and permitted flags only, never server DSN. Service tokens rejected | `apps/core/src/monitoring/routes.ts` | +| `GET /notifications` | Active human session; own enabled-plugin notifications, with record access rechecked | `apps/core/src/notifications/routes.ts` | +| `POST /notifications/:id/read` | Active human session; own notification and current record access | `apps/core/src/notifications/routes.ts` | +| `POST /notifications/read-all` | Active human session; own notifications up to the list snapshot boundary | `apps/core/src/notifications/routes.ts` | +| `GET /oauth-apps` | Human with oauth/read or update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `POST /oauth-apps` | Human with oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `PUT /oauth-apps/:id` | Human with oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `POST /oauth-apps/:id/secret` | Human with oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `GET /oauth-apps/status` | Human with oauth/read or update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `GET /oauth-interactions/:uid` | Human; interaction UID, own session, application rules, and consent | `apps/core/src/oauth/interactions.ts` | +| `POST /oauth-interactions/:uid` | Human; interaction UID, own session, application rules, and consent | `apps/core/src/oauth/interactions.ts` | +| `ALL /oauth/*` | oidc-provider protocol endpoints: discovery/JWKS are public; other routes use OAuth/OIDC checks | `apps/core/src/oauth/routes.ts` | +| `POST /oauth/complete/:uid` | Current user, signed OAuth interaction cookies, and matching Origin; user ID must match the session | `apps/core/src/oauth/interactions.ts` | +| `GET /permissions` | Human with policies/read or update; superuser bypass | `apps/core/src/permissions/routes.ts` | +| `POST /permissions` | Human superuser only: edit policy contents and permissions | `apps/core/src/permissions/routes.ts` | +| `DELETE /permissions/:id` | Human superuser only: edit policy contents and permissions | `apps/core/src/permissions/routes.ts` | +| `GET /permissions/:id` | Human with policies/read or update; superuser bypass | `apps/core/src/permissions/routes.ts` | +| `PATCH /permissions/:id` | Human superuser only: edit policy contents and permissions | `apps/core/src/permissions/routes.ts` | +| `GET /permissions/me` | Active principal; own permissions only | `apps/core/src/permissions/routes.ts` | +| `GET /policies` | Human with policies/read or update; superuser bypass | `apps/core/src/policies/routes.ts` | +| `POST /policies` | Human superuser only: edit policy contents and permissions | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id` | Human superuser only: edit policy contents and permissions | `apps/core/src/policies/routes.ts` | +| `GET /policies/:id` | Human with policies/read or update; superuser bypass | `apps/core/src/policies/routes.ts` | +| `PATCH /policies/:id` | Human superuser only: edit policy contents and permissions | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id/permissions/:permissionId` | Human superuser only: edit policy contents and permissions | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/permissions/:permissionId` | Human superuser only: edit policy contents and permissions | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/users` | Human superuser OR policies/update with policy in allowed set; atomic assignment replacement without changing self or superusers | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id/users/:userId` | Human superuser OR policies/update with policy in allowed set; manager cannot change self or superusers | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/users/:userId` | Human superuser OR policies/update with policy in allowed set; manager cannot change self or superusers | `apps/core/src/policies/routes.ts` | +| `GET /policies/applications` | Human with policies/read or update; policy-managed application catalog without secrets | `apps/core/src/policies/routes.ts` | +| `POST /presence` | Active human session; each heartbeat checks collection/row read or section access. Files requires human files/read or update, or superuser. Returns only ID, name, avatar, and window count; no email, sessions, or record values | `apps/core/src/presence/routes.ts` | +| `DELETE /presence/:clientId` | Active human session; remove only its own window without requiring continued read access | `apps/core/src/presence/routes.ts` | +| `GET /public/files/:id/content` | Unauthenticated: explicitly public ready files only. Private/deleted files return 404; no-store/CSP/nosniff. No listing or metadata. | `apps/core/src/files/public-routes.ts` | +| `GET /ready` | Public readiness check without secrets | `apps/core/src/health/readiness.ts` | +| `DELETE /realtime/locks` | Active human session; release only own lease with matching clientId, even after losing read/update | `apps/core/src/realtime/routes.ts` | +| `POST /realtime/locks` | Active human session; read/update on the specified field and row. Atomic 30-second lease; 409 FIELD_LOCKED on conflict | `apps/core/src/realtime/routes.ts` | +| `GET /realtime/stream` | Active human session through Bearer or HttpOnly cookie; scope uses the same collection/row read rules as GET /items. Stream rechecks access and closes when lost. Presence belongs only to this session | `apps/core/src/realtime/stream.ts` | +| `GET /schema` | Active human or service account: accessible collections, wire types, field grants. No records, defaults, or policy conditions; private/no-store. Accepts scoped CLI schema:read and includes permission-gated generated plugin model contracts | `apps/core/src/schema/routes.ts` | +| `GET /search` | Active principal; collection action/fields, related data, and history checked separately | `apps/core/src/items/routes.ts` | +| `GET /service-accounts` | Human with services/read or update; superuser bypass | `apps/core/src/services/routes.ts` | +| `POST /service-accounts` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/routes.ts` | +| `GET /service-accounts/:id` | Human with services/read or update; superuser bypass | `apps/core/src/services/routes.ts` | +| `PUT /service-accounts/:id` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/routes.ts` | +| `POST /service-accounts/:id/federations` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/federation-routes.ts` | +| `DELETE /service-accounts/:id/federations/:federationId` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/federation-routes.ts` | +| `POST /service-accounts/:id/keys` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/routes.ts` | +| `DELETE /service-accounts/:id/keys/:keyId` | Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations | `apps/core/src/services/routes.ts` | +| `GET /settings/access` | Active human; view/edit sections and allowed policy-assignment set from current database state | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/assistant` | Human with assistant/read or update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `PUT /settings/assistant` | Human with assistant/update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `GET /settings/assistant/telemetry` | Human with assistant/read or update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `GET /settings/integrations` | Active human superuser only; safe parameters and key-presence flags, without secret values | `apps/core/src/integrations/routes.ts` | +| `PUT /settings/integrations/:section` | Superuser only; environment groups cannot change; revision checks and atomic re-encryption on protection changes | `apps/core/src/integrations/routes.ts` | +| `POST /settings/integrations/:section/test` | Superuser only; credential rate limit; test without saving settings | `apps/core/src/integrations/routes.ts` | +| `GET /settings/monitoring` | Human superuser only; current-instance local metrics, without DSNs or user data | `apps/core/src/monitoring/routes.ts` | +| `GET /settings/options/collections` | Human with policies/read or update; schema projection without records | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/options/policies` | Human with users, policies, or services read/update; IDs and names only | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/options/users` | Human with oauth/read or update; IDs and emails only | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/plugins` | Human with plugins/read or update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `GET /settings/plugins/:namespace` | Human with plugins/read or update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `PUT /settings/plugins/:namespace` | Human with plugins/update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `GET /settings/terms` | Human with terms/read or update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `POST /settings/terms` | Human with terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `PUT /settings/terms/:id` | Human with terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `POST /sign/sso/:provider` | Exact AUTH_UI_URL Origin; configured provider; linking requires an active session. | `apps/core/src/auth/browser/sso.ts` | +| `GET /sign/sso/:provider/callback` | One-time state/PKCE and browser proof; linking checks owner session. | `apps/core/src/auth/browser/sso.ts` | +| `GET /system-collections` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `DELETE /system-collections/:name/fields/:field` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `POST /system-collections/:name/fields/:field/configuration` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `PUT /system-collections/:name/fields/:field/configuration` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `GET /system-collections/:name/records` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `GET /system-collections/:name/records/:id` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `PATCH /system-collections/:name/records/:id` | Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected | `apps/core/src/system-collections/routes.ts` | +| `GET /table-views/:collection` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/preferences/table-view-routes.ts` | +| `POST /table-views/:collection` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/preferences/table-view-routes.ts` | +| `DELETE /table-views/:collection/:id` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/preferences/table-view-routes.ts` | +| `PUT /table-views/:collection/:id` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/preferences/table-view-routes.ts` | +| `GET /table-views/:collection/default` | Human with collection access; personal owner, shared views editable by superusers | `apps/core/src/preferences/table-view-routes.ts` | +| `GET /translations` | Active human or service account; schema limited to accessible collections/fields, static enabled-plugin catalogs, no records/defaults | `apps/core/src/translations/routes.ts` | +| `GET /users` | Human with users/read or update OR policies/read or update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `POST /users` | Human with users/update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/access` | Human with users/read or update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `PUT /users/:id/delegation` | Human superuser only: explicit allowed set of existing policies for a user's assignments | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/extension` | Active user with users/read and ordinary collection read grants | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/:id/extension` | Active user with users/update and ordinary collection read/create/update grants | `apps/core/src/auth/profile-extension-routes.ts` | +| `POST /users/:id/invitation` | Human superuser OR users/update; re-invitation within the allowed set only, excluding accounts with their own delegation | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/profile` | Active user with users/read; public profile information without credentials | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/:id/profile` | Active user with users/update; profile fields only, without status/privilege changes | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/:id/profile-display` | Active human with users/read or superuser; configured target-user profile projection | `apps/core/src/auth/profile-display-routes.ts` | +| `POST /users/:id/recovery` | Fresh human superuser; another active ordinary user only. One-time 30-minute recovery link, audited and not delegated | `apps/core/src/auth/user-routes.ts` | +| `GET /users/me` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/profile-routes.ts` | +| `PATCH /users/me` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/profile-routes.ts` | +| `POST /users/me/avatar` | Own human session; private raster image up to 2 MiB, configured storage, and credential rate limit | `apps/core/src/auth/profile-avatar-routes.ts` | +| `GET /users/me/extension` | Own human session and ordinary collection read grants; hidden rows/fields remain hidden | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/me/extension` | Own human session; read plus create/update, with ordinary row/field/relation/file rules | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/me/identities` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/sso/routes.ts` | +| `DELETE /users/me/identities/:id` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/sso/routes.ts` | +| `GET /users/me/oauth-apps` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/oauth/consent-routes.ts` | +| `DELETE /users/me/oauth-apps/:id` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/oauth/consent-routes.ts` | +| `GET /users/me/passkeys` | Own active human session; public passkey information only | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/passkeys` | Own session-bound challenge, recent sign-in, WebAuthn verification; up to ten passkeys | `apps/core/src/auth/passkeys/routes.ts` | +| `DELETE /users/me/passkeys/:id` | Own passkey, recent human sign-in; cannot remove the last sign-in method | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/passkeys/options` | Own active human session authenticated within five minutes | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/password` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/profile-routes.ts` | +| `POST /users/me/password/setup` | Own fresh human session; only if no password exists | `apps/core/src/auth/profile-routes.ts` | +| `GET /users/me/preferences` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/preferences/routes.ts` | +| `PATCH /users/me/preferences` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/preferences/routes.ts` | +| `GET /users/me/profile-display` | Active human; own values from preconfigured selfVisible paths only, without arbitrary collection reads | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/me/sessions` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/profile-routes.ts` | +| `DELETE /users/me/sessions/:id` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/auth/profile-routes.ts` | +| `GET /users/me/table-preferences/:collection` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/preferences/routes.ts` | +| `PATCH /users/me/table-preferences/:collection` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/preferences/routes.ts` | +| `PUT /users/me/workspace` | Human, own profile/resource only; workspace additionally checks visibility | `apps/core/src/workspaces/routes.ts` | +| `GET /users/profile-display` | Human superuser only; profile-block configuration | `apps/core/src/auth/profile-display-routes.ts` | +| `PUT /users/profile-display` | Human superuser only; explicitly grants a bounded projection to user managers and, with selfVisible, the profile owner | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/profile-display/sources` | Human superuser only; permitted custom fields and M2O paths without record data | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/profile-extension` | Active user with users/read; selected collection/key names | `apps/core/src/auth/profile-extension-routes.ts` | +| `PUT /users/profile-extension` | Human superuser only; binds an ordinary UUID collection without granting access or deleting data | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/references` | Human session: superuser or users-section read; only user IDs/labels with bounded search/pagination; service keys forbidden | `apps/core/src/auth/user-routes.ts` | +| `GET /workspaces` | Human; workspaces containing accessible collections only (superusers see all) | `apps/core/src/workspaces/routes.ts` | +| `POST /workspaces` | Human superuser | `apps/core/src/workspaces/routes.ts` | +| `DELETE /workspaces/:id` | Human superuser | `apps/core/src/workspaces/routes.ts` | +| `PUT /workspaces/:id` | Human superuser | `apps/core/src/workspaces/routes.ts` | diff --git a/docs/reference/sdk-guide.md b/docs/reference/sdk-guide.md index a93b4f6..055b92a 100644 --- a/docs/reference/sdk-guide.md +++ b/docs/reference/sdk-guide.md @@ -2,15 +2,13 @@ # @asmblyr-collaborative/sdk -Типизированный HTTP-клиент Asmblyr для браузера и Node.js 22+. -Каждая операция вызывает основной API. В пакете нет H3, Knex, PostgreSQL, -компилятора TypeScript или загрузчика плагинов во время исполнения. -Для разработки расширений используется отдельный `@asmblyr-collaborative/kit`. +Typed Collaborative HTTP client for browsers and Node.js 22+. Every operation calls the main API. Runtime dependencies do not include H3, Knex, PostgreSQL, the TypeScript compiler, or a plugin loader. Use the separate @asmblyr-collaborative/kit to develop extensions. -## Профиль пользователя + -Собственный профиль доступен с пользовательским access token. Статус, права, -пароли и системные даты нельзя менять через профиль. Все новые поля необязательны. +## User profile + +A human access token can access its own profile. Profile updates cannot change status, permissions, passwords, or system timestamps. New profile fields are optional. ```ts await client.users.updateMe({ @@ -22,30 +20,22 @@ await client.users.updatePreferences({ timezone: "Asia/Yekaterinburg" }); const { data: me } = await client.users.me(); ``` -Дополнительные поля потребитель настраивает в обычной UUID-коллекции и выбирает -её в настройках пользователей. `asm connect` / `asm generate` включает её поля -в схему проекта. Для клиента с этой схемой: +Configure extra fields in an ordinary UUID collection and select it in user settings. asm connect / asm generate includes its fields in the project schema: ```ts const { data: profile } = await client.users.extension("user_profiles"); await client.users.saveExtension("user_profiles", { bio: "Hello" }); ``` -`saveExtension` создаёт запись с ID текущего пользователя или обновляет её. -Для первого сохранения нужны `create` и `read`, затем `update` и `read`. -Действуют обычные правила полей и строк, права на связи и файлы. Передайте только -поля сгенерированной схемы; имя коллекции должно совпадать с выбранным расширением. -Скрытые поля могут отсутствовать в результате, а `data` — быть `null`. -После первого сохранения запись и её связи доступны через обычный `client.items` -и fluent API; произвольное создание профиля через `items.create` запрещено. +`saveExtension` creates/updates the current user's ID. First save needs create/read; later changes need update/read. Normal row/field, relationship, and file rules apply. Supply generated-schema fields only, and use the selected extension's collection name. Hidden fields may be absent and data may be null. After initial creation, ordinary items/fluent APIs edit its records and relationships. Arbitrary profile creation through items.create is forbidden. + + -## Подключение клиента +## Connect a client -Личные уведомления доступны через `client.notifications.list()`, `.read(id)` и -`.readAll(result.readBefore)`. Они требуют человеческой сессии; сервисные ключи -не дают доступ к личным входящим. Core хранит последние 200 событий пользователя. +Personal notifications use client.notifications.list(), .read(id), and .readAll(result.readBefore). They require a human session; service keys cannot access an inbox. Core retains the latest 200 events per user. -Предварительная версия устанавливается с тегом `beta`: +Install prereleases with the beta tag: ```sh npm install @asmblyr-collaborative/sdk@beta @@ -55,7 +45,7 @@ npm install --save-dev @asmblyr-collaborative/cli@beta ```ts import { createClient } from "@asmblyr-collaborative/sdk"; -// В браузере: сессия админки остаётся в HttpOnly cookies. +// Browser sessions remain in HttpOnly cookies. const client = createClient({ baseUrl: "/api" }); const records = await client.items.list("articles", { @@ -69,11 +59,9 @@ const record = await client.items.get("articles", 4); const profile = await client.users.me(); ``` -`baseUrl` — корень API, например `http://localhost:3001` для прямого доступа -к Core или `http://localhost:3000/api` для браузерной админки. Префикс `/api` -автоматически не добавляется. Путь `/api` без origin работает в браузере. +`baseUrl` is the API root: `http://localhost:3001` for direct Core or `http://localhost:3000/api` on the admin domain. The SDK does not add /api automatically. A relative /api works in browsers. -Для прямых запросов к Core передайте access token: +Direct Core requests can supply an access token: ```ts const client = createClient({ @@ -83,119 +71,85 @@ const client = createClient({ }); ``` -`accessToken` может быть строкой или асинхронной функцией. Функция вызывается для -каждого запроса. Поддерживаются access tokens пользователя и сервисного аккаунта; -права проверяет Core. `/users/me` предназначен для пользователя. -SDK не хранит токены в браузере, не выполняет вход и не обновляет токены сам. -В админке браузерная сессия хранится в HttpOnly cookie, которую проверяет Core. - -## Реализованные методы - -| Метод | HTTP | Ответ | -| ------------------------------------------------ | -------------------------------- | -------------------------- | -| `items.list(collection, options?, request?)` | `GET /items/:collection` | `{ data, labels, page }` | -| `items.get(collection, id, options?, request?)` | `GET /items/:collection/:id` | `{ data, label }` | -| `users.me(request?)` | `GET /users/me` | `{ data: CurrentUser }` | -| `items.create(collection, values, request?)` | `POST /items/:collection` | `{ data: row \| null }` | -| `items.update(collection, id, values, request?)` | `PATCH /items/:collection/:id` | `{ data: row \| null }` | -| `items.delete(collection, id, request?)` | `DELETE /items/:collection/:id` | `void` (204) | -| `items.commit(collection, draft, request?)` | `POST /items/:collection/commit` | `{ data: { id: string } }` | - -Остальные системные ресурсы и массовые операции пока не включены. -Прямого доступа к базе данных и переключения actor/superuser нет. - -`list` поддерживает `fields`, `page`, `limit`, `sort`, `direction`, `q`, `filter`. -SDK сериализует фильтр и query, Core проверяет значения, права и ограничения. -Фильтр — группа `{ logic: "and" | "or", children: [...] }`, значения условий — -строки или массивы строк. Поиск и фильтры по связям используют правила основного API. -Для списка предел страницы — 100 записей. `page.total` остаётся строкой. -Core дополнительно применяет условия permissions; SDK не передаёт доверенный -контекст. В одной странице условно разрешённые поля могут отсутствовать у части -записей. См. [права и ограничения](../../docs/features/access.md). - -Без `fields` выдаются доступные поля. `fields: []` отправляется как `fields=` -и возвращает только первичный ключ. Ключ всегда добавляет Core. Явно выбранное -закрытое поле приводит к 403. Названия коллекций и полей — технические. -Подписи `label`/`labels` могут использовать другие разрешённые поля. - -Большие целые ID передавайте строками. SDK отклоняет небезопасные JS-числа. -Строковые ключи кодируются как один сегмент URL; ключи `.` и `..` не поддерживаются, -поскольку HTTP-клиенты нормализуют такие сегменты. Даты HTTP-ответов — ISO-строки, -значения `bigserial` — строки. SDK не преобразует их в `Date` или `number`. - -## Запись и сохранение связей +`accessToken` may be a string or async function called per request. Human and service tokens are supported; Core checks permissions. `users.me` requires a human. SDK does not store browser tokens, sign in, or refresh tokens itself. Admin sessions use HttpOnly cookies validated by Core. + + + +## Available methods + +| Method | HTTP | Response | +| ---------------------------------------------- | ------------------------------ | ------------------------ | +| items.list(collection, options?, request?) | GET /items/:collection | { data, labels, page } | +| items.get(collection, id, options?, request?) | GET /items/:collection/:id | { data, label } | +| users.me(request?) | GET /users/me | { data: CurrentUser } | +| items.create(collection, values, request?) | POST /items/:collection | { data: row or null } | +| items.update(collection, id, values, request?) | PATCH /items/:collection/:id | { data: row or null } | +| items.delete(collection, id, request?) | DELETE /items/:collection/:id | void (204) | +| items.commit(collection, draft, request?) | POST /items/:collection/commit | { data: { id: string } } | + +This is the central record API, not complete coverage of every system resource or bulk operation. There is no direct database access or actor/superuser switching. + +List supports fields, page, limit, sort, direction, order, q, and filter. SDK serializes query/filter values; Core validates access and limits. Filters are logic:and/or groups with children; condition values are strings or string arrays. Relationship search/filtering follows the main API. Pages are capped at 100, and page.total remains a string. + +Core additionally applies permission conditions; SDK cannot provide trusted context. Conditionally permitted fields may be missing from some rows. See [access rules](../features/access.md). + +Omitting fields returns permitted fields. Fields:[] sends fields= and returns only the primary key, always added by Core. Explicitly requesting a closed field yields 403. Collection/field names are technical identifiers. Label/labels may use other permitted fields. + +Pass large integer IDs as strings. Unsafe JavaScript numbers are rejected. String IDs are encoded as one URL segment; "." and ".." are unsupported because HTTP clients normalize them. HTTP dates are ISO strings and bigserial values are strings; SDK does not convert them to Date/number. + + + +## Writes and relationships ```ts -const created = await client.items.create("articles", { title: "Статья" }); -await client.items.update("articles", 4, { title: "Новое название" }); +const created = await client.items.create("articles", { title: "Article" }); +await client.items.update("articles", 4, { title: "New title" }); await client.items.delete("articles", 4); -// Одна транзакция для основной записи и связанных изменений. +// One transaction for the root record and related changes. await client.items.commit("articles", { id: "4", - values: { title: "Статья с категорией" }, - references: { category_id: { values: { title: "Новая категория" } } }, + values: { title: "Article with a category" }, + references: { category_id: { values: { title: "New category" } } }, }); ``` -Core применяет права create/update/delete, ограничения полей, проверки связей, -валидацию и историю изменений. `create`/`update` возвращают только читаемые поля; -`{ data: null }` означает успешную запись без права чтения, а не ошибку. -`delete` разрешается на уровне записи, без списка удаляемых полей. +Core enforces create/update/delete, field constraints, related-record access, validation, and history. Create/update return readable fields only; data:null means a successful write without read access. Delete authorizes the record, not a field list. -Каждый вызов — отдельная транзакция. Последовательность `create` и `update` -не становится одной транзакцией. Для черновика со связями используется `commit`: -ошибка откатывает все его изменения и историю. Существующие записи в черновике -требуют права чтения; обычный `update` допускает право записи без чтения. +Each call is a separate transaction. Sequential create/update calls do not become atomic. Commit saves a related draft and rolls back all changes/history on failure. Existing records in a draft require read access; ordinary update can work with write-only grants. -`ItemCommitDraft` описывает `references`, `relations` (attach/detach/create/links) -и `records` — изменения существующих записей. Core ограничивает черновик -100 изменениями и глубиной 5. `commit` возвращает только ID корневой записи, -в том числе при создании без права чтения. UI-поля preview/label/key не передаются. +ItemCommitDraft contains references, relations (attach/detach/create/links), and records for existing changes. Limits: 100 changes and depth 5. Commit returns only the root ID, including creation without read access. UI-only preview/label/key values are not sent. -Для защиты от перезаписи передайте исходные значения изменяемых полей: +For overwrite protection, supply original values of modified fields: ```ts const previous = await client.items.get("articles", "4"); await client.items.commit("articles", { id: "4", - values: { title: "Моя правка" }, + values: { title: "My edit" }, expectedValues: { title: previous.data.title }, }); ``` -При несовпадении Core возвращает `ApiError` со `status: 409` и -`code: "ITEM_CHANGED"`; весь commit откатывается. Сохраните локальный черновик, -прочитайте актуальные данные и предложите пользователю выбрать значение. -Автоматически повторять запрос с новым исходным значением нельзя: это заменит -чужую правку. Изменения разных полей не конфликтуют; уже применённое такое же -значение считается успешным. Для вложенных записей задайте их собственные -`expectedValues`, включая обновляемые FK. Проверка требует read/update каждого -сравниваемого поля. Пропуск параметра и обычный `items.update` сохраняют -прежнее безусловное поведение. JSON и массивы сравниваются целиком. +A mismatch returns ApiError with status:409 and code:ITEM_CHANGED and rolls back the commit. Keep the draft, fetch current data, and let the user choose. Do not automatically retry with a new baseline: that would overwrite another person's change. + +Different-field edits do not conflict; an already-applied identical value succeeds. Nested records need their own expectedValues, including modified foreign keys. Every compared field needs read/update. Omitting expectedValues or using items.update retains unconditional behavior. JSON/arrays compare as whole values. -## Участники страницы + + +## Page participants ```ts -const clientId = crypto.randomUUID(); // одно открытое окно +const clientId = crypto.randomUUID(); // one open window const scope = { kind: "record", collection: "articles", id: "4" } as const; const { data } = await client.presence.touch({ clientId, scope }); -// data.participants: ID, имя, аватар, число окон, self; data.total +// data.participants: ID, name, avatar, window count, self; data.total await client.presence.leave(clientId); ``` -`touch` продлевает отметку на 30 секунд и возвращает до 50 пользователей; -текущий пользователь идёт первым, окна одного человека объединяются. Повторяйте -вызов, пока окно открыто: этот HTTP API требует самостоятельного таймера. -Realtime API ниже обновляет присутствие сам. SDK сам не создаёт -таймеры. `leave` удаляет только окно текущей человеческой сессии и безопасен при -повторении. До 32 активных окон на одну сессию; превышение возвращает 429. +Touch extends a 30-second lease and returns up to 50 users, current user first, merging windows of one human. Repeat while the window is open: this legacy HTTP API has no SDK-managed timer. Realtime below maintains presence automatically. -Область — разрешённая `page`, `collection` либо `record`; каждый `touch` заново -проверяет права просмотра. Пользовательские поля, email, токены и ID сессий не -возвращаются. Сервисные principal не участвуют. При недоступной сети индикатор -может отставать до истечения отметки; присутствие не блокирует запись и не -показывает редактируемое поле. +Leave removes only the current human session's window and is idempotent. Sessions support up to 32 active windows; excess returns 429. Scopes are accessible page, collection, or record; each touch rechecks read access. Responses omit custom fields, email, tokens, and session IDs. Services do not participate. Network loss may leave presence until expiry; presence does not lock writes or identify active fields. ## Collaborative Live @@ -205,7 +159,7 @@ const unsubscribe = live.subscribe( { kind: "record", collection: "articles", id: "4" }, (event) => { if (event.type === "record.updated") { - // Перечитайте запись через items.get; payload не содержит значения полей. + // Reread with items.get; events contain no field values. void client.items.get(event.payload.collection, event.payload.recordId); } }, @@ -218,22 +172,20 @@ const lock = { clientId: crypto.randomUUID(), }; await live.locks.acquire(lock); -// Пока редактор открыт: await live.locks.refresh(lock) не реже 30 секунд. +// While editing, refresh the lock before its 30-second lease expires. await live.locks.release(lock); unsubscribe(); offState(); live.close(); ``` -`subscribe` работает для page, collection и record. Состояния — `connecting`, -`connected`, `reconnecting`, `offline`. SDK повторяет подписку с backoff и -jitter. После переподключения он отправляет `collection.changed`, чтобы клиент -перечитал данные. Повторные события с тем же ID в потоке пропускаются. Через -админку используется HttpOnly cookie; внешнему API-клиенту нужен пользовательский -Bearer token. Сервисный ключ для realtime не подходит. Блокировки — временный -UX-сигнал, а `items.commit` с `expectedValues` остаётся защитой записи. +Subscribe supports page, collection, and record scopes. States: connecting, connected, reconnecting, offline. SDK resubscribes with backoff/jitter and emits collection.changed after reconnect so clients reread. Duplicate event IDs within a stream are ignored. + +Admin browsers use HttpOnly cookies; external clients need human Bearer tokens. Service keys are unsupported. Locks are temporary UX signals; items.commit with expectedValues remains the write safeguard. -## Типизация коллекций + + +## Collection typing ```ts interface Schema { @@ -252,35 +204,19 @@ const result = await client.items.list("articles", { // result.data[0].title: string | undefined ``` -TypeScript проверяет имя коллекции, выбор и сортировку полей; возвращаемая -проекция учитывает `fields`. Поля конкретной схемы остаются опциональными: -их наличие зависит от прав. Неявно добавленный первичный ключ не выводится -из имени `id`: у коллекции может быть другой ключ. Чтобы получить его в типе -проекции, перечислите его в `fields`. - -Без `Schema` работает динамический режим со словарём JSON-значений. -В своей схеме задавайте `date` как `string` (`YYYY-MM-DD`), обычный `bigint` -как `string` с точным целым значением, а `integer` с вариантами — как `number` -или числовой union. SDK передаёт эти значения без преобразования в `Date`/`Number`. -Связанные пути в фильтрах пока проверяются Core во время запроса. -Схему можно описать вручную или получить из Core через CLI `asm connect`: -подробнее — [генерация типов и fluent-запросы](#generated-types-fluent-queries-and-plugin-methods). -SDK не валидирует структуру ответов во время исполнения; текущие права и -ограничения данных проверяет Core. - -При вручную описанной схеме данные `create`/`update` и корневые `values` в `commit` типизируются как -`Partial`: проверяются имена, типы и nullable из вашей схемы. -Обязательность при создании, значения по умолчанию, неизменяемые ключи и системные -поля проверяет Core по метаданным. Тип строки сам по себе их не описывает. -Сгенерированная схема отдельно описывает чтение, создание и обновление, включая -обязательные поля и доступные действия. Вложенные черновики пока используют -JSON-контракт без вывода типов связанных коллекций. - -`CurrentUser`, контракты записей, страниц, фильтров и ошибок переэкспортируются -из `@asmblyr-collaborative/contracts`. Core использует этот же пакет. Строки БД, хеши паролей -и внутренние модели Core не являются публичными типами SDK. - -## Ошибки и отмена +TypeScript checks collection names, selection, and sorting; projections reflect fields. Schema properties remain optional because access can hide them. A primary key is not inferred from the name id; collections may use another key. Include it explicitly in fields to have it in the projected type. + +Without Schema, the client uses dynamic JSON dictionaries. Model date as string (YYYY-MM-DD), ordinary bigint as an exact integer string, and integer choices as number or numeric unions. SDK never converts these to Date/Number. Core validates related filter paths at request time. + +Write a schema manually or obtain it with asm connect; see [generated types and fluent queries](#generated-types-fluent-queries-and-plugin-methods). SDK does not validate response structure at runtime; Core enforces current permissions and constraints. + +Manual schemas type create/update and root commit values as Partial<Schema[collection]>, checking names, types, and nullability. Core metadata still checks required create fields, defaults, immutable keys, and system fields. Generated schemas separately model read/create/update with required fields and available actions. Nested drafts still use the JSON contract without inferred related-collection types. + +CurrentUser and record/page/filter/error contracts are re-exported from @asmblyr-collaborative/contracts, also used by Core. Database rows, password hashes, and internal models are not public SDK types. + + + +## Errors and cancellation ```ts import { ApiError } from "@asmblyr-collaborative/sdk"; @@ -302,28 +238,17 @@ try { } ``` -HTTP-ошибка становится `ApiError`. Ошибки сети и отмены остаются исходными -ошибками `fetch`; локально некорректный путь или ID — `TypeError`. -Таймаут по умолчанию — 10 секунд, включая чтение тела ответа; `timeoutMs: 0` -отключает его. `request.signal` позволяет отменить конкретный запрос. -`request.timeoutMs` переопределяет таймаут одного вызова; редактор админки передаёт -`0` для commit, не ограничивая длительность запроса таймаутом SDK. -SDK не повторяет запросы автоматически и отклоняет HTTP-редиректы. -Ответы запрашиваются с `cache: "no-store"`. +HTTP failures become ApiError. Network/cancellation errors retain their fetch errors; locally invalid paths/IDs throw TypeError. Default timeout is ten seconds including body reads; timeoutMs:0 disables it. Request.signal cancels one call; request.timeoutMs overrides its timeout. The admin uses zero for commits so SDK does not limit save duration. + +Requests are never retried automatically, HTTP redirects are rejected, and fetch uses cache:no-store. Cancellation/connection failure does not prove rollback of a submitted write: inspect the result before retrying. -Отмена или ошибка соединения не подтверждает откат уже отправленной записи: -сервер мог успеть сохранить данные. Перед повтором проверьте результат. +Custom fetch, headers, and credentials are supported. Credentials defaults to same-origin; SDK does not configure server CORS. On servers, create clients per user/request to avoid sharing tokens across users. -Можно передать `fetch`, `headers` и `credentials` для собственного HTTP-окружения. -По умолчанию `credentials: "same-origin"`; SDK не настраивает CORS сервера. -На сервере создавайте клиент для конкретного пользователя/запроса, чтобы не -разделять его токен между пользователями. + -## Сборка и проверки +## Build and checks -Проверка установки архивов вне workspace: `pnpm packages:check`. -После выпуска `pnpm packages:check --registry` проверяет установку той же версии -из npm без локальных алиасов зависимостей. +Run pnpm packages:check to verify tarball installation outside the workspace. After publishing, pnpm packages:check --registry verifies the same version from npm without local aliases. ```sh pnpm --filter @asmblyr-collaborative/sdk build @@ -332,10 +257,11 @@ pnpm --filter @asmblyr-collaborative/sdk test node scripts/test.mjs core-sdk ``` -Интеграционный набор запускает Core на временном HTTP-порту с отдельной тестовой -БД и сравнивает SDK с вызовом через kit, включая ограничения прав. +Integration tests start Core on a temporary port with a disposable database and compare SDK/Kit behavior, including permissions. -## Переводы + + +## Translations ```ts const { data } = await client.translations.get("en"); @@ -343,17 +269,15 @@ console.log(data.core["appearance.ocean"]); console.log(data.schema.articles?.fields.title?.label); ``` -`GET /translations?locale=ru|en` (в админке `/api/translations`) требует -активной сессии или сервисного ключа. Ответ версии 1 содержит язык, -`fallbackLocale: "ru"`, плоский каталог `core`, каталоги активных `plugins` -по namespace и подписи доступной `schema`. Схема использует права каталога -коллекций, включая поля создания/изменения; это не разрешение читать записи. -Ответ не содержит записей, значений по умолчанию или настроек плагинов. -Пропуск языка выбирает русский; неподдержанный язык возвращает 400. +GET /translations?locale=ru|en (or /api/translations on the admin domain) requires an active human or service credential. Version 1 returns locale, fallbackLocale:ru, flat core strings, active plugin catalogs by namespace, and accessible schema labels. + +Schema visibility follows the collection catalog, including create/update metadata, without granting record reads. Responses contain no records, defaults, or plugin settings. Omitted locale defaults to Russian; unsupported locales return 400. + + ## Generated types, fluent queries and plugin methods -[CLI](./cli-guide.md) connects through the admin browser approval screen, or +[CLI](cli-guide.md) connects through the admin browser approval screen, or uses an API token from the environment/stdin. It saves no credentials: ```sh @@ -396,10 +320,10 @@ The example assumes those fields exist. Aliases derive from technical collection names, independent of translated labels. Query builders are immutable. where calls combine with AND; predicates support and/or groups. exec returns rows; result returns the existing ItemListResult envelope. orderBy supports one field, matching -the Core API. `search("корм")` selects relevance unless a field order was already +the Core API. `search("feed")` selects relevance unless a field order was already explicitly chosen. `orderBy` selects field order; `orderByRelevance()` restores relevance and keeps the chosen field as a tie-breaker. The low-level list API -accepts `{ q: "корм", order: "relevance" }`, and `page.order` reports the effective +accepts `{ q: "feed", order: "relevance" }`, and `page.order` reports the effective mode. Relevance is calculated by Core before pagination using readable searchable fields and their configured priority, with exact, word-prefix, whole-word and substring matches. Without `q`, Core uses field order. @@ -432,4 +356,4 @@ asm schema check verifies hashes and detects remote contract changes. --offline compares generated source with the saved snapshot; asm generate recreates source from it. Names, choices and plugin annotations can disclose project structure; commit generated files only when appropriate. Npm publication remains a separate -release action. See [package preparation](./packages.md). +release action. See [package preparation](packages.md). diff --git a/docs/ru/architecture/realtime.md b/docs/ru/architecture/realtime.md new file mode 100644 index 0000000..f23332b --- /dev/null +++ b/docs/ru/architecture/realtime.md @@ -0,0 +1,41 @@ + + +# Архитектура Collaborative Live + +SSE выбран для односторонних уведомлений: браузер обращается к Core через `/api`, +а получение и освобождение locks остаётся обычным HTTP-запросом. +Отдельный WebSocket transport не требуется для этого обмена. + +```mermaid +flowchart TD + UI[UI и SDK] -->|SSE с cookie или Bearer| Gateway[Core realtime routes] + Gateway --> Auth[Access и row permissions] + Gateway --> Presence[(asmblyr_presence)] + Gateway --> Locks[(asmblyr_field_locks)] + API[HTTP / Kit writes] --> Domain[Items services] + Domain --> Events[(asmblyr_item_events)] + Events -->|pg_notify после commit| Bus[PostgresRealtimeBus] + Bus --> Gateway +``` + +`recordItemEvent` записывает историю и публикует минимальный envelope внутри +той же транзакции. `LISTEN` использует отдельное соединение; переподключение +слушателя закрывает SSE-потоки, чтобы клиенты перечитали данные. В памяти Core +остаются только подписчики текущего процесса. `InMemoryRealtimeBus` служит для +изолированных тестов; PostgreSQL bus работает без дополнительного Redis. + +Подписка ограничена одним scope (`page`, `collection`, `record`) и одним +clientId. Gateway повторно загружает права перед выдачей события. Коллекция +получает только invalidation, запись — metadata события после проверки строки. +Число соединений ограничено существующим лимитом 32 окон на сессию. +SSE heartbeat не обращается к БД; продление presence и прав выполняется раз в +20 секунд и отправляет клиенту актуальный состав участников. Отставший сокет +закрывается при переполнении буфера. При завершении Core хук `preClose` закрывает +активные SSE-потоки до остановки HTTP-сервера. + +Field locks лежат в Core-owned таблице с составным ключом +`(collection_id, record_id, field)`. `INSERT ... ON CONFLICT ... WHERE` +атомарно разрешает обновление своему editor или перехват истёкшей lease. +Фоновая очистка удаляет просроченные блокировки и публикует `field.unlocked`. +Миграция только добавляет таблицу; откат удаляет временные leases, а не данные +пользователя. diff --git a/docs/ru/assets/brand/README.md b/docs/ru/assets/brand/README.md new file mode 100644 index 0000000..efc251e --- /dev/null +++ b/docs/ru/assets/brand/README.md @@ -0,0 +1,29 @@ + + +# Фирменные материалы Collaborative + +Публичное название — Collaborative. Символ объединяет четыре петли вокруг +квадратного просвета; верхняя правая петля выделена мятным цветом. + + + +## Файлы + +- [Символ](../../../assets/brand/collaborative-symbol.svg). +- [Логотип для светлого фона](../../../assets/brand/collaborative-logo-light.svg). +- [Логотип для тёмного фона](../../../assets/brand/collaborative-logo-dark.svg). +- [Обложка репозитория 1280 × 640](../../../assets/brand/collaborative-social.png). + +SVG — редактируемые исходники с векторными контурами, включая буквы. В них нет +растровых вставок, внешних шрифтов и скриптов. Используется Geist под +[SIL OFL 1.1](https://github.com/Asmblyr/Collaborative/blob/main/apps/ui/src/app/fonts/OFL.txt). + +Сохраняйте геометрию, пропорции и свободное пространство вокруг символа. +Выбирайте версию для нужного фона. Основные цвета: графит #1B2930, мятный #88BFAA, +тёплый белый #F5F3EE. + +README выбирает логотип по светлой/тёмной теме читателя. Social preview GitHub +задаётся отдельно в настройках: добавление PNG в Git не выбирает его автоматически. + +Скриншоты продукта размещаются в docs/assets/screenshots и используют вымышленные +данные. Не публикуйте персональные данные, секреты и приватные адреса инфраструктуры. diff --git a/docs/ru/development/architecture.md b/docs/ru/development/architecture.md new file mode 100644 index 0000000..02b0536 --- /dev/null +++ b/docs/ru/development/architecture.md @@ -0,0 +1,98 @@ + + +# Архитектура + +Asmblyr состоит из HTTP API и приложения админки. Основная поставка использует +отдельные образы Core и UI; совместимый монолитный образ также доступен. +PostgreSQL хранит структуру, данные и состояние авторизации; S3 хранит содержимое +файлов. + +```mermaid +flowchart LR + Browser[Браузер] -->|страницы| UI[Next.js UI] + Browser -->|/api| Core[Fastify Core API] + UI -->|серверный рендеринг| Core + Client[Внешний клиент / SDK] --> Core + Core --> PG[(PostgreSQL)] + Core --> Objects[(S3-совместимое хранилище)] +``` + +Браузер обращается к Core напрямую через `/api`; Core ведёт браузерную сессию. +Внешний клиент обращается к Core напрямую с собственным access token. +Файлы проходят через Core: метаданные находятся в PostgreSQL, содержимое — +в настроенном объектном хранилище. + + + +## Core и UI + +`apps/core` использует Fastify и Knex. Маршруты разбирают запрос, доменные сервисы +проверяют права и поведение, слой запросов работает с PostgreSQL. `/collections` +управляет структурой, `/items/:collection` — данными. Системные таблицы имеют префикс +`asmblyr_`; структура меняется версионными миграциями. + +`apps/ui` использует Next.js, React и shadcn и отвечает за рендеринг страниц. +Браузер обращается к Core напрямую через `/api`; Next не содержит API-обработчиков. +Core управляет HttpOnly browser session, входом и callback провайдеров. При серверном +рендеринге Next читает cookie и обращается к Core от имени текущего пользователя. + +Production использует отдельные образы и Deployment Core/UI, собранные из одного +коммита. Ingress направляет `/api`, `/sign/sso`, `/connections/google/callback` и +`/oauth` в Core; более длинный путь `/oauth/interaction` и остальные страницы — в UI. +Согласие завершается в Core на `/oauth/complete/:uid`, cookie протокола имеет путь +`/oauth`. Общий HTTPS origin сохраняет работу сессий и callbacks. + +В локальной разработке, Compose и совместимом монолитном target публичный listener +Core обрабатывает `/api` и передаёт страницы в Next. Порты монолита: публичный 3000, +Core 3001, Next 3002. В Helm этот listener выключен: ingress обращается к компонентам +напрямую. Миграции выполняются отдельным Job. PostgreSQL и S3 разворачивает оператор. + +Общие сессии, ограничения входа, запросы отмены ассистента и подготовленные формы +плагинов находятся в PostgreSQL. Контроллер активной генерации остаётся в её процессе; +перезапуск прерывает поток. Пользовательские плагины должны учитывать несколько реплик. + + + +## Контракты и расширения + +`packages/contracts` содержит общие типы. `packages/sdk` предоставляет HTTP-клиент, +`packages/cli` подключает SDK и генерирует типы, `packages/kit` содержит контексты, +декларации и сборщик расширений. Встроенные комментарии находятся в +`packages/plugin-comments`; инструменты Google — в `packages/plugin-google-workspace`. +Учебные примеры — в `examples/plugins`. +Установленные пакеты включаются явно в `asmblyr.plugins` корневого manifest. + +```mermaid +flowchart LR + Package[Пакет расширения / Kit] --> Routes[H3 file routes и handlers] + Routes --> Core[Core: HTTP и права] + Routes --> Context[defineModelContext] + Context --> MCP[Внутренний MCP] + MCP --> Assistant[Ассистент] +``` + +`defineModelContext` добавляется только к нужному handler: остальные маршруты +остаются HTTP-only. Ассистент и внутренний MCP используют тот же обработчик; +доступ к данным всё равно проверяет Core. [Пошаговый пример](first-extension.md) +и [архитектура ассистента](assistant-architecture.md) раскрывают этот путь. + +Серверные обработчики расширений используют H3 file routing. UI импортирует +отдельный браузерный entry. Объявленные model handlers используются HTTP, +ассистентом и внутренним MCP с общими проверками. Таблицы расширений имеют префикс +`plugin__` и собственные миграции. Подробности — в +[Kit](../reference/kit-guide.md) и [контракте действий](plugin-actions.md). + + + +## Доступ и хранение + +Права проверяет Core: действия, поля, условия строк и grants настроек. UI metadata +не является границей доступа. История и транзакционные hooks проходят через общий +writer; прямой SQL и внешние эффекты не получают эти гарантии автоматически. + +Файлы имеют метаданные в PostgreSQL и содержимое в приватном S3 bucket. +Workspace группирует видимые коллекции, но не изолирует компании. +Произвольные плагины исполняются как доверенный код; песочницы нет. + +Рекомендации по изменениям — в +[CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.ru.md). diff --git a/docs/ru/development/assistant-architecture.md b/docs/ru/development/assistant-architecture.md new file mode 100644 index 0000000..16c8c5f --- /dev/null +++ b/docs/ru/development/assistant-architecture.md @@ -0,0 +1,54 @@ + + +# Архитектура ассистента и model context + +Ассистент — опциональная функция Core. Ключ и маршрут OpenAI-совместимого +провайдера остаются на сервере; UI передаёт текущую страницу и показывает потоковый +ответ. Поведение и лимиты описаны на [странице функции](../features/assistant.md). + +```mermaid +flowchart LR + UI[UI: чат и контекст страницы] --> Core[Core: запрос и поток ответа] + Core --> Provider[Настроенный модельный провайдер] + Core --> MCP[Внутренний MCP: инструменты] + MCP --> Items[Сервисы items и permissions] + MCP --> Model[Model handlers расширений] + Model --> Items + Items --> DB[(PostgreSQL)] +``` + + + +## Инструменты и данные + +Встроенные инструменты исследуют разрешённую схему, читают/ищут записи и готовят +фильтры и формы. Часть контекста поступает из display metadata и системного +справочника терминов. Технические имена остаются контрактом API; словарь терминов +помогает сопоставить выражения пользователя с данными, но не меняет права. +Точные сценарии и ограничения перечислены в [функциональном описании](../features/assistant.md). + +Чат требует человеческого пользователя: superuser, хотя бы один data grant +read/create/update либо активное личное Google-подключение. +Каждый вызов инструмента проходит серверную проверку. `items` применяет права +действий, полей и строк; при MCP-вызове учитывается также `mcp.enabled` у каждой +затронутой коллекции, включая связанные пути и superuser. Публичного внешнего MCP +endpoint сейчас нет. Данные, возвращённые инструментом, могут попасть к +настроенному модельному провайдеру. + + + +## Действия расширений + +`server/api/**/*.post.ts` — обычный H3 HTTP handler. Разработчик явно оборачивает +подходящий обработчик в `defineModelContext(defineHandler(...), annotation)`. +`defineModelAnnotation` задаёт название, описание, `AccessGate`, при необходимости +`page` и `readOnly`. Сборщик Kit выводит runtime-схемы входа и выхода из типов +TypeScript. Один обработчик и его middleware вызываются по HTTP, из ассистента +и внутреннего MCP; ручного реестра в `plugin.ts` нет. + +`AccessGate.authenticated` допускает запрос, но не выдаёт доступ к коллекциям. +`useItems(event)` использует права вызывающего. Для `readOnly: true` запись через +items блокируется. Привилегированное `storage.own` model handler не получает. +Подробный контракт, валидация и ограничения — в +[действиях расширений](plugin-actions.md); рабочий пример — +[calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator). diff --git a/docs/ru/development/documentation.md b/docs/ru/development/documentation.md new file mode 100644 index 0000000..8b59778 --- /dev/null +++ b/docs/ru/development/documentation.md @@ -0,0 +1,61 @@ + + +# Сопровождение документации + +Пользовательское поведение описывается в `docs/guide` и `docs/features`, архитектура +и эксплуатация — в `docs/development`, границы безопасности — в `docs/security`. +Публичные страницы содержат актуальные контракты, примеры и ограничения. + + + +## Языки + +По умолчанию сайт открывается на английском. Русские страницы повторяют пути +в каталоге docs/ru и публикуются под /ru/. Переключатель сохраняет текущую +страницу. Существующие i18next-каталоги админки, язык API по умолчанию и маршруты +приложения от этой локализации не меняются. + +При изменении поведения обновляйте обе версии. Руководства пакетов используют +английские исходники и русские файлы .ru.md; генератор выбирает нужный язык. +Идентификаторы API, примеры, права и ограничения должны совпадать по смыслу. +Перевод не должен превращать план в реализованное поведение. + + + +## Источники справочников + +| Содержание | Где редактировать | +| ----------------------- | ----------------------------------------------------------------------------------- | +| HTTP-маршруты | Обработчики Core; AST scanner читает объявления | +| Требования доступа | `docs/reference/endpoints.json`, после проверки handler и service | +| Подробные OpenAPI-схемы | `scripts/docs/http-contracts.mjs` и файлы контрактов рядом | +| Переводы API | Каталоги в `scripts/docs/locales` | +| SDK/Kit | Публичные TypeScript-типы и руководства внутри пакетов | +| Руководства SDK/Kit | README, HOOKS, FIELDS, LIFECYCLE, CAPABILITIES, UI и версии `.ru.md` внутри пакетов | + +Генератор создаёт OpenAPI, матрицу маршрутов, копии package guides и справочники +TypeDoc. Сгенерированные файлы не редактируются вручную. Внутренние планы, +исследования, локальные отчёты и приватный архив не копируются в сайт. + + + +## Проверки + +```sh +pnpm docs:generate +pnpm docs:check +pnpm docs:build +``` + +`docs:check` проверяет покрытие статических Core routes, актуальность каталога +и сгенерированных контрактов, а также наличие пар страниц на обоих языках. Сборка сайта проверяет Markdown-ссылки. +Динамические маршруты расширений и protocol endpoints oidc-provider описываются +отдельно. Уровень `route-only` означает, что подробная схема операции ещё отсутствует. + +При изменении endpoint проверяйте не только handler, но и предметную авторизацию, +обновляйте контракт и соответствующую функциональную страницу. Описание доступа +не исполняет авторизацию: её подтверждают отрицательные интеграционные тесты. + +Интеграционные проверки запускайте через `scripts/test.mjs` с одноразовой БД. +Для изменений общих контрактов выполняйте `pnpm check` и `pnpm build`. +Отдельно указывайте проверки, для которых не было подготовлено live-окружение. diff --git a/docs/ru/development/first-extension.md b/docs/ru/development/first-extension.md new file mode 100644 index 0000000..373b225 --- /dev/null +++ b/docs/ru/development/first-extension.md @@ -0,0 +1,155 @@ + + +# Первое расширение + +Этот путь использует работающие примеры [overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview) +(страница и HTTP endpoint) и [calculator](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/calculator) +(действие для ассистента). Примеры не включены в обычную установку. +Начните с [локального запуска](../guide/getting-started.md). + + + +## 1. Создайте пакет + +Для собственного workspace-пакета, например `@asmblyr-collaborative/plugin-hello`, используйте +`examples/plugins/hello`: +`pnpm-workspace.yaml` включает этот каталог. Удобная отправная точка — исходники +`examples/plugins/overview`, без копирования `dist` и `.asmblyr`. Переименуйте +`name` в `package.json` и `asmblyr.manifest.namespace`; для нового namespace +переименуйте первый каталог в `server/api/`. Namespace должен быть +уникальным и совпадать с первым сегментом HTTP-пути. Сохраните exports для +корневого entry, `./routes`, `./collections`, `./package.json` и `./ui`, script +`"build": "asmblyr-plugin build"`, TypeScript-конфигурацию и зависимости Kit/H3. +Версия манифеста сейчас `1`. [Пакет overview](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/package.json) +показывает полный рабочий manifest. + +Обязательный `plugin.ts` минимален: + +```ts +import { definePlugin } from "@asmblyr-collaborative/kit"; + +export default definePlugin({}); +``` + +Маршруты, страницы и model handlers здесь вручную не регистрируются. + + + +## 2. Добавьте серверный обработчик + +После переименования namespace файл `server/api/hello/session.get.ts` определяет +`GET /hello/session`. Сохраните тело обработчика overview и общий тип ответа: + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; +import type { Overview } from "../../../shared/overview.js"; + +export default defineHandler((event): Overview => { + const { actor } = useAsmblyr(event); + return { + viewer: { + id: actor.id, + kind: actor.kind, + displayName: actor.displayName ?? null, + }, + checkedAt: new Date().toISOString(), + }; +}); +``` + +Это адаптация [обработчика overview](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/overview/server/api/overview/session.get.ts). +Core требует аутентификацию до вызова обработчика. Для записей используйте +`useAsmblyr(event).items` или `useItems(event)`: методы применяют права вызывающего. +`actor.displayName` требует заявленной capability `identity.profile` и +отдельного одобрения проекта. Одно лишь имя в manifest доступа не даёт. +Обработчики с записью проверяют предметные права и валидируют вход. + + + +## 3. Добавьте страницу + +`ui/index.ts` — отдельный браузерный entry; он не импортирует `plugin.ts` или +серверные файлы: + +```ts +"use client"; + +import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; +import { OverviewPage } from "./pages/overview-page.tsx"; + +export default defineUiPlugin({ + pages: [{ id: "home", title: "Обзор", component: OverviewPage }], +}); +``` + +В компоненте `request("/session")` из `PluginPageProps` вызывает namespace endpoint +в Core через `/api` с текущей сессией. Рабочие компонент и hook есть в +[overview](https://github.com/Asmblyr/Collaborative/tree/main/examples/plugins/overview/ui). +После подключения страница появляется по адресу +`/extensions/hello/home`. Отдельных прав на UI-страницу пока нет: +серверный endpoint сам должен проверять доступ к своим данным. Для вкладок записи +и редакторов полей смотрите [Kit UI](../reference/kit-ui.md) и +[поля](../reference/kit-fields.md). + + + +## 4. Подключите пакет и права + +Добавьте пакет в корневые `dependencies` или `devDependencies` через +`"@asmblyr-collaborative/plugin-hello": "workspace:*"`, выполните `pnpm install` и включите +его в корневом `package.json`: + +```json +{ + "asmblyr": { + "plugins": ["@asmblyr-collaborative/plugin-hello"], + "pluginPermissions": { + "@asmblyr-collaborative/plugin-hello": ["identity.profile"] + } + } +} +``` + +Это **фрагмент**, не замена существующего блока: сохраните встроенные +`plugin-comments` и `plugin-google-workspace` с их разрешениями. Скопированный +manifest overview уже заявляет `identity.profile`; +в примере выше проект отдельно одобряет её для нового пакета. Отсутствующее +одобрение означает отсутствие +возможности. Серверные пакеты выполняются в процессе Core как доверенный код; +capabilities не создают песочницу. [Точный контракт](../reference/kit-capabilities.md). + + + +## 5. Опубликуйте действие для ассистента, если нужно + +Обычный H3 endpoint доступен по HTTP, но не появляется во внутреннем MCP. +Для model handler используйте `defineModelContext` вокруг того же +`defineHandler`, задайте `defineModelAnnotation({ title, description, +middleware: AccessGate.authenticated, readOnly: true })`. Модельный обработчик +должен быть статическим `.post.ts` маршрутом. Kit выведет схемы входа/выхода из +TypeScript; неподдерживаемые типы остановят сборку. Рабочий код с UI-формой — +[calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts). +Права на данные `AccessGate` не заменяет; внутри используйте `useItems(event)`. +Принцип вызова и ограничения описаны в [архитектуре ассистента](assistant-architecture.md) +и [контракте действий](plugin-actions.md). + + + +## 6. Проверьте и соберите + +```sh +pnpm --filter @asmblyr-collaborative/plugin-hello build +pnpm --filter @asmblyr-collaborative/plugin-hello typecheck +pnpm dev +``` + +Имя фильтра замените реальным именем пакета. Перезапустите Core и UI после +изменения списка плагинов. Войдите в админку, откройте +`/extensions/hello/home` и проверьте запрос страницы. Для отдельного +примерного набора из репозитория доступны `pnpm build:examples` и +`node scripts/test.mjs core-plugins` с одноразовой БД. + +Перед выпуском запускайте `pnpm build`: production loader читает собранный +`dist/routes.json`, а UI — отдельный `./ui` entry. `dist` и `.asmblyr` не включайте +в Git. Собственные коллекции, миграции, hooks и настройки разбираются в +[Kit lifecycle](../reference/kit-lifecycle.md) и [Kit hooks](../reference/kit-hooks.md). diff --git a/docs/ru/development/local-development.md b/docs/ru/development/local-development.md new file mode 100644 index 0000000..a5cd056 --- /dev/null +++ b/docs/ru/development/local-development.md @@ -0,0 +1,62 @@ + + +# Локальная разработка + + + +## Структура репозитория + +| Каталог | Ответственность | +| ---------------------------------- | ----------------------------------------------------------- | +| `apps/core` | Fastify API, доменные сервисы, Knex, миграции и авторизация | +| `apps/ui` | Next.js админка; сессию и API обслуживает Core | +| `packages/contracts` | Общие публичные типы | +| `packages/sdk` | Типизированный HTTP-клиент | +| `packages/cli` | Подключение SDK и генерация типов | +| `packages/kit` | Серверный и браузерный контракт расширений, сборщик | +| `packages/plugin-comments` | Установленный по умолчанию плагин комментариев | +| `packages/plugin-google-workspace` | Инструменты личных Google-подключений | +| `examples/plugins` | Не включённые по умолчанию примеры | +| `deploy`, `docs`, `scripts` | Развёртывание, документация и проверки | + +[Схема компонентов](architecture.md) показывает путь запросов и данных. + + + +## Запуск и изменения + +Требуются Node.js 22+, pnpm 11.13.1 и Docker Compose. Выполните +[первый запуск](../guide/getting-started.md), затем работайте через `pnpm dev`. +Эта команда собирает пакеты и запускает Core с UI. Маршруты расширений в локальных +workspace-пакетах отслеживает Core; изменение списка установленных пакетов требует +перезапуска Core и UI. Для production UI и пакеты собираются заново. + +Структуру Core меняйте новой версионной миграцией в `apps/core/migrations`. +Пользовательские коллекции живут в схеме `public`; имена `asmblyr_` и `plugin_` +зарезервированы. Миграции существующей установки выполняются через `pnpm db:migrate`. +Доступ к данным, правам и файлам проверяйте в сервисах Core, а не только в UI. +[Политики и границы доступа](../features/access.md) и +[матрица доступа](../security/access-matrix.md) описывают текущие правила. + + + +## Проверки + +```sh +pnpm format:check +pnpm typecheck +pnpm lint +pnpm docs:build +pnpm build +``` + +Для интеграционных тестов используйте **только** `node scripts/test.mjs` и +одноразовую БД: скрипту нужен локальный PostgreSQL и `TEST_DATABASE_ADMIN_URL` +с правом `CREATEDB`. `pnpm check` объединяет публикационную проверку, формат, +типы, lint, тесты, документацию; полный набор может быть долгим. +Инструкции по обновлению сгенерированных справочников — в +[сопровождении документации](documentation.md). + +Перед pull request прочитайте [CONTRIBUTING.md](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.ru.md). +Рабочие health/ready проверки, журналы и резервные копии описаны в +[эксплуатации](operations.md). diff --git a/docs/ru/development/operations.md b/docs/ru/development/operations.md new file mode 100644 index 0000000..e6da8c3 --- /dev/null +++ b/docs/ru/development/operations.md @@ -0,0 +1,123 @@ + + +# Эксплуатация и восстановление + +Одна команда использует отдельную установку, PostgreSQL и приватный bucket. +SMTP необязателен: администратор может вручную передать приглашение или ссылку +восстановления. Workspaces не разделяют данные компаний. + + + +## Сборка и установка + +Dockerfile собирает отдельные образы Core и UI из одного коммита с закреплёнными +Node image digest, pnpm и lockfile; приложения работают от пользователя `node`. +UI доступен на порту `3000`, Core API — на `3001`. Единый образ сохранён для +совместимости. PostgreSQL и S3 подключаются отдельно. +GitHub workflow проверяет типы, lint, тесты, документацию, production build и +запуск контейнера с одноразовым PostgreSQL перед публикацией образа в GHCR. + +[Шаблоны установки](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.ru.md) +включают локальную установку с S3 и вариант для внешних PostgreSQL/bucket. +Миграции выполняются перед Core. Для публичной установки нужны HTTPS reverse proxy, +точный внешний `AUTH_UI_URL`, приватные БД и bucket. OAuth signing keys сохраняйте +между выпусками и резервируйте отдельно. + +Перед обновлением создайте копию данных и проверьте миграции на восстановленной +установке. Откат образа допустим только с совместимой схемой; разрушительный +rollback миграций не заменяет восстановление. + + + +## Копии локальной Docker-установки + +Следующие инструменты обслуживают только `deploy/local/compose.yaml`: + +```sh +node scripts/operations/backup.mjs /private/asmblyr.env asmblyr-local /private/backups/new-copy +node scripts/operations/restore.mjs /private/restore.env asmblyr-restore-check /private/backups/new-copy +node scripts/operations/status.mjs /private/restore.env asmblyr-restore-check +``` + +Backup останавливает UI/Core и S3, сохраняет `pg_dump -Fc` и offline object volume, +записывает SHA-256 и ID образов, затем поднимает сервисы даже при ошибке копирования. +Каталог копии должен быть новым; других авторов БД/S3 в этот момент быть не должно. + +Restore проверяет hashes и версии образов, допускает только новый проект +`asmblyr-restore-NAME` без существующих volumes. Он выполняет `pg_restore`, +восстанавливает object volume и запускает миграции. Используйте отдельный UI port. +После восстановления проверьте вход, число записей, доступ и содержимое файлов. +Env и signing keys сохраняются отдельно от копии БД/объектов. + +Для внешних PostgreSQL/S3 оператор организует согласованную копию при остановленных +авторах и восстановление в новые ресурсы. Скрипты локального compose не копируют +внешние облачные bucket и не создают инфраструктуру. Копии содержат пользовательские +данные и credential hashes: храните их приватно и шифруйте средствами backup-системы. + + + +## Восстановить администратора + +После сборки Core оператор с доступом к БД может выполнить: + +```sh +node scripts/operations/admin-recover.mjs admin@example.test /private/new-credential.json +``` + +`DATABASE_URL` передаётся через окружение процесса. Команда создаёт приватный файл +с новым случайным паролем, не печатает его и отказывается заменить существующий +файл. Предыдущие способы входа, сессии и OAuth consents отзываются. +Обычных пользователей восстанавливает superuser через ссылку в админке. + + + +## Ключи OAuth + +Ключи создаются отдельно от сборки: + +```sh +node scripts/operations/create-oauth-keys.mjs .local-data/oauth-keys.json +``` + +Укажите `OAUTH_ISSUER_URL` и `OAUTH_KEYS_FILE` в окружении Core. Путь к ключам +должен быть доступен из рабочего каталога Core; для локального запуска из +`apps/core` это `../../.local-data/oauth-keys.json`. В контейнере подключайте +приватный файл отдельно и указывайте его путь внутри контейнера. Команда +отказывается заменить существующий файл; не пересоздавайте ключи при обновлении. + + + +## Диагностика + +Опциональный [Sentry и локальные p95/p99](../features/monitoring.md) подключаются +в системных настройках. Мониторинг сам по себе не создаёт правила алертов, +retention или dashboards в вашем Sentry: их настраивает оператор. + +Воспроизводимый замер API на синтетической схеме запускается только явно. +Из корня репозитория в PowerShell: + +```powershell +$env:ASMBLYR_PERFORMANCE_AUDIT = "1" +node scripts/test.mjs core-performance +Remove-Item Env:\ASMBLYR_PERFORMANCE_AUDIT +``` + +Runner создаёт и удаляет отдельную временную локальную БД. Нужен доступ к локальному +PostgreSQL с правом CREATE DATABASE; удалённая рабочая БД не допускается. Тест +создаёт 40 коллекций/20 000 записей, замеряет список, поиск и глубокую страницу +при 1/8 параллельных запросах, выводит p50/p95/p99, SQL/request и планы поиска. +Это Fastify inject без браузера и внешней сети, для superuser и простой схемы; +он не доказывает производственный SLO или скорость сложных прав/связей. + +Для сравнения накладных расходов задайте `ASMBLYR_PERFORMANCE_MONITORING=disabled` +или `enabled`. Enabled использует настоящий Sentry SDK с офлайн-транспортом, +API performance и SQL spans; сеть/доставка в Sentry не измеряются. +`ASMBLYR_PERFORMANCE_SAMPLE_RATE` задаёт долю трассировок от 0 до 1 (по умолчанию 1 +в тесте). `ASMBLYR_PERFORMANCE_REPORT` сохраняет JSON в указанный приватный путь. +После проверки удалите тестовые переменные из окружения оболочки. + +`/health` проверяет процесс, `/ready` — БД и обязательные миграции. +Логи не должны содержать тела запросов, токены и конфигурационные секреты. +Retentions задаются окружением Core; история бизнес-изменений по умолчанию +не удаляется автоматически. Настройте мониторинг, внешние копии, проверку +восстановления и доставку алертов для своей установки. diff --git a/docs/ru/development/plugin-actions.md b/docs/ru/development/plugin-actions.md new file mode 100644 index 0000000..e99289f --- /dev/null +++ b/docs/ru/development/plugin-actions.md @@ -0,0 +1,223 @@ + + +# Действия плагинов: HTTP, MCP и ассистент + +Действующий контракт: file routing H3 v2, типизированная аннотация и один обработчик. +Публичного MCP endpoint пока нет. Инструкции по разработке расширений — в [Kit](../reference/kit-guide.md). + + + +## Объявление + +`server/api/calculator/calculate.post.ts`: + +```ts +import { + AccessGate, + defineHandler, + defineModelAnnotation, + defineModelContext, +} from "@asmblyr-collaborative/kit"; +import type { CalculationInput } from "../../../shared/calculation.ts"; + +const annotate = defineModelAnnotation({ + title: "Расчёт стоимости", + description: "Рассчитать стоимость подписки по параметрам пользователя.", + middleware: AccessGate.authenticated, + readOnly: true, + page: "home", +}); + +export default defineModelContext( + defineHandler(async (event) => { + // Core проверил JSON до входа; здесь обычный H3 и предметная логика. + const input = (await event.req.json()) as CalculationInput; + return { total: input.users * input.monthlyPrice * input.months }; + }), + annotate, +); +``` + +Полный расчёт с копейками и округлением — в +[calculator](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/calculator/server/api/calculator/calculate.post.ts). +Пример выше иллюстрирует объявление, не денежную арифметику. + +- Путь и метод определяет файл. Model handler требует статический `.post.ts` + внутри namespace. ID получается из пути после namespace: `calculate`; + вложенные сегменты соединяются дефисами. Коллизия ID останавливает запуск. +- `plugin.ts` остаётся `definePlugin({})`. Ручной регистрации и отдельного + `server/actions` нет. Общий `/extensions/:namespace/actions/:action` не используется. +- Обёртка `defineModelContext` явно публикует инструмент; обычные H3 endpoints + в MCP не видны. Аннотация сохраняется в нативной metadata H3. +- `middleware` сейчас принимает `AccessGate.authenticated` или `AccessGate.superuser`. + Дополнительную предметную middleware можно указать во внутреннем `defineHandler`. +- `page` — необязательный ID страницы, принимающей подготовленный результат. + `readOnly` по умолчанию false. Для вычислений указывайте true. +- Старые `defineAction`/`defineActionContract` сохранены как deprecated для совместимости. + Новый код использует этот API. `bindModelDefinition` предназначен загрузчику Core; + разработчик плагина его не вызывает. + + + +## Генерация и валидация + +Существующий сборщик плагинов анализирует `` и возвращаемый тип внутреннего +обработчика **до стирания типов TypeScript**. Результат async разворачивается из +Promise. При желании результат можно явно описать интерфейсом. + +Поддержаны строки, числа, boolean, литералы, null, объединения внутри полей, +массивы (включая readonly) и вложенные объекты с известными ключами. Корневые вход +и выход — объекты. Все поля обязательны; отсутствие значения обозначается null. +Лишние ключи запрещены, в том числе во вложенных объектах. + +`any`, `unknown`, необязательные свойства, рекурсивные типы, index signatures, +классы, функции, кортежи и не-JSON типы останавливают сборку. Нельзя молча заменить +непонятный тип на разрешение любых данных. Ограничение сложности — 20 уровней и +1000 узлов. У плагина нужен tsconfig; прямой вызов обёртки должен быть default export +или инициализатором экспортированной по умолчанию локальной переменной. Импорт +`defineModelContext` с alias поддержан; произвольные фабрики вокруг него пока нет. + +Описания и ограничения свойств задаются в JSDoc: + +```ts +export interface CalculationInput { + /** + * Количество пользователей. + * @title Пользователи + * @integer + * @minimum 1 + * @maximum 100000 + */ + users: number; +} +``` + +Обычный комментарий становится description. Поддержаны `@title`, `@description`, +`@integer`, `@minimum`, `@maximum`, `@multipleOf`, `@minLength`, `@maxLength`, +`@minItems`, `@maxItems`. Числовые ограничения применяются к простому типу свойства; +несовместимый тип, неизвестный тег или некорректные границы дают ошибку. + +Схемы пишутся в `.asmblyr/models/<путь маршрута>.json` и production route index. +`.asmblyr` исключён из Git. Сборка не заменяет предыдущий dist при ошибке типов +или генерации. Source loader пересоздаёт схемы перед загрузкой обработчиков, +поэтому изменение общего TS-типа подхватывается при dev-перезапуске Core. +Отсутствующая схема у аннотированного обработчика останавливает загрузку. + +Core проверяет вход до handler и выход после него. Runtime использует Zod из +сгенерированной JSON Schema. Generic сам по себе не выполняет валидацию без Core. + +Форма импортирует JSON и использует `useAction(model, props, options)` +из `@asmblyr-collaborative/kit/ui/use-action`. `modelField` из `@asmblyr-collaborative/kit/model` возвращает +подпись/описание поля. В tsconfig нужен `resolveJsonModule: true`. Браузер не +импортирует серверный handler или корневой Kit. Для начальной генерации — +`pnpm build:plugins`; `pnpm dev` уже выполняет её перед запуском UI. + + + +## Права и данные + +`AccessGate.authenticated` разрешает вход аутентифицированному пользователю или +сервисному аккаунту. Это не разрешение на коллекции. `AccessGate.superuser` +дополнительно требует superuser; такой инструмент скрыт от остальных при discovery +и повторно проверяется на каждом вызове. + +```ts +const items = useItems(event); +await items.update("articles", id, { title }); +``` + +`useItems(event)` — общий API данных для обычных endpoints и model handlers. +Он автоматически использует личность и права текущего запроса: коллекцию, действие, +поля, проверки связей, валидацию и историю Core. Нельзя передать другого actor или +повысить права. HTTP и MCP используют тот же handler и сервис `/items`. +При отказе HTTP возвращает 403, MCP — ошибку `PERMISSION_DENIED` без содержимого +недоступных записей. Сессия и права заново проверяются перед каждым MCP-вызовом. + +В MCP есть дополнительное пересечение с публикацией коллекций: `mcp.enabled=false` +закрывает чтение и запись, включая фильтры, поиск, подписи и изменение связей. +Это действует и для superuser. HTTP-права от настройки публикации не меняются. +`readOnly: true` блокирует create/update/delete/commit через items в обоих каналах. +Удаление через MCP также блокируется, если настроенные cascade/setNull/setDefault +могут затронуть отключённую коллекцию, включая цепочку каскадов. Проверка по схеме +консервативна: она не читает скрытые строки и действует даже при пустой дочерней таблице. + +`useActionContext(event)` даёт actor, superuser, signal и items. Полный контекст +`useAsmblyr`, привилегированный storage плагина, Knex, токены и cookies не передаются. +Условия прав на строки применяются через общий items API. Пользовательский фильтр запроса не является политикой доступа; плагин не может подменить actor или расширить его grants. + +Пакеты выполняются как доверенный серверный код, без песочницы. `readOnly` ограничивает +выданный items, но не может запретить сторонний сетевой клиент, импортированный самим +плагином. MCP hints отражают readOnly; для произвольного кода openWorldHint=true. + + + +## Вызов и транзакции + +- Core: `POST /calculator/calculate`. +- UI: `POST /api/calculator/calculate` напрямую в Core с браузерной сессией. +- MCP: `plugin_calculator__calculate`, JSON text и structured content. +- Ответ HTTP: `{ data: { namespace, actionId, input, output } }`. + +Handler получает нормализованный POST с проверенным JSON. Исходные auth headers, +cookies и query не передаются. H3 middleware выполняется в обоих каналах. +Response, redirect, streaming, изменение заголовков или статуса не поддерживаются; +возвращается JSON-объект. Обычные endpoints без обёртки сохраняют полный HTTP API. + +Каждая запись items и её история транзакционны. Весь произвольный handler одной +транзакцией не является. Для связанного сохранения есть items.commit. Ошибка после +завершённой записи (в том числе в выходной схеме) не отменяет её. После таймаута +нельзя автоматически повторять действие с записью: результат сначала проверяется. + +Вызов ограничен 10 секундами, результат — 48 КБ. Отмена прекращает ожидание, +запрещает последующие вызовы items и создание prepared-формы; уже начатую запись +она гарантированно не откатывает. Доверенный handler должен соблюдать signal. + + + +## Подготовленная форма и ответ ассистента + +Успешное действие с `page` создаёт приватный снимок входа и выхода на 20 минут: +до 32 на пользователя, 256 на установку. Снимки хранятся в PostgreSQL, доступны +всем репликам Core и сохраняются при перезапуске до истечения срока. Значений формы в URL нет. + +`GET /extensions/:namespace/drafts/:id` проверяет сессию, владельца, namespace +и доступ к действию. Чужой или истёкший снимок даёт одинаковый 404. При изменении +прав на данные снимок не перечитывается из коллекций: он является результатом уже +выполненного действия владельца, а не новой выдачей данных. + +Ассистент объясняет результат в сообщении и вызывает `present_plugin_result` с +ID успешного результата текущего обращения. Появляется кнопка **«Открыть страницу»**; +автоматической навигации нет. Возвращается `requiresUserClick: true`. Поддельный ID +не принимается. Наличие prepared-формы само по себе не доказывает изменение данных. + +`useAction` передаёт host dirty/busy, проверяет ID и выходную схему, защищает форму +при переходе из ассистента или замене снимка. Общая защита всех переходов через +меню пока не реализована. Частичные формы и чтение текущих ручных черновиков не +поддержаны. Детальные разрешения на отдельные действия и публичный MCP — позднее. + + + +## Проверка изменений + +- File routing и схемы совпадают в source и built. Нет параллельного реестра. +- HTTP/MCP используют один handler, H3 middleware и валидацию входа/выхода. +- Plain endpoints не видны MCP; superuser и отозванные права проверяются при вызове. +- Items защищает действия/поля, MCP-публикацию и связанные коллекции; readOnly блокирует запись. +- Форма использует генерируемую схему без серверных импортов. +- Сохраняются владелец/TTL снимка, защита dirty-формы и кнопка без автоперехода. +- Документация и calculator соответствуют текущему API. + +```sh +pnpm build:plugins +pnpm build:examples +node scripts/test.mjs core-plugins +``` + +Runner создаёт и удаляет отдельную временную PostgreSQL. Интеграционные тесты +нельзя запускать напрямую на базе проекта. Изменения UI также проверяются в браузере. + + + +## Личные подключения + +Аннотация `connection: "google"` требует активного собственного подключения и одобренной capability `connections.google`. `useActionContext(event).connections.google` даёт ограниченный broker без credentials. `proposeWrite` подготавливает owner-bound предложение; подтверждение реализовано в Core и отсутствует в MCP. Google-плагин остается H3 file routes со схемами из типов; отдельной регистрации инструментов нет. См. [Google Workspace](../features/google-workspace.md). diff --git a/docs/ru/features/access.md b/docs/ru/features/access.md new file mode 100644 index 0000000..481783e --- /dev/null +++ b/docs/ru/features/access.md @@ -0,0 +1,130 @@ + + +# Права и политики + + + +## Модель + +Permission — самостоятельная настройка доступа. Policy объединяет любое количество permissions; одно permission можно связать с несколькими policies. Пользователь получает несколько policies, их разрешения объединяются. Отдельных deny-правил нет. + +У permission одна цель: коллекция **либо** раздел настроек. Для коллекции задаются action `create/read/update/delete`, поля и необязательный `rowFilter`. Без условия разрешение действует для всех записей. Для раздела — `section`, `action: read | update`, `fields: ["*"]`. `update` включает просмотр; `read` не разрешает изменения. + +Прямое изменение общего permission влияет на все использующие его политики. Редактор конфигурации политики сохраняет её собственный набор атомарно, не меняя чужую политику через случайное редактирование общего экземпляра. + + + +## Проверка + +Core загружает активного principal и актуальные grants из БД. Отзыв политики не требует перевыпуска пользовательского токена. Действие, поля и связанные записи проверяются в сервисах, которыми пользуются HTTP, Kit и внутренний MCP. + +Первичный ключ остаётся идентификатором читаемой записи. Запись без read может успешно вернуть `data: null`. System routes не являются обычными items: для них собственные проверки допуска. + + + +## Приложения в политиках + +Приложение с включённым управлением через политики появляется во вкладке +«Приложения» редактора политики. «Разрешить вход» допускает пользователя к SSO; +галочки ниже назначают права внешнего сервиса из его настроенного каталога. +Несколько политик объединяются, deny-правил нет. Отсутствие назначения запрещает +вход даже superuser; superuser Collaborative не означает администратора внешнего сервиса. + +`GET /policies/applications` возвращает каталог для редактора и требует человеческого +доступа `policies/read` или `update`. Полная конфигурация `POST /policies` и +`PATCH /policies/:id` принимает необязательное поле +`applications: [{ appId, scopes }]`. Пустой `scopes` разрешает только вход, +пустой `applications` снимает все назначения приложений этой политики. +Пропуск `applications` при обновлении сохраняет назначения. Права приложения, +права данных и пользователи сохраняются одной транзакцией. `GET /policies/:id` +возвращает этот же список. Неизвестные приложения и разрешения отклоняются. + +Состав меняет только человек-superuser. Ограниченное назначение готовых политик +действует и для их прав на приложения: менеджеру нужен явный разрешённый набор, +он не меняет себя или superuser. OAuth-права не дают доступ к Core API. +Подробности токенов и сроков отзыва — в [интеграциях](integrations.md#доступ-через-политики). + + + +## Условия записей + +В редакторе политики нажмите действие коллекции: «Нет доступа», «Все записи» или «По условию». Условие состоит из поля, оператора и значения. Поиск поля учитывает его название и техническое имя; оператор зависит от типа. Значением может быть литерал или совместимый параметр из поиска. Параметры и их описание раскрываются по мере необходимости. Группы поддерживают И/ИЛИ; выбор полей свёрнут отдельно. «Применить» возвращает правило в черновик; доступ изменяется после общего сохранения политики. + +```json +{ + "collection": "articles", + "action": "read", + "fields": ["title", "body"], + "rowFilter": { + "logic": "and", + "children": [ + { + "field": "author_id", + "op": "eq", + "value": { "kind": "context", "path": "user.id" } + }, + { + "field": "status", + "op": "eq", + "value": { "kind": "literal", "value": "published" } + } + ] + } +} +``` + +`rowFilter: null` явно снимает условие. При PATCH permission отсутствие `rowFilter` сохраняет старое условие. Настройки `section` условий не поддерживают. Общий permission разделяется между политиками; редактор конфигурации учитывает условие при выборе экземпляра и сохраняет независимость других политик. + +Каталог параметров принадлежит Core/Contracts: `user.id`, `user.email`, `service.id`, `actor.id`, `request.now`. Значения берутся из аутентифицированного principal и времени сервера; клиент не может передать контекст. Несовместимый тип отклоняется при сохранении. Если параметр отсутствует для участника запроса, недействительно **всё правило**, в том числе его ветки ИЛИ. Повреждённое или устаревшее условие также не открывает доступ. + +Поддерживаются прямые скалярные поля, включая ID, внешний ключ M:1 и timestamps. Операторы совпадают с обычным фильтром по этому типу; `exists/notExists`, поля через точку, JSON, repeaters и массивы файлов в этой версии не поддерживаются. Группы непустые, глубина до 3, до 20 условий/30 узлов, JSON до 8192 символов. Литералы — строки; списки значений — массивы строк. + +Чтение ограничивает SQL до пагинации и count. Поля каждой записи проецируются по сработавшим правилам: разрешение «title всем» вместе с «body своим» не открывает чужой body. Фильтр, сортировка и агрегаты по условно доступному полю работают только по строкам, в которых разрешено это поле. Поиск проверяет каждое искомое поле отдельно. Те же границы применяются к labels, связанным спискам/кандидатам, файлам, Kit и MCP. + +Создание проверяет итоговую запись с серверными defaults. Проверяются только поля, переданные пользователем; defaults не требуют самостоятельного field grant. Изменение проверяет исходную и итоговую запись по тем же сработавшим веткам для изменяемых полей. Выход из области разрешения, например смена автора при праве «редактировать свои», откатывает всю транзакцию. Bulk и вложенный draft также откатываются целиком при отказе. Удаление проверяет исходную запись. Результат записи возвращает только читаемые поля либо `data: null`. + + + +### Границы первой версии + +- Related search и фильтры по полям через связь отключены, если исходная, целевая или промежуточная коллекция использует условное чтение. Прямой внешний ключ и списки отношений продолжают работать с проверкой строк. Это предотвращает утечку через EXISTS до поддержки условий на пути связи. +- История хранит diff, а не полное состояние каждого старого объекта. Поэтому условные grants не открывают историю: видны только поля из безусловных read grants. При отсутствии таких grants история пуста, включая удалённые записи. Поддержка истории по условиям требует полных исторических снимков. +- Superuser обходит условия. Доверенный `storage.own` остаётся отдельной ограниченной capability плагина; обычный `items` проверяет права вызывающего. + + + +## Сильные административные права + +- Состав политик, permissions, создание и удаление политик меняет только человек-superuser. +- `policies/update` позволяет назначать и снимать готовые политики из явного разрешённого набора пользователя. Менеджер не меняет собственные назначения и назначения superuser. +- `services/update` позволяет управлять сервисами, если все их текущие и запрошенные политики входят в разрешённый набор. Ключи и федерации проверяют ту же границу. +- `users/update` позволяет приглашать пользователей и видеть их доступ; само по себе не разрешает изменение политик. +- `superuser` обходит обычные data/settings grants, но не проверки корректности и защиты структуры. + +Права настроек не дают чтение записей коллекций и DDL автоматически. Сервисные токены не могут использовать настройки, даже если связанная политика содержит section grants. + + + +## Пока отсутствует + +Запрет через deny. Фильтр таблицы, workspace, скрытая коллекция и MCP enable — разные механизмы; они не заменяют permissions. + + + +## Ограниченное назначение политик + +Администратор открывает пользователя → панель доступа → «Разрешено назначать», выбирает готовые политики и сохраняет набор. Это отдельная настройка конкретного пользователя, не permission внутри политики. Назначение другой политики не переносит чужое право делегирования. По умолчанию набор пуст; собственные права пользователя не являются разрешением выдавать их другим. + +Нужны оба допуска: `policies/update` для назначений людям либо `services/update` для сервисов, и ID политики в разрешённом наборе. `read` разрешает просмотр. Назначение самому себе, изменение состава политики и редактирование разрешённого набора доступны только superuser. + +`PUT /users/:id/delegation` принимает только `{ policyIds }` и доступен superuser. `PUT /policies/:id/users` принимает только `{ userIds }` и атомарно заменяет назначения одной готовой политики. Индивидуальные PUT/DELETE назначений проверяют те же ограничения. Чужие политики целевого пользователя не заменяются. Существующие собственные назначения и назначения superuser в bulk-запросе можно сохранить без изменения. + +Сервис с хотя бы одной политикой вне набора доступен менеджеру только для просмотра. Нельзя выпустить или отозвать ключ, изменить федерацию, отключить аккаунт или удалить его закрытые политики через PUT. Сервис без политик не имеет прав на данные; менеджер может настроить его в пределах своего набора. Повторное приглашение не выдаётся менеджеру для аккаунта с закрытыми политиками либо с собственным правом делегирования. + +Список `GET /users` содержит `hasDelegation`: признак непустого личного разрешённого набора. UI скрывает для менеджера кнопку повторного приглашения у таких пользователей, собственного аккаунта и аккаунтов с политиками вне набора. Сам набор редактируется только в панели доступа администратором; сервер перепроверяет ограничения независимо от кнопок. + +Проверки читают актуальное состояние из БД; отзыв набора действует со следующим запросом без смены токена. Разрешение делегировать не даёт права читать данные по выбранным политикам. Сервисные токены не управляют политиками или делегированием. + +Миграция `20261003040000_policy_delegations.cjs` создаёт пустой набор для существующих менеджеров; прежние grants и назначения сохраняются. Непривилегированные менеджеры теряют неограниченное изменение политик: администратор должен явно настроить разрешённый набор. Down отказывается удалять непустые настройки делегирования. + +Отзыв набора ограничивает будущие административные операции; уже выданные ключи и назначенные получателям политики автоматически не отзываются. Их нужно отзывать отдельно. Изменение администратором состава разрешённой политики меняет её права у всех получателей. diff --git a/docs/ru/features/assistant.md b/docs/ru/features/assistant.md new file mode 100644 index 0000000..7a53a30 --- /dev/null +++ b/docs/ru/features/assistant.md @@ -0,0 +1,275 @@ + + +# Ассистент и внутренний MCP + +Путь запроса, инструментов и модельных обработчиков показан в +[архитектуре ассистента](../development/assistant-architecture.md). + +Ассистент опционален: Core подключается к OpenAI-совместимому провайдеру, ключ хранится на сервере. UI передаёт контекст текущей страницы; ответы передаются потоком. Переход на предложенную страницу выполняет пользователь кнопкой. + +Инструменты могут исследовать доступную схему, искать/читать данные, формировать фильтры и выборки, подготавливать формы и вызывать явно объявленные model-handlers расширений. Названия для человека берутся из display metadata; технические имена используются для API. + +Для обычного поиска ассистенту задан короткий сценарий: нужные записи и кнопка +полной выборки. Подсчёты, рейтинги и дополнительные справочники запрашиваются +для соответствующих вопросов; подробный анализ можно продолжить следующим +сообщением. Это инструкция модели, а не гарантия длительности ответа провайдера. + + + +## Границы + +Чат требует человека: superuser, хотя бы один data grant read/create/update либо активное личное Google-подключение. Право `assistant` позволяет управлять настройками, но само не даёт права чата/данных. + +Внутренний MCP — общий контракт инструментов для ассистента. Публичного внешнего MCP endpoint пока нет. Каждая операция выполняется с правами пользователя, дополнительно учитывая включение коллекции в MCP, в том числе для superuser. Нельзя выдавать модели сырой SQL или привилегированный storage. + +Для плагина разработчик оборачивает H3 handler в `defineModelContext` и задаёт аннотацию. HTTP и MCP используют тот же handler и проверку. Входной AccessGate не заменяет права на затронутые данные. `readOnly` должен запрещать запись в доступных инструментах. + + + +## Настройки и журнал + +В разделе настроек доступны инструкции, модель и телеметрия: пользователь, время, запрошенная/фактическая модель, вызовы инструментов, входные/выходные токены, длительность и результат. Если провайдер не вернул usage, неизвестные значения не превращаются в ноль. + +Сводка ответа содержит `toolTrace`: до 32 вызовов с порядковым номером, +именем объявленного инструмента, длительностью, результатом и фиксированным +кодом ошибки. Аргументы, содержимое результатов и тексты исключений в эту +диагностику не попадают. Старые ответы могут не содержать `toolTrace`. +Счётчики токенов суммируются по обращениям к модели внутри одного ответа; +это не размер одного контекста. + +Общий таймаут одного ответа, включая сжатие истории и продолжения с +инструментами, по умолчанию составляет 180 секунд. `OPENAI_API_TIMEOUT_MS` +задаёт значение от 1000 до 300000 мс; явное значение сохраняется. Число +обращений к модели остаётся ограниченным восемью, отмена пользователем +прерывает текущую генерацию и инструменты. +Прокси админки допускает до 310 секунд, чтобы не оборвать поток раньше +максимального таймаута Core. + +Ошибки инструментов возвращают безопасные `code` и `hint`, например +`SCHEMA_REQUIRED` для чтения без описания коллекции и `INVALID_FILTER` для +неверного фильтра. Идентичный неудачный вызов внутри ответа блокируется +без повторного выполнения; модель переходит к итоговому сообщению. +Повтор с исправленными аргументами допускается, пока остаются шаги. +Для фильтров `reason` различает неверную структуру JSON, путь, оператор, +тип значения, квантор и превышение ограничений; `hint` объясняет исправление +без раскрытия отклонённых значений. Фильтр передаётся JSON-строкой группы +`logic`/`children`. Пустая строка очищает условия запроса, настоящий JSON +`null` наследует контекст; строка `"null"` отклоняется. Поиск другого клиента +должен использовать его условие вместо условия открытой таблицы. Это +инструкция модели; Core по-прежнему проверяет все права и фильтры. +Смысловые условия вроде «активные» должны сохраняться в поиске, подсчёте +и карточке через настроенный термин или явно заданный критерий. Если термин +не определяет однозначный смысл, ассистент должен уточнить критерий. +В аргументе поиска `q` строка `"null"` отклоняется как ошибочное обозначение +пустого значения. Для наследования условий нужен JSON `null`, для очистки — +пустая строка; поиск буквального слова возможен через `q="NULL"` или значение +текстового фильтра. Это ограничение внутренних инструментов, HTTP/SDK-поиск +не меняется. Унаследованный поиск из таблицы сохраняется целиком. +Режим `order` (`relevance`/`field`) наследуется вместе с поиском и сохраняется +в подборке: при её открытии таблица повторяет проверенный порядок результатов. +Явная сортировка по полю переключает обычный порядок, `order=relevance` +использует это поле только для равных совпадений. + +Если завершённый ответ сопровождается только `present_selection` или +`present_plugin_result`, Core проверяет и создаёт карточки, затем показывает +этот ответ без ещё одного обращения к модели. Ошибка карточки, пустой текст +или дополнительные инструменты требуют обычного продолжения. Это не +изменяет правила подтверждения записей или навигации пользователем. + +`describe_collection` описывает M2O, O2M и M2M. Для доступных связей ко многим +`relation.relatedRead` указывает целевую коллекцию, поле фильтра, оператор, +квантор и исходный ключ. Ассистент читает связанные записи отдельным запросом; +поля alias нельзя включать в `fields`. Подсказки учитывают права чтения полей, +включение коллекций в MCP и ограничения фильтрации связей при row rules. + +Системный справочник терминов связывает пользовательские слова со смыслом данных; конкретные привязки к коллекции пока настраивает superuser. + + + +## Сессии и история + +Ход работы отображается отдельно от ответа: время выполнения, небольшие серые +статусы действий и публичные пояснения ассистента. После завершения этот блок +сворачивается; внутри остаются шаги и статистика. Итоговый ответ содержит только +финальное сообщение модели. Таблицы, карточки выборок и важные оговорки остаются +в результате. Внутренние рассуждения, аргументы инструментов и их сырые результаты +в блок хода работы не передаются. + +У текстового ответа появляется компактная кнопка копирования, когда генерация +закончилась или была остановлена. Она копирует исходный текст ответа с Markdown, +включая таблицы и код; ход работы, статистика и отдельные карточки действий в буфер +не добавляются. После копирования появляется подтверждение. Если браузер не дал +доступ к буферу обмена, интерфейс предлагает выделить и скопировать текст вручную. +Иконки в шапке снабжены подсказками; новая сессия начинает отдельный разговор, +сохраняя переписку и черновик предыдущей сессии. + +В узкой панели широкие таблицы прокручиваются по горизонтали внутри ответа; +числа и идентификаторы не разбиваются для подгонки колонок. Область таблицы +доступна с клавиатуры. Блоки кода также имеют отдельную горизонтальную прокрутку. +При уменьшении высоты панели поле ввода и отступы становятся компактнее, +сохраняя место для переписки; длинный черновик прокручивается внутри поля. + +API возвращает отдельный `activity`: до 64 записей `status`/`note`, до 8000 +символов в записи и 64 000 символов в сумме. Новые сообщения сохраняют его +отдельно от `content`; при восстановлении истории блок доступен для раскрытия. +Перед продолжением и сжатием диалога используются только завершённые сообщения, +без `activity`. Старые сохранённые ответы остаются неизменными. + +NDJSON дополнен событием `activity`. `text-delta` с `provisional=true` относится +к текущему незавершённому шагу и показывается в ходе работы; такой текст ещё +не является итоговым ответом. При завершении модельного шага Core либо сохраняет +его как публичное пояснение, либо передаёт финальный текст с `reset=true`. +`reset` заменяет текущий текст ответа. Остановка сохраняет полученные публичные +пояснения отдельно, а уже подтверждённую часть итогового ответа — в `content`. + +Пустой диалог показывает приветственную карточку с двумя действиями: поиск и +объяснение структуры, а на странице коллекции — фильтр и объяснение полей. +При отключённом контексте предлагаются общие вопросы о проектировании данных; +инструменты базы и Google остаются доступны, если отдельно включён доступ к данным. +Нажатие подставляет редактируемый вопрос в поле ввода; отправка остаётся за +пользователем. Карточка также открывает личные подключения Google Drive и Sheets +и исчезает после начала переписки. + +В карточке сохранённой записи доступна кнопка «Спросить об этой записи». +Ассистент открывается внутри редактора, включая вложенные карточки и импортированные +materialized view. Сворачивание ассистента не закрывает карточку и сохраняет текст +ввода. Контекст показывает коллекцию и ID записи; после закрытия карточки снова +используется контекст страницы. + +Над вводом показан контекст следующего вопроса. Если он отличается от последнего +отправленного вопроса, появляется компактная подсказка о смене истории для ответа. +Переходы между контекстами отмечены разделителями в ленте и сохраняются при открытии +истории сессии. Значок справки объясняет изоляцию истории, её сжатие и режим без +контекста страницы. Поиск и фильтр внутри той же таблицы не создают отдельную историю. +Переход во время генерации не меняет уже отправленный запрос: подсказка показывает +его прежний контекст, а новое место применяется к следующему вопросу. + +В снимок передаётся только идентификатор сохранённой записи. Core проверяет +доступность коллекции в MCP и право чтения конкретной строки до обращения к модели. +Значения полей ассистент запрашивает обычными инструментами с актуальными правами +пользователя. Несохранённые значения формы и новые записи в черновике не передаются. +Условия открытой таблицы не переносятся в контекст записи, кнопка применения фильтра +таблицы в карточке недоступна. + +API-контекст записи: `{ page: "items", workspaceId: null, collection: "articles", record: { id: "1" } }`. +`record` принимает только строковый `id` до 255 символов; Core дополнительно проверяет +тип первичного ключа. `record` и `table` взаимно исключают друг друга. Для выключенной +в MCP коллекции снимок не содержит запись, её данные и структуру прочитать нельзя. +Существующий контекст таблицы остаётся совместимым. + +Контекст страницы и доступ к данным управляются отдельно. Переключатель над вводом +включает только сведения о текущей странице, коллекции, записи и условиях таблицы. +В окне «Подключения ассистента» доступен переключатель «Доступ к данным и +подключениям»: он разрешает инструменты базы, личного Google и расширений для +следующих вопросов. Выключение не удаляет подключённые аккаунты и не останавливает +уже отправленный запрос. Проверки прав пользователя, публикации коллекций в MCP +и подтверждения записи в Google сохраняются. + +Новый необязательный параметр `/assistant/messages`: +`dataAccess: { enabled: true, workspaceId: null }`. С `context: null` инструменты +работают без сведений о текущей странице: модель должна явно найти/указать +коллекцию, условия поиска и фильтра не наследуются, предложение фильтра открытой +таблицы недоступно. `enabled: false` отключает все инструменты, независимо от +контекста. При переданном контексте `workspaceId` должен совпадать с ним; это +привязка разговора, а не дополнительные права и не ограничитель каталога коллекций. +Без `dataAccess` прежние клиенты сохраняют старое поведение: отсутствие контекста +отключает инструменты. Некорректный параметр отклоняется, а не приводит к включению. + +Рабочая история и резюме разделены по режиму доступа и пространству даже без +контекста страницы. Выключение доступа не скрывает уже видимую переписку, но для +нового ответа не используются сообщения и резюме режима с доступом к данным. +Возврат к прежнему режиму продолжает его историю. Повтор вопроса сохраняет снимок +страницы исходного запроса. Доступ при повторе не расширяется: если пользователь +его выключил после сбоя, инструменты остаются отключёнными и при повторе. + +Переписка виджета хранится в PostgreSQL и принадлежит текущему пользователю. +Кнопка «История сессий» открывает предыдущие диалоги; после перезагрузки +восстанавливается последняя сессия. «Новая сессия» создаёт отдельный пустой +контекст, сохраняя прежнюю переписку в истории. Есть удаление сессии и загрузка +более ранних сообщений. История других пользователей недоступна, в том числе +superuser. + +Неотправленный текст хранится отдельно для каждой сессии в памяти открытой +админки. При создании нового разговора поле ввода пустое, а черновик прежней +сессии остаётся доступен через историю с отметкой «Черновик» и короткую подсказку +«Вернуться к черновику» после создания нового разговора. Возврат к сессии +восстанавливает её ввод; загрузка более ранних сообщений его не меняет. +Ошибка создания или открытия сессии не очищает текущий текст. Текст, введённый +до появления первой сессии, привязывается к ней. Удаление сессии удаляет её +черновик после успешного ответа сервера. + +Черновики не отправляются в API или модели и не сохраняются в браузерное +хранилище. Сворачивание ассистента их сохраняет, перезагрузка страницы или +выход из открытой админки — очищает. Сохранение переписки в PostgreSQL +не включает неотправленный ввод. + +Хранятся тексты сообщений, публичный ход работы, привязка к контексту страницы, результат +завершения/остановки и метрики ответа. Исходный контекст страницы, результаты +инструментов, служебные токены подключений и внутренние рассуждения модели в +историю не копируются. Текст переписки сохраняется таким, каким его отправили +пользователь и ассистент. +Интерактивные карточки фильтров, форм и подтверждений записи доступны в текущем +ответе; при восстановлении истории они не активируются повторно. Для нового +действия нужно снова обратиться к ассистенту. + +Перед обращением к модели Core собирает завершённые пары сообщений текущего +контекста. При превышении 24 000 символов, 25 сообщений или 50 000 байт +сериализованной истории старая часть сжимается той же моделью без инструментов. +Резюме ограничено 4000 символами; последние сообщения и текущий вопрос +сохраняются в рабочем контексте. Полная переписка при этом остаётся в базе. +Резюме ведётся отдельно для контекста каждой страницы и каждой записи и не переносится в новую +сессию. Видимая переписка остаётся общей, но модель получает завершённые сообщения +только текущего контекста. Его содержимое является данными разговора и не даёт разрешений на действия. +Запросы сжатия входят в телеметрию и общий лимит восьми обращений к модели. + +POST /assistant/messages поддерживает сохранённый режим с conversationId, +messageId, content и необязательными settings/context. Повтор завершённого +запроса с тем же messageId возвращает сохранённый текст без повторного обращения +к модели. Изменение тела с тем же ID запрещено. Прежний формат messages остаётся +совместимым и не сохраняет диалог. API сессий: GET/POST /assistant/conversations, +GET/DELETE /assistant/conversations/:id; пагинация передаёт before. + +Одновременно в сессии выполняется один запрос. При отключении клиента +генерация останавливается; полученный к этому моменту текст сохраняется при +обработке остановки. После аварийного завершения процесса незавершённый ответ +отмечается прерванным по истечении одиннадцати минут. Сжатие и ответ делят +общий настроенный таймаут запроса. Генерации в фоне и возобновления +потока после перезагрузки нет. Удаление пользователя удаляет его личные сессии; +HISTORY_RETENTION_DAYS также очищает неактивные сессии по времени последнего +сообщения, если срок хранения явно включён. + +Лимиты общие для реплик через PostgreSQL: по умолчанию 100 запросов на пользователя в сутки UTC и два одновременных запроса. Настраиваются `ASSISTANT_DAILY_REQUESTS` и `ASSISTANT_CONCURRENT_REQUESTS`. В одном запросе максимум восемь обращений к модели, 100 000 байт сериализованного контекста перед каждым обращением и настроенный предел выходных токенов. Последнее доступное обращение резервируется для итогового ответа без инструментов, учитывая уже выполненное сжатие истории. При нехватке шагов модель должна объяснить, какие сведения ещё не получены. Если провайдер всё же возвращает вызов инструмента на итоговом шаге, Core его не выполняет. Это ограничение количества/объёма, не денежный бюджет провайдера. История сохраняется по умолчанию; `HISTORY_RETENTION_DAYS` явно включает очистку по возрасту. + +При подтверждённом отключении ассистента его закрытый виджет скрыт. Если панель +уже открыта, она сохраняет диалог и ввод и сообщает об отключении. Сетевая ошибка, +таймаут или некорректный ответ проверки статуса не скрывают ассистента: в панели +появляется сообщение и кнопка «Повторить». Повтор проверяет подключение и не +отправляет сообщение модели. До успешной проверки отправка новых вопросов +недоступна, но текст можно редактировать. Последняя известная конфигурация и +пользовательские параметры сохраняются при техническом сбое. + +401 и 403 показывают отдельные сообщения об истёкшей сессии или отсутствии прав. +При первоначальном подтверждённом отсутствии доступа закрытый виджет не +отображается. Ручная проверка и события фокуса/смены настроек отменяют предыдущую +проверку; устаревший ответ не заменяет новый. Проверка ограничена восемью секундами. + +Реальные обсуждения записей остаются +в плагине комментариев, а [уведомления](notifications.md) доступны отдельным +колокольчиком в шапке админки и работают независимо от AI. + +Данные, попавшие в контекст/результат инструмента, отправляются настроенному провайдеру. Выбор провайдера, объём передачи и договорные требования — эксплуатационное решение. Текст записи и результат плагина могут содержать prompt injection: модельный ответ не считается границей авторизации. + +## Google Workspace + +[Личное подключение Google Drive и Sheets](google-workspace.md) добавляет чтение по запросу и предложения записи с подтверждением. Активное подключение также разрешает чат человеку без data grants, сохраняя ограничения данных Core. + +Карточки Google-записи различают выполнение, отклонение, отказ до записи и +неизвестный результат. Итог можно открыть повторно и обновить отдельным чтением +статуса; подтверждение не повторяется. При наличии ссылка ведёт в файл или таблицу +Google. Просмотр ограничен сроком предложения и владельцем подключения; подробнее — +в [описании Google Workspace](google-workspace.md). + +Отмена ответа доступна через любую реплику Core: запрос сохраняется в PostgreSQL, +активный процесс проверяет его каждые 500 мс. При перезапуске процесса поток +прерывается; бесшовное продолжение генерации не поддерживается. Подготовленные формы +плагинов сохраняются в PostgreSQL на 20 минут и проверяют владельца при открытии. diff --git a/docs/ru/features/connections.md b/docs/ru/features/connections.md new file mode 100644 index 0000000..bf8a1a3 --- /dev/null +++ b/docs/ru/features/connections.md @@ -0,0 +1,112 @@ + + +# Подключения и защита секретов + +Суперпользователь управляет подключениями на `/admin/settings/integrations`. +Список открывает правую панель с безопасными параметрами, состоянием ключей, +проверкой соединения и включением/отключением. Изменения подхватываются ядром +без перезапуска, включая другие экземпляры, подключённые к той же базе. + +[Sentry и мониторинг](monitoring.md) настраиваются отдельно на +`/admin/settings/monitoring`. DSN используют те же write-only secrets, revision, +локальное шифрование/Yandex KMS и приоритет env. + + + +## Источники настроек + +Непустые параметры подключения в окружении Core имеют приоритет над настройками +админки. Вся соответствующая группа становится read-only: API также отклоняет +изменение с 409. Пустые примеры переменных не блокируют группу. Явное +`ASSISTANT_ENABLED=false` или `FILES_STORAGE=disabled` отключает подключение и +блокирует редактирование. Незавершённая env-конфигурация не дополняется +сохранёнными в админке ключами. +Изменение env-файла требует обычного перезапуска Core; изменения через админку +подхватываются автоматически. + +API возвращает адрес API, модель, бакет, регион, источник и признаки наличия +ключей. URL с логином, паролем, query или fragment не допускаются. Секреты +write-only: пропуск сохраняет прежний ключ, `null` удаляет, пустая строка запрещена. +Параметры и секреты сохраняются при отключении подключения. + +В read-only настройках S3 бакет, регион, endpoint и ID сервисного аккаунта можно +выделить или скопировать кнопкой рядом с полем. Ключи доступа не раскрываются и +не копируются; провайдер и включение подключения остаются недоступны для изменения. + +`GET /settings/integrations`, `PUT /settings/integrations/:section` и +`POST /settings/integrations/:section/test` требуют активной человеческой сессии +superuser. `section`: `storage`, `assistant`, `encryption`. Изменение передаёт +текущую `revision`: устаревший или конкурентный запрос получает 409. +Делегированные `files/update` и `assistant/update` не дают доступа к ключам +подключений. Лимит проверки соединения — 12 запросов в минуту по правилам +credential rate limit. + + + +## Файлы и ассистент + +S3 поддерживает бакет, регион, HTTPS endpoint и отдельные access/secret/session +keys. Yandex Object Storage также поддерживает федеративный обмен OIDC → IAM +без постоянного S3-ключа: в админке задаются бакет и ID сервисного аккаунта, +на сервере — `YC_OIDC_TOKEN_FILE` с обновляемым projected token. Ранее +настроенные `FILES_YC_*` продолжают работать. Создание bucket, федерации, +сервисного аккаунта и IAM-политик выполняет оператор установки. + +Проверка S3 выполняет HeadBucket и не загружает файлы. При наличии файлов +ядро запрещает смену местоположения хранилища; перенос файлов — отдельная +операция оператора. Отключение сохраняет метаданные, но загрузка и скачивание +байтов требуют включённого подключения. + +AI-провайдер поддерживает OpenAI-compatible Responses и Chat Completions, +base URL, модель, лимит ответа, таймаут и поддерживаемые настройки reasoning / +thinking. API-ключ хранится зашифрованным. Проверка запрашивает `GET /models` +без генерации: она проверяет доступность этого endpoint, но не гарантирует +доступ к конкретной модели. Некоторые совместимые провайдеры не реализуют +список моделей. Инструкции и телеметрия остаются на странице AI-ассистента. + + + +## Шифрование + +Все обратимые секреты, сохранённые этими настройками админки, хранятся в +PostgreSQL как версионированный ciphertext. Пароли и session/access tokens +продолжают храниться в виде хешей; OAuth signing keys и env-секреты остаются +конфигурацией оператора. +Секреты OAuth-приложений и OAuth protocol payload сохраняют существующее +шифрование ключом из `OAUTH_KEYS_FILE`; новый провайдер относится к подключениям +S3/AI и не меняет действующие OAuth-сеансы. + +| Режим | Настройки админки | Bootstrap на сервере | +| ---------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Локальный | Выбор локального провайдера | `SECRETS_LOCAL_KEY`: 32 случайных байта, base64url | +| Yandex KMS | ID симметричного ключа и сервисного аккаунта | `YC_OIDC_TOKEN_FILE`: projected OIDC token, федерация и права `kms.keys.encrypterDecrypter` на ключ | + +Локальный режим использует AES-256-GCM со случайным nonce и AAD, привязывающим +секрет к установке и полю. KMS использует native REST encrypt/decrypt с AAD +и краткоживущим федеративным токеном; ключ KMS не передаётся приложению. +Размер одного секрета ограничен 8000 байт UTF-8. Lockbox в этом механизме +не участвует: KMS защищает ключом ciphertext в базе. + +Смена провайдера или KMS key ID проверяет новый ключ, расшифровывает секреты +старым и шифрует новым в одной транзакции. Ошибка сохраняет прежнее состояние; +автоматического переключения на локальный ключ нет. Для защиты от потери +данных сначала смените провайдера в админке, затем при необходимости закрепите +его env-переменными `SECRETS_PROVIDER`, `SECRETS_YC_KMS_KEY_ID`, +`SECRETS_YC_SERVICE_ACCOUNT_ID`. Изменение env поверх несовместимого сохранённого +провайдера блокирует чтение секретов до восстановления согласованной настройки. + +Bootstrap-ключ не задаётся через админку. Его нельзя заменять случайным новым +значением поверх действующей установки: прежние ciphertext требуют прежнего +ключа. Резервная копия базы должна сопровождаться отдельной защищённой копией +локального ключа либо сохранённым доступом к прежним версиям ключа KMS. +После переноса в KMS старый локальный ключ может потребоваться для восстановления +старых резервных копий. + +Сохранённые ciphertext и секреты не включаются в журналы изменений или ответы +API. Событие аудита содержит только раздел, провайдер и число ключей. +Настройки можно просмотреть даже при недоступности KMS; работа подключений +требует успешного получения их секретов. Уже созданные клиенты могут держать +расшифрованные ключи в памяти до смены конфигурации или остановки процесса. + +См. [Yandex Workload Identity Federation](https://yandex.cloud/ru/docs/iam/concepts/workload-identity) +и [KMS symmetric encryption](https://yandex.cloud/ru/docs/kms/concepts/symmetric-encryption). diff --git a/docs/ru/features/data.md b/docs/ru/features/data.md new file mode 100644 index 0000000..0dc4ffa --- /dev/null +++ b/docs/ru/features/data.md @@ -0,0 +1,424 @@ + + +# Коллекции, записи и связи + + + +## Структура + +Главная страница `/` показывает доступные коллекции выбранного рабочего пространства +и открывает их записи по `/items/`. Управление структурой, полями и связями +находится отдельно в `/admin/collections` и доступно суперпользователю. Настройки +команды и системы остаются в `/admin/settings`. Маршруты Core API `/collections` +и `/items` от этого разделения не меняются. + +Коллекция описывает пользовательскую таблицу либо явно подключённое materialized view в `public`. Core-таблицы используют `asmblyr_`, таблицы расширений — `plugin__`; эти префиксы зарезервированы. Их структуру нельзя произвольно менять через API. + +Создание коллекции задаёт техническое имя, режим одной записи, первичный ключ (serial, bigserial, UUID либо ручная строка), системные даты. Отображаемое название, шаблон подписи записи, скрытие из навигации, MCP-описание и участие в MCP настраиваются отдельно. Скрытие не отзывает доступ по API. + +В редакторе поля выбирается обычный тип либо связь; настройки разделены по назначению. После создания нельзя менять техническое имя/тип обычного поля. `nullable` отвечает за допустимость NULL в БД; `required` — за непустое значение в API. Это разные ограничения. + +Системное состояние по умолчанию использует опубликовано/черновик/архивировано, новая запись — опубликована. Стандартное представление скрывает черновики/архив; это поведение отображения, а не ограничение доступа. Значения состояния настраиваются. + + + +## Работа с данными + + + +### Дополнительные поля системных коллекций + +В `/admin/collections` раздел «Показать системные коллекции» по умолчанию свёрнут. +Он содержит пользователей, файлы, политики доступа, рабочие пространства и сервисные +аккаунты. Сессии, токены, секреты и служебные таблицы не показываются. Встроенные поля +помечены замком: их имя, тип, ограничения и отображение нельзя менять этим редактором. +Сами системные коллекции нельзя переименовать или удалить. + +Суперпользователь может добавлять собственные физические колонки: обычные типы, +даты, JSON, теги, списки вариантов, файлы и связи «многие к одному» с обычными +пользовательскими коллекциями. Настройки открываются в стандартной правой панели. +Вычисляемые/условные правила и настройка поиска для этих полей +пока не поддерживаются. Все дополнительные колонки остаются nullable и необязательными, +чтобы регистрация пользователя и другие штатные операции не требовали новых значений. +Можно задать default; при добавлении он применяется к существующим строкам, +а его последующее изменение влияет только на новые записи. + +Кнопка «Дополнительные данные» открывает выбор существующей записи и форму её +пользовательских полей. У пользователей эти поля также доступны суперпользователю +в карточке профиля в `/admin/settings/users`, с отдельной кнопкой сохранения. +Создание и удаление системных записей остаётся в штатных +разделах. Удаление собственного поля требует ввода его имени и удаляет значения; +зависимые SQL-представления блокируют удаление, `CASCADE` не используется. + +Например, создайте коллекцию `departments`, затем в системной коллекции «Пользователи» +добавьте поле «Многие к одному» с именем `department_id` и выберите `departments`. +В дополнительных полях пользователя появится обычный селектор записей с поиском. +В БД создаются внешний ключ и индекс; удаление департамента очищает ссылку +(`ON DELETE SET NULL`), сохраняя пользователя. Удаление самой коллекции департаментов +блокируется, пока существует поле связи. Целевая коллекция и тип ключа после создания +не меняются. Системные/плагинные таблицы, singleton и materialized view не могут быть +целями этих связей; обратные O2M/M2M-поля и default для связи не поддерживаются. + +Чтобы создать такую связь через существующий POST configuration, передайте: + +```json +{ + "field": { + "name": "department_id", + "type": "relation", + "targetCollection": "departments" + }, + "presentation": { "label": "Департамент" } +} +``` + +Core API: `GET /system-collections`, `POST|PUT +/system-collections/:name/fields/:field/configuration`, `DELETE +/system-collections/:name/fields/:field`, `GET /system-collections/:name/records` +и `GET|PATCH /system-collections/:name/records/:id`. Имена `users`, `files`, `policies`, +`workspaces`, `service_accounts` являются отдельным системным каталогом. PATCH принимает +`{ "values": { "custom_field": "value" } }` и изменяет только зарегистрированные +пользовательские колонки. Встроенные значения и секреты не возвращаются; селектор +содержит ID и подпись (для пользователя — email). Все эти маршруты доступны только +активному человеческому суперпользователю, включая заполнение значений. Системные +таблицы не становятся обычными `/items`-коллекциями и не включаются в генерацию SDK. +Самостоятельное редактирование этих полей обычными пользователями не включено; +отдельная коллекция расширения профиля с обычными политиками продолжает работать. + +Владение колонкой фиксируется атомарно с DDL в `asmblyr_system_fields`. +Существующая колонка никогда не присваивается пользовательскому расширению. +При будущих миграциях Core нужно проверять конфликт имён и разрешать его явно, +с сохранением пользовательских данных. Откат реестра запрещён, пока в нём есть поля. +Изображения и галереи используют обычную файловую библиотеку; привязанные файлы +защищены от удаления. Интеграционные проверки: `node scripts/test.mjs core-system-collections`. + + + +### Связь обычной коллекции с пользователем + +В редакторе обычной коллекции выберите «Многие к одному», затем +«Пользователи · системная». Например, поле `manager_id` связывает департамент +с руководителем. Форма, фильтр и ячейки таблицы используют имя и почту пользователя; +само значение в записи и SDK — UUID. Системные таблицы не появляются в обычном каталоге. + +Создание через `POST /collections/:name/relations` доступно суперпользователю: + +```json +{ + "kind": "m2o", + "name": "manager_id", + "targetCollection": "@users", + "nullable": true, + "onDelete": "setNull" +} +``` + +`@users` — специальная цель связи, отличная от пользовательской коллекции `users`. +Core создаёт UUID-столбец, индекс и внешний ключ на системного пользователя. +Поддерживаются `restrict` и `setNull`; каскадное удаление записей, обратные поля, +значения по умолчанию, вычисления и зависимые фильтры этой связи не поддерживаются. +Фильтрация записей по UUID руководителя поддерживается; поиск внутри связанного +профиля и переходы вроде `manager_id.email` не предоставляются. + +Для поиска пользователей, получения подписей и назначения пользователя требуется +человеческая сессия с чтением раздела «Пользователи» (или суперпользователь). +Права записи исходной коллекции также обязательны. Сервисные ключи не получают +доступ к каталогу пользователей. Очистить необязательную связь можно с правом +изменения самого поля. Назначить можно только активного пользователя. +Сохранённую связь можно оставить без изменения, даже если пользователь отключён +или у редактора больше нет доступа к каталогу пользователей. + +`GET /users/references` возвращает только `id` и `label`, принимает `q`, `page`, +`limit` (1–100) и `ids` (до 100 UUID через запятую). Поиск показывает активных; +запрос известных `ids` также позволяет подписать уже сохранённые ссылки на +отключённых пользователей. Без права чтения подпись недоступна, UI показывает UUID. +`/items/asmblyr_users` остаётся закрытым. Миграция не добавляет пользовательские +поля автоматически; откат блокируется, пока существуют связи с пользователями. + +### Materialized views + +На `/admin/collections` суперпользователь может подключить готовое PostgreSQL +materialized view через кнопку-иконку «Подключить представление». В правой панели +выбирается источник из `public`, затем название, ключ строки, папка, подпись записи +и подписи полей. Подключение добавляется в выбранное рабочее пространство. +Сохранение регистрирует только метаданные Asmblyr; отмена ничего не создаёт. +После подключения обычные настройки полей и конструктор карточки позволяют +изменить форматы, переводы, порядок и расположение полей. + +Без отдельной раскладки карточка представления автоматически располагает короткие +поля в две колонки (в одну в узкой панели), а заголовок и длинное содержимое — на +полную ширину. Числа показываются с разделителями разрядов без потери точности +decimal/bigint, булевы значения — компактными метками. Указанные подписи, переводы, +форматы, скрытие и раскладка сохраняют приоритет. Автоматические подписи только +заменяют подчёркивания пробелами; перевод пользовательских полей и валюта не +угадываются. Это оформление при чтении: SQL и сохранённые метаданные не изменяются, +поэтому оно работает и для ранее подключённых представлений. + +Первый вариант требует заполненное представление с поддерживаемыми скалярными +полями и одним устойчивым ключом: UUID, положительный integer/bigint или непустая +строка не длиннее 255 символов. На ключе должен существовать действующий полный +одноколоночный UNIQUE индекс; составные, частичные и expression-индексы не подходят. +Имена источника и полей должны соответствовать обычным API-именам коллекций. +Несовместимые и уже подключённые объекты отображаются с причиной и недоступны +для выбора. Зарезервированные Core/plugin объекты не предлагаются. + +Представления доступны только для чтения: таблица, карточка, фильтры, сортировка, +поиск, SDK и MCP используют обычный read API. Для других пользователей нужны +отдельные read grants на поля и выходные строки. Права на исходные коллекции +не переносятся автоматически. В схеме SDK `sourceKind` равен `materialized-view`, +create/update/delete выключены, а генератор типов запрещает операции записи. + +В списке записей подключённого materialized view показана метка «Только просмотр». +Она отличает представление от редактируемой коллекции; настройка отображения, +поиск и фильтры остаются доступны в рамках прав пользователя. + +Создание, редактирование и удаление строк, bulk/commit с вложенными записями, +физические поля, индексы, CRUD-состояния и FK-связи запрещены сервером, включая +суперпользователя. Изменяемые метаданные отображения не изменяют SQL-объект. +«Отключить представление» удаляет подключение и его права/метаданные из каталога, +сохраняя само materialized view и данные в PostgreSQL. + +SQL-определение, создание и `REFRESH MATERIALIZED VIEW` управляются внешними +миграциями или процессом проекта. Перезагрузка таблицы выполняет только чтение. +Внешний процесс должен сохранять выбранный ключ уникальным, непустым и устойчивым +между пересчётами. Изменение колонок или потеря уникального индекса возвращают +`409 MATERIALIZED_VIEW_CHANGED`; источник нужно подключить заново. +Незаполненный источник возвращает `503 MATERIALIZED_VIEW_NOT_POPULATED`. +Время последнего внешнего пересчёта не показывается. Составные ключи, обычные views +и логические связи с представлениями не поддерживаются этим вариантом. + + + +### Типы и простые редакторы + +Поле `date` хранит календарную дату без времени и часового пояса. API, SDK и Kit +передают её строкой `YYYY-MM-DD`; форма показывает календарный ввод и не пересчитывает +сутки по часовому поясу устройства. Поддерживается диапазон `0001-01-01`–`9999-12-31`; +несуществующие даты и строки со временем отклоняются. `datetime` сохраняет прежний +контракт момента времени с часовым поясом. + +Обычный `bigint` — знаковое 64-битное целое, отдельное от `bigserial` первичного ключа. +API принимает и возвращает каноническую десятичную строку, например +`"9007199254740993"`. Диапазон — от `"-9223372036854775808"` до +`"9223372036854775807"`. JS-числа, дроби, экспоненциальная запись, ведущие нули и `-0` +отклоняются. Фильтрация и сортировка выполняются по числовой колонке PostgreSQL. + +В уже управляемой коллекции `character varying`/`varchar` распознаётся как `text`. +Это не меняет SQL-тип и ограничение длины колонки и не подключает произвольные +существующие таблицы автоматически. + +Для одного варианта используется штатный `select`: у `text` значения вариантов +строковые, у `integer` — числовые, включая `0` и отрицательные числа. Пример настройки: +`{ interface: "select", options: [{ value: 0, label: "Обычный" }] }`. +Сервер проверяет тип, допустимые значения и default. Удалённые варианты остаются +видимыми в старых записях; новые записи и изменения этого поля принимают только +текущий список. `multiselect` для JSON сохраняет строковые варианты. + +«Теги» в выборе нового поля создаёт JSON-колонку с `interface: "tags"`. Этот же +редактор можно выбрать для существующего JSON-поля. Ввод показывает компактные +плашки: Enter или выход из поля добавляет тег, крестик удаляет его. Просмотр +карточки показывает все теги, таблица — первые три и `+N` с остальными в подсказке. + +API и редактор принимают массив строк: до 100 тегов, каждый до 120 символов после +обрезки пробелов по краям. Пустые теги, повторы после обрезки и управляющие символы +отклоняются; регистр и порядок сохраняются. Обязательное поле требует хотя бы +одного тега; необязательное допускает `[]`, а nullable-поле — также `null`. +Default проходит ту же проверку. Настройка редактора не переписывает старые записи: +несовместимое значение остаётся видимым как исходный JSON и проверяется при явном +изменении этого поля. `/schema` экспортирует теги как `strings` без закрытого enum, +генератор SDK — как `string[]` (с `| null` для nullable-поля). После смены редактора +обновите схему SDK через `asm schema pull` и `asm generate`. + +Для radio-сценариев достаточно этого же списка. Обычный JSON редактируется в +текстовом поле с проверкой JSON перед сохранением и на сервере. Текстовое поле с +интерфейсом `url` принимает абсолютный HTTP(S)-адрес без учётных данных; компактная +кнопка рядом открывает корректную ссылку в новой вкладке. При просмотре ссылка +также доступна для открытия. + + + +### Редактирование и таблица + +Нажатие строки открывает боковой редактор; запись имеет собственный URL `/items//`. Без права update редактор работает на чтение. Создание использует такой же диалог. Выделение строк показывает плавающую панель действий. + +Таблица поддерживает страницы, сортировку, перестановку/скрытие и ресайз колонок, поиск, +фильтры и виды. При вводе поиска включается порядок по релевантности: точное +совпадение, начало названия с целым словом, целое слово внутри названия, затем +подстрока. Точный первичный ключ имеет наивысший приоритет. При равном совпадении +основные поисковые поля выше дополнительных; автоматический приоритет берётся +из шаблона/поля названия записи, иначе из первого текстового поля. Приоритет +настраивается в редакторе поля или связи. Поиск остаётся буквальным и без учёта +регистра; исправления опечаток и морфологии нет. + +Ширина меняется перетаскиванием правого края заголовка. Двойной щелчок возвращает +стандартную ширину. На разделителе работают стрелки влево/вправо (шаг 16 px), +Home/End (80/1200 px) и Delete/Backspace (сброс); Escape отменяет текущий жест. +Ширины сохраняются в личных настройках после отпускания указателя, восстанавливаются +после перезагрузки и входят в определение именованного вида. Скрытие и перестановка +сохраняют ширины; удаление поля или потеря права чтения исключают его настройки. +Это настройка отображения: право редактировать записи не требуется. + +`GET/PATCH /users/me/table-preferences/:collection` и `columns` в определениях +`/table-views/:collection` поддерживают необязательное `widths`, например +`{"order":["id","title"],"hidden":[],"widths":{"title":400}}`. +Значения — целые CSS-пиксели от 80 до 1200, ключи — доступные поля коллекции. +Пустая карта возвращает все ширины к стандартным; отсутствие ключа поля — его +стандартная ширина. Старые определения без `widths` остаются допустимыми. + +Участие поля в поиске и индекс — отдельные решения. Поиск по настроенной связи +ограничен одним уровнем, учитывает название связанной записи и не размножает +строки источника. Скрытые правами и чувствительные поля не участвуют в совпадении +или ранжировании. Связи с условными правами строк пока исключены из поиска. +Релевантность вычисляется в PostgreSQL до пагинации; равные результаты упорядочены +по столбцу и первичному ключу. Клик по заголовку включает обычную сортировку, +переключатель возвращает релевантность. Режим сохраняется в личных видах. +Глобальный поиск ранжирует записи внутри каждой коллекции; общий межколлекционный +рейтинг не вычисляется. Наличие индекса не гарантирует приемлемый план на любом +размере данных: проверяйте EXPLAIN на своей выборке. + +В HTTP/SDK параметр `order` принимает `relevance` или `field`. Запрос с `q` без +явных `sort`/`direction` использует релевантность; явный столбец сохраняет обычный +порядок. `order=relevance` вместе с `sort` задаёт столбец для равных результатов. +Без поисковой строки эффективный режим всегда `field`, он возвращается в `page.order`. + +Таблица связанных записей без явно заданных колонок показывает ключ, поле подписи +и до двух доступных M2O-полей для контекста (например, площадку категории). +Ссылка на коллекцию текущей карточки не дублируется. Закрытые и чувствительные +поля не добавляются в автоматический контекст; выбранный пользователем набор +колонок имеет приоритет. Пустое название остаётся пустым, вместо него не подставляется название +другого поля. Подписи M2O загружаются с обычными правами на связанные записи. + +Фильтры объединяют условия AND/OR, включая допустимые связанные поля. Нельзя использовать закрытое поле как канал фильтрации/сортировки. Максимум HTTP-страницы — 100; total передаётся строкой. + + + +## Связи и сохранение + +Есть M:1, обратные 1:M и M:N с промежуточной коллекцией. В форме можно выбирать существующие записи и создавать новые; права каждой затронутой коллекции/поля проверяются отдельно. Перенос между родителями требует соответствующего изменения связи. + +`POST /items/:collection/commit` сохраняет корневую запись и связанные изменения одной транзакцией. Ошибка откатывает также историю и транзакционные hooks. Draft ограничен 100 изменениями и глубиной 5. Последовательные обычные HTTP-запросы одной транзакцией не становятся. + + + +## Правила поля и зависимые формы + +Во вкладке «Правила» бокового редактора доступны скрытие в форме, режим чтения, +условная обязательность и копирование значения из M2O-связи. `rules.hidden` +меняет только форму; доступ через API по-прежнему задаётся политикой. +`rules.readonly` отклоняет явную запись поля через штатный writer, включая +superuser. Default при создании применяется без ручной записи readonly-поля. + +Пример `presentation` для поля `note`: + +```json +{ + "rules": { + "requiredWhen": { + "mode": "all", + "rules": [{ "field": "enabled", "operator": "eq", "value": true }] + } + } +} +``` + +Условие использует другие скалярные поля этой коллекции: `eq`, `ne`, `empty`, +`notEmpty`, объединение `all`/`any`, до 12 правил. `false` и `0` не пусты. +Core проверяет итоговую запись с defaults и прежними значениями при PATCH. +Обязательное значение — не NULL и не пустая/пробельная строка. Необходимые +скрытые поля раскрываются при проверке формы; серверная ошибка сохраняет черновик. + +`rules.computed: { relation: "project", field: "client" }` копирует `project.client` +при сохранении. Копируются скалярные поля. Типы полей должны совпадать, а для FK — ещё и целевая коллекция. +Копирование ограничено одной прямой M2O; цепочки, циклы и произвольные выражения +отклоняются. Явная запись computed-поля запрещена. Пользователю нужны права +чтения источника и записи изменяемого результата. Отсутствующий источник связи +даёт NULL с обычной проверкой nullable/required. Это вычисление при сохранении, +не динамическая колонка: изменение исходной записи само по себе не обновляет +все зависимые записи. Чувствительный источник разрешён только для чувствительного результата. + +M2O-поле может задать `relationFilter`, например выбор города по региону формы: + +```json +{ + "relationFilter": { + "logic": "and", + "children": [ + { + "field": "region", + "op": "eq", + "value": { "kind": "field", "field": "region" } + }, + { + "field": "active", + "op": "eq", + "value": { "kind": "literal", "value": true } + } + ] + } +} +``` + +Список кандидатов использует обычные фильтры и права строк/полей. До заполнения +зависимости выбор закрыт. Изменение родителя в форме сбрасывает зависимые ссылки, +включая следующий уровень. Core проверяет новое значение при создании и при +изменении самой связи либо её зависимости. Несоответствующая связь отклоняется; +сервер сам не выбирает и не очищает её. Старый архивный выбор разрешено сохранить +при редактировании другого поля. Литералы скалярные или массив скаляров, зависимости +— скалярные поля формы. AND/OR ограничены 30 узлами и глубиной 3. Связанный путь +фильтра — один переход; условия прав строк при таком переходе по-прежнему +отклоняются безопасно. Условие с недоступной зависимостью не раскрывается в каталоге. + +Эти проверки общие для HTTP, SDK, Kit, массового изменения и вложенного commit. +Ошибочный ряд откатывает транзакцию целиком. Прямой SQL не проходит через writer. + +Шаблоны подписей поддерживают до двух M2O-переходов, например +{{project.client.name}} · {{title}}. Каталог предлагает допустимые поля. +Чтение проверяется на каждом переходе, включая условные права строки; при +недоступном поле/записи используется обычная подпись или ключ. Подстановка +использует пакетные запросы. To-many пути и вычисления внутри шаблона не поддержаны. +Панели обратных 1:M и M:N связей можно разместить в секции или вкладке конструктора +формы существующей записи; повторной панели внизу не будет. В новой записи +размещённая панель показывает подсказку до первого сохранения. + + + +## Защита от перезаписи + +Боковой редактор запоминает исходные значения и передаёт `expectedValues` только для изменяемых полей. Core проверяет их под блокировкой строки в той же транзакции. Для вложенных записей, FK и атрибутов промежуточной связи применяется такая же проверка. Системные даты не нужны: сравниваются фактические значения, поэтому изменение через обычный PATCH, плагин или прямой SQL тоже обнаруживается при следующем защищённом сохранении. + +Изменения разных полей сохраняются независимо. Если другое сохранение уже записало точно такое же значение, повтор считается успешным. Несовпадение одного изменяемого поля возвращает `409 ITEM_CHANGED` и откатывает весь commit. UI оставляет черновик в панели, загружает доступные актуальные значения и предлагает выбрать «Ваше значение / Актуальное». После выбора пользователь продолжает редактирование и сохраняет явно; повторная запись снова проверяет исходные значения. При ошибке чтения или изменении доступа черновик остаётся в открытом окне. + +SDK и Kit могут включить эту проверку через `items.commit` с `expectedValues` для каждой существующей изменяемой записи. Пропуск параметра совместим с прежними клиентами. Обычные `update`, массовые изменения и прямой SQL сами по себе не требуют исходных значений. Это проверка содержимого изменяемых полей, не счётчик версий или блокировка всей карточки. Возврат поля к исходному значению не считается конфликтом. JSON, rich text и массивы сравниваются целиком; состав связей меняется явными attach/detach, без проверки снимка всего списка. Черновик хранится в памяти панели и не переживает перезагрузку страницы. + +Проверка не раскрывает актуальные значения в ошибке и требует права чтения и записи каждого сравниваемого поля, включая условные grants. + + + +## Кто сейчас здесь + +В существующей карточке записи показываются компактные аватарки участников, открывших эту же запись. Наведение или фокус раскрывает имя; один пользователь с несколькими окнами получает одну аватарку. Видны три аватарки и счётчик остальных. Новой несохранённой записи присутствие не назначается. + +UI обновляет список раз в 5 секунд. Имя и аватар берутся из профиля; без заданного имени отображается «Участник». Core хранит отметки в PostgreSQL на 30 секунд и удаляет их при уходе; закрытие окна без сети покрывает срок действия. Сессии, которые отозваны или истекли, и отключённые пользователи исключаются сразу при следующем чтении списка. Ошибка присутствия скрывает аватарки и не мешает редактированию. + +Каждое обновление проверяет актуальные права чтения конкретной записи. Имена, аватары и число окон видны другим участникам этой записи; email, URL с поиском/фильтрами, сессии и значения записи не передаются. Присутствие не означает, что человек редактирует поле. Плагинные коллекции используют обычное присутствие записей. + + + +## История и удаление + +История item events хранит actor, время и изменения значений. + +Текст/email с `presentation.sensitive: true` маскируется в форме просмотра и +таблице, редактируется как password и не используется в подписи записи. +Вместо значения в `before`/`after` истории сохраняется `[REDACTED]`, включая +create/update/delete, bulk и nested commit. Включение настройки в той же +транзакции безвозвратно очищает прежние события этого поля; выключение не +восстанавливает их. Default чувствительного поля запрещён. Это не шифрование +и не write-only API: разрешённый read по-прежнему возвращает живое значение, +его ограничивают права поля. Копии БД, прямой SQL и внешние логи проверяет оператор. + +Чтение истории проецируется на разрешённые поля; события, затрагивающие только закрытые поля, не раскрываются. История схемы и безопасный rollback поля с восстановлением данных — отдельная нерешённая задача. + +Прямой SQL и каскады, исполняемые самой БД, не проходят через обычный writer и не обеспечивают такую же историю/hooks. Удаление коллекции/поля требует superuser и проверки влияния; backup остаётся обязанностью эксплуатации. + +Источники: `apps/core/src/collections`, `apps/core/src/items`; контракты — `packages/contracts/src/items.d.ts`, SDK — [справочник](../reference/sdk-guide.md). diff --git a/docs/ru/features/files.md b/docs/ru/features/files.md new file mode 100644 index 0000000..7edb936 --- /dev/null +++ b/docs/ru/features/files.md @@ -0,0 +1,56 @@ + + +# Файлы и объектное хранилище + +Файл — отдельная сущность с метаданными в PostgreSQL и содержимым в настроенном S3/YC backend. Поля коллекций хранят ссылки на файлы; пользовательский доступ к содержимому проверяет Core. + +| Операция | Текущий допуск | +| -------------------------------------- | ------------------------------------------------------------ | +| Список библиотеки, история файла | Человек + files/read (либо update); superuser | +| Загрузка, изменение, удаление | Человек + files/update; superuser | +| Метаданные/скачивание связанного файла | files/read, superuser либо read доступ к полю с этой ссылкой | +| Resolve списка ссылок | Только разрешённые для principal ссылки | + +Скачивание по умолчанию — attachment/octet-stream; preview выставляет ограничительную CSP и nosniff. Ограничение загрузки — 25 MiB; загрузка буферизуется в памяти. Секреты хранилища не передаются клиенту. + + + +## Настройка + +Поддерживается Yandex IAM token provider, включая настройку федеративного обмена. Это инфраструктурная конфигурация Core; пользовательская федерация GitLab CI для входа в Collaborative — другой механизм. + +Параметры хранилища задаются в [подключениях админки](connections.md) либо в окружении Core по `apps/core/.env.example`. Env-конфигурация блокирует редактирование группы в админке. Bucket и IAM создаёт оператор своей установки. + +Для выбора новой файловой ссылки в поле нужен files/read и право записи этого поля. Уже сохранённую ссылку можно читать через разрешённую запись без доступа ко всей библиотеке. При отзыве files/read библиотека и выбор новых ссылок закрываются; интерфейс загрузки/редактирования отключён для files/read без update. + +Библиотека общая для одной установки команды: files/read открывает все её файлы, а не только собственные загрузки. Отдельных прав на каждый объект пока нет. Workspaces не разделяют файлы на tenants. Локальные инструменты установки сохраняют согласованные PostgreSQL и S3 копии при остановленных авторах; [backup/restore](../development/operations.md) описан отдельно. + + + +## Публичная ссылка на один файл + +Новые и существующие файлы имеют `visibility: "private"`. Человек с `files/update` +(либо superuser) может изменить видимость одного готового файла на `public` через +боковой редактор или `PATCH /files/:id`. После сохранения появляется кнопка +копирования ссылки. Неправильное значение видимости отклоняется. + +`GET /public/files/:id/content` в Core и `/api/public/files/:id/content` в UI +возвращают байты опубликованного файла без сессии. `?preview=1` разрешает только +уже поддержанные растровые превью. Публичного списка и метаданных нет, приватные +маршруты сохраняют свои проверки доступа. Это явная публикация для любого +обладателя ссылки, а не персональное приглашение. Ссылка не содержит IAM/SDK +токенов, bucket остаётся приватным. + +При возврате к `private`, удалении либо состоянии не ready публичный маршрут +отвечает 404 до обращения к хранилищу. Ответы используют `no-store`, CSP и +`nosniff`; загрузка по умолчанию — attachment. Отзыв останавливает будущие +скачивания, но не отзывает уже полученные байты или начатый поток. +Публичный маршрут обслуживает Core; аутентификация для него не требуется. + + + +## Ограничения + +Открыто: квоты общего объёма, проверка вредоносного содержимого, преобразования/превью как сервис и reconciliation оставшихся объектов. PostgreSQL и S3 не образуют одну транзакцию. Приватность и резервирование внешнего bucket проверяет оператор конкретной установки. + +Источники: `apps/core/src/files`, `apps/core/src/files/storage`. ACL внешнего bucket проверяет оператор своей установки. diff --git a/docs/ru/features/google-workspace.md b/docs/ru/features/google-workspace.md new file mode 100644 index 0000000..9749905 --- /dev/null +++ b/docs/ru/features/google-workspace.md @@ -0,0 +1,134 @@ + + +# Google Workspace в ассистенте + +Опциональный пакет `@asmblyr-collaborative/plugin-google-workspace` добавляет поиск файлов +Google Drive, чтение текстовых файлов и Google Docs, структуру Google Sheets +и чтение диапазонов. Запись готовится как предложение с конкретными данными; +пользователь проверяет его в правой панели и нажимает «Подтвердить изменение». + + + +## Настройка установки + +В Google Cloud включите Drive API и Sheets API. Создайте отдельный OAuth web client +для ассистента с External audience. Добавьте redirect URI +`https://admin.example.com/connections/google/callback`; для локальной разработки +допустим `http://localhost:3000/connections/google/callback`. + +В `/admin/settings/integrations` откройте Google Workspace, задайте client ID, +redirect URI и write-only client secret, затем включите подключение. +На сервере должен быть настроен [ключ local или Yandex KMS](settings.md). +Проверка конфигурации проверяет параметры и шифрование. Корректность client secret +и разрешённого callback проверяется при реальном OAuth-подключении. + +Альтернатива — `GOOGLE_WORKSPACE_ENABLED=true`, `GOOGLE_WORKSPACE_CLIENT_ID`, +`GOOGLE_WORKSPACE_CLIENT_SECRET`, `GOOGLE_WORKSPACE_REDIRECT_URI` в конфигурации Core. +Наличие любого из этих env-параметров делает всю группу readonly в админке. +Пакет включается в корневом `asmblyr.plugins`, capability `connections.google` +подтверждается в `asmblyr.pluginPermissions`. Исключение пакета убирает инструменты, +сохраняя личные подключения до явного отключения. + +Запрашиваются `openid`, `email`, `profile`, полный `drive` и `spreadsheets`. +Это доступ к разрешённым пользователю файлам, включая общие диски согласно их ACL. +В режиме Google Testing подключаются только тестовые пользователи; refresh tokens +для этих scopes обычно истекают через семь дней. Для публичного приложения +нужны проверка Google и опубликованная политика конфиденциальности; администратор +Workspace может запретить стороннее приложение. См. [OAuth Google](https://developers.google.com/identity/protocols/oauth2) +и [scopes Drive](https://developers.google.com/workspace/drive/api/guides/api-specific-auth). + + + +## Личное подключение + +Пользователь открывает значок подключений в ассистенте или +`/settings?tab=applications`, нажимает «Подключить Google» и предоставляет согласие +в Google. Подключение принадлежит активному человеку и не доступно сервисным +аккаунтам, другим пользователям или администратору через интерфейс. Вход в Asmblyr +и Google API-подключение — разные потоки: email не связывает профили автоматически. + +OAuth использует authorization code, PKCE S256, state, nonce, проверку issuer, +ID token и subject через `openid-client`. Поток привязан к пользователю, конфигурации +клиента и HttpOnly browser cookie, действует десять минут и принимается один раз. +Callback требует действующую сессию Asmblyr. Отключение во время callback отменяет +поток. Токены и PKCE proof хранятся зашифрованными в PostgreSQL, с отдельным AAD +на запись; смена local/KMS пересчитывает и эти секреты в одной транзакции. +При недоступности KMS нет автоматического перехода на local. + +Токены обновляются на сервере; `invalid_grant`, смена OAuth client или отзыв доступа +требуют подключения заново. Токены не передаются модели, плагину или браузерному +JavaScript. Отключение удаляет локальные секреты и предложения, затем пытается +отозвать Google grant. При неудаче UI просит также отозвать его в аккаунте Google. +Недоступность шифрования отражается флагом `unavailable` в личном статусе; +метаданные аккаунта и локальное отключение остаются доступны. +Уже выполняющийся внешний запрос нельзя гарантированно отменить. + + + +## Инструменты и ограничения + +| Инструмент | Поведение | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `find-files` | Поиск по имени, 25 файлов на страницу, source URLs и следующий page token | +| `read-text` | Текстовые файлы и plaintext export Google Docs, до 30 КБ с явной отметкой обрезки | +| `describe-sheet` | Название таблицы, имена и размеры вкладок | +| `read-cells` | Явный A1 прямоугольник до 500 ячеек, ответ до 36 КБ | +| `propose-write` | Подготовка создания/замены текстового файла, переименования, перемещения в корзину, создания таблицы, замены ячеек или добавления строк | + +Значения Sheets записываются в режиме RAW: строки не становятся формулами. +Размер прямоугольника должен совпадать с массивом значений. `append_cells` +добавляет строки после логической таблицы, найденной Google в указанном диапазоне, +и сдвигает последующие строки; это отображается в подтверждении. +Редактирование содержимого нативного Google Docs, произвольные бинарные документы, +OCR, Slides, файловые вложения и постоянная индексация в этот пакет не входят. + +Инструменты доступны только при активном личном подключении, одобренной capability +и включённом доступе к данным в «Подключениях ассистента». Контекст страницы +включается отдельно и для Google не обязателен. Подключение позволяет пользоваться чатом без +прав на коллекции, но не добавляет права чтения данных Core. Коллекция, выключенная +из MCP, остаётся закрытой; личные инструменты Google продолжают работать. +Документы и ячейки являются недоверенными данными. Запрошенные фрагменты передаются +настроенному AI-провайдеру; сам grant не запускает обход всех файлов. + +Запросы Drive используют `www.googleapis.com/drive/v3`, а Sheets — +`sheets.googleapis.com/v4`, включая создание таблиц и запись ячеек. +Ошибки инструментов различают необходимость переподключения, отказ Google, +отсутствующий файл, ограничение частоты запросов, неверные параметры и сетевой сбой. +Сообщения берутся из фиксированного списка: содержимое ошибок Google и токены +модели не передаются. Сбой запроса не доказывает отсутствие доступа к файлу. + +Предложение действительно двадцать минут, до 32 ожидающих на пользователя. +Его данные зашифрованы, подтверждение привязано к тому же подключению и владельцу. +Модель не получает инструмент подтверждения; HTTP подтверждает сохранённый payload, +не принимая новое содержимое от браузера. Перед записью проверяются версия файла +или значения диапазона. Между этой проверкой и Google write остаётся небольшое +окно: API не предоставляет общей транзакции с PostgreSQL. + +Подтверждение принимается один раз. Сбой после отправки Google write даёт +`uncertain`; нужно проверить файл перед новым предложением. Перезапуск во время +записи оставляет `executing`, повтор недоступен. Журнал содержит только провайдера, +вид операции и результат. По истечении TTL предложения и flow удаляются очисткой. + +Карточка показывает отдельные исходы: «Выполнено», «Отклонено», «Не выполнено» +и «Проверьте результат». Последний используется для `uncertain` и оставшегося +`executing`: он не утверждает, что Google не изменил файл. У изменившейся цели +показывается причина отказа, предлагается подготовить новое изменение. +Кнопка «Посмотреть результат» остаётся доступной после завершения; в правой панели +можно просмотреть исходное предложение, краткий итог и обновить статус. +«Обновить статус» выполняет только GET, повторной записи нет. При потере ответа +подтверждения UI сверяет состояние с сервером и сохраняет запрет второй отправки. + +Для успешной операции Core сохраняет зашифрованный ограниченный итог: ссылку на +файл/таблицу, число изменённых ячеек/строк и диапазон, если Google их вернул. +Произвольный ответ провайдера не сохраняется; ссылки строятся из ID файлов на +доменах Google. У существующей цели ссылка доступна и при неизвестном результате. +Созданный файл получает ссылку только после подтверждённого ответа с его ID. +Отклонение сохраняет статус `cancelled` и не отправляет запрос записи в Google. +Просмотр предложения и итога ограничен прежними двадцатью минутами, владельцем +и тем же подключением. После истечения или отключения аккаунта серверный просмотр +недоступен. Восстановленная история чата не активирует старые карточки. + +Контракты Core `/connections/google`, OAuth start/callback и owner-bound +`/connections/google/writes/:id` описаны в [HTTP API](../reference/http.md). +Плагин использует обычные H3 file routes и `defineModelContext`; схемы генерирует Kit. +Проверки с временной БД и локальным Google adapter: `node scripts/test.mjs core-connections`. diff --git a/docs/ru/features/identity.md b/docs/ru/features/identity.md new file mode 100644 index 0000000..31cdebc --- /dev/null +++ b/docs/ru/features/identity.md @@ -0,0 +1,116 @@ + + +# Пользователи и вход + +Первый superuser создаётся в UI с setup secret. Следующие пользователи — через приглашение либо настроенный SSO flow. Email уникален; одинаковый email не означает разрешение автоматически объединить две внешние личности. + + + +## Профиль и дополнительные поля + +Базовый профиль включает почту, отображаемое имя, необязательные имя и фамилию, описание «О себе», аватар и даты создания, изменения профиля, последнего входа и активности. UI выбирает отображаемое имя, затем имя с фамилией, затем почту. Существующее отображаемое имя сохраняется. Профиль не позволяет изменять почту, статус, superuser, credentials или системные даты. + +Аватар загружается кнопкой **Загрузить аватар** через `POST /users/me/avatar` в настроенное файловое хранилище, включая S3: PNG, JPEG, GIF или WebP до 2 MiB. Внешнюю ссылку на изображение в форме вводить не нужно. Право на библиотеку файлов для собственного аватара не требуется. Загрузка создаёт приватный файл; `PATCH /users/me` с `avatarId` прикрепляет его. При редактировании другого пользователя загруженный файл прикрепляется через `PATCH /users/:id/profile`, который требует `users/update`. Несохранённые загрузки остаются в библиотеке и могут быть удалены администратором. Собственный аватар видит его владелец; чужой — пользователь с доступом к разделу пользователей либо обычным правом на файл. Прикреплённый файл удалить нельзя. Отдельный `pictureUrl` остаётся в API для совместимости и публичного OAuth profile; форма его не редактирует, а приватный аватар туда не экспортируется. + +В настройках внешнего вида сохраняется IANA-часовой пояс пользователя, например `Asia/Yekaterinburg`. `null` означает часовой пояс устройства. Он применяется к датам профиля и сессий; значения в БД и API остаются UTC timestamps. `lastLoginAt` обновляется при выдаче новой сессии, а refresh его не продлевает. `lastActiveAt` приблизительный: успешные запросы с пользовательским access token обновляют его не чаще раза в пять минут; сервисные ключи учитываются отдельно. + +Должность, департамент, организация и руководитель не встроены в ядро. Для таких данных superuser создаёт обычную коллекцию с UUID-ключом и выбирает её в **Настройки → Пользователи → Расширение профиля**. Можно добавить любые поддержанные поля, M2O/O2M/M2M-связи и настроить форму стандартным редактором коллекций. Рекомендуется начать с пустой коллекции. У существующих записей ID должны совпадать с ID пользователей; один пользователь имеет одну запись профиля. Служебный FK защищает привязку к пользователю; переименование коллекции сохраняет настройку. + +Дополнительные данные открываются из собственного профиля и бокового окна пользователя. Первое сохранение через `PATCH /users/me/extension` либо `PATCH /users/:id/extension` создаёт запись с ID пользователя, последующие — изменяют её. Обычный `POST /items/:collection` для коллекции профиля не создаёт профили со случайными ID. Уже созданные записи и связи редактируются стандартным редактором записей. Отключение расширения сохраняет данные и FK; перед удалением коллекции расширение нужно отключить. + +Настройка расширения сама по себе не выдаёт права. Пользователю необходимы обычные права коллекции: `read`, `create` для первого заполнения и `update` для изменений. Для личных данных задайте row filter: поле `id` (либо имя вашего UUID-ключа), оператор «Равно», параметр «Текущий пользователь» для всех этих действий. Права на связанные коллекции и файлы проверяются отдельно. Управление чужим профилем дополнительно требует `users/read` или `users/update`; скрытые поля и строки не раскрываются. Валидация, вычисляемые поля, audit и mutation hooks совпадают с обычными коллекциями. Схема расширения входит в `GET /schema` и стандартную генерацию CLI — [пример SDK](../reference/sdk-guide.md#профиль-пользователя). + + + +## Настраиваемая информация профиля + +В **Настройки → Пользователи → Вид профиля** superuser настраивает отдельный блок +для чтения в личном профиле и карточке пользователя. До 12 строк: подпись, источник, +порядок и галочка **Показывать владельцу профиля** (по умолчанию выключена). +Пустой заголовок использует переведённое «Дополнительная информация». + +Источники — дополнительные поля системной коллекции `users`. Можно пройти до двух +одиночных M2O-связей и выбрать конечное значение (например, отдел → название или +отдел → руководитель). Поддержаны текст, email, числа, флаги, даты, теги и ссылка на +пользователя, которая раскрывает только отображаемое имя с fallback на ФИО/почту. +Фотографии и произвольные поля связанного пользователя не передаются. Системные +колонки, sensitive-поля, файлы, произвольный JSON, связи ко многим и коллекции +плагинов не доступны как источники. Настроенные значения читаются из исходных +записей при загрузке профиля, без копирования. + +Это отдельное явное разрешение на проекцию: управляющие с `users/read` видят +настроенные строки; владелец — только строки с `selfVisible`. Для этих точечных +значений не требуются обычные права коллекций. Доступ к справочникам, другим +профилям и редактированию при этом не выдаётся. Выбирайте только сведения, +предназначенные для такой аудитории. Настраивать проекцию может только superuser. + +Удалённый источник, изменение цели связи, пометка sensitive или удаление и +повторное создание столбца скрывают затронутую строку до повторной настройки. +Имена полей и физическая идентичность столбцов входят в проверку привязки; +переименование источников и восстановление БД с другими OID требуют повторного +сохранения конфигурации. Отсутствующее значение показывается как «Не указано». + +API: `GET|PUT /users/profile-display`, `GET /users/profile-display/sources`, +`GET /users/me/profile-display`, `GET /users/:id/profile-display`. +Проверки включены в `node scripts/test.mjs core-profile`. + + + +## Пароль и сессии + +Пароли хранятся как Argon2id hash отдельно от пользователя. Access/refresh — случайные непрозрачные токены; в БД только SHA-256 hashes. Access живёт 15 минут, сессия до 30 дней; refresh вращается. Повтор использованного refresh отзывает сессию. + +Для браузера Core выдаёт отдельную непрозрачную HttpOnly cookie `asmblyr_session` +(SameSite=Lax, Secure при HTTPS). В БД хранится только SHA-256 hash. Сессия действует +до 30 дней без продления срока; каждый запрос проверяет срок, отзыв и статус +пользователя. Refresh-токены браузеру не выдаются: параллельные вкладки и реплики +используют одну проверяемую в БД сессию без гонок refresh. Профиль позволяет менять +собственные данные/пароль, смотреть сессии и отзывать их. + +Вход браузера: `/api/auth/browser/login`, `/api/auth/browser/invitations/claim`, +`/api/auth/browser/passkeys/options` и `/api/auth/browser/passkeys/login`. Ответ +успешного входа — `{ok:true}`, секрет передаётся только через Set-Cookie. Выход: +`POST /api/auth/browser/logout`. Записывающие запросы с cookie и все browser auth +операции требуют точного `Origin`, равного `AUTH_UI_URL`; чужой или отсутствующий +Origin отклоняется. Явный Authorization имеет приоритет над cookie. + +При обновлении с версии с BFF нужно повторно войти: прежние access/refresh cookies +не используются. Данные, учётные записи, passkey и API-сессии сохраняются. + +Администратор указывает email и передаёт пользователю ссылку `/invite#token=…` любым выбранным способом. Ссылка одноразовая, живёт семь дней и входит в аккаунт без почтовой доставки и обязательного пароля. Fragment удаляется из адреса страницы до отправки; GET сам по себе ссылку не активирует. Ссылка подтверждает владение секретом приглашения, а не почтовым ящиком. После входа можно добавить пароль, passkey или привязать SSO. Для повторного входа необходимо сохранить хотя бы один из этих способов; повторная активация использованной ссылки запрещена. + +Приглашения требуют section `users/update`. Менеджер повторно выдаёт приглашение только ещё не активированному аккаунту без пароля, passkey и SSO, все политики которого входят в его разрешённый набор. Собственный аккаунт и пользователи с личным правом делегирования закрыты для такого действия. UI скрывает недоступную кнопку; сервер перепроверяет состояние. [Правила делегирования](access.md#ограниченное-назначение-политик) описаны отдельно. + + + +## Passkey и восстановление + +WebAuthn реализован через SimpleWebAuthn: discoverable credential, обязательное подтверждение пользователя на устройстве, проверка challenge, подписи, origin, RP ID и счётчика. Challenge живёт пять минут, одноразовый; регистрация привязана к конкретной сессии. До десяти ключей на аккаунт. Закрытый ключ остаётся на устройстве. Добавление/удаление и установка первого пароля требуют входа за последние пять минут; refresh не продлевает этот срок. Последний доступный способ входа удалить нельзя. + +Активному обычному пользователю superuser со свежей сессией выдаёт одноразовую ссылку восстановления на 30 минут. При её использовании отзываются прежние сессии, пароль, passkey, SSO-привязки и OAuth-согласия. Выдача ссылки сама по себе ничего не сбрасывает. Такое восстановление не делегируется менеджерам и не применяется к superuser. Потерявшего доступ администратора восстанавливает оператор на сервере через `scripts/operations/admin-recover.mjs`; инструкция в [эксплуатации](../development/operations.md). + +`AUTH_UI_URL` задаёт точный публичный origin и обязателен в production. HTTPS обязателен, кроме локального localhost. Опциональный `PASSKEY_RP_ID` должен совпадать с hostname. При смене домена нужны новые ключи. Core дополнительно связывает passkey challenge с HttpOnly cookie браузера. + + + +## Подключение CLI + +`asm connect` открывает `/sdk/connect` в админке для входа и подтверждения получения +схемы. CLI использует PKCE S256, одноразовый код на 60 секунд и точный callback на +`127.0.0.1` со случайным портом. Доступ `schema:read` действует 10 минут, связан с +активной сессией и принимается только `GET /schema`. Он не даёт читать или менять +записи. CLI не сохраняет токен; следующий онлайн-запрос снова требует входа либо +отдельного API-ключа из env/stdin. Подробнее — [CLI](../reference/cli-guide.md). + +## SSO + +Поддержаны OIDC и OAuth через конфигурацию провайдеров. Ключ провайдера задаёт URL +`/sign/sso//callback` на публичном домене. Core обслуживает начало входа +`POST /sign/sso/:provider` и callback, проверяет browser proof и создаёт сессию. +Программные endpoints `/auth/sso/:provider/start` и `/callback` также сохраняются. + +OIDC использует discovery и проверку issuer, state, PKCE и nonce. Привязать новый провайдер можно из уже подтверждённой учётной записи; совпадение email само по себе не связывает аккаунты. Секреты провайдеров остаются в серверной конфигурации. + +Ограничения: нет отдельного MFA flow, общего LDAP/SAML-коннектора и самостоятельного email-восстановления пароля. SMTP не требуется для первой беты. Общий лимит credential endpoints хранится в PostgreSQL; дополнительные ограничения маршрутов локальны процессу. На встроенном публичном listener Core видит соединение браузера напрямую. За внешним reverse proxy общий лимит источника относится к адресу proxy: внешним forwarded headers Core не доверяет. + +Источники: `apps/core/src/auth`, `apps/core/src/auth/browser`. Авторизацию для внешних приложений описывает [интеграционная страница](integrations.md). diff --git a/docs/ru/features/integrations.md b/docs/ru/features/integrations.md new file mode 100644 index 0000000..32f9423 --- /dev/null +++ b/docs/ru/features/integrations.md @@ -0,0 +1,102 @@ + + +# Сервисы, федерации и OAuth-приложения + + + +## Сервисные аккаунты + +Аккаунту назначают политики. Секрет ключа показывается один раз, в БД хранится hash. `POST /auth/service-token` меняет ключ на короткий Bearer token до 15 минут, без refresh. Отзыв ключа/отключение аккаунта проверяется при последующих обращениях. + +В карточке каждого ключа видны дата и время выпуска, срок действия, последняя +активность и число обращений к API. Последняя активность учитывает успешный +обмен ключа на токен и HTTP-запросы с успешно проверенным токеном этого ключа. +Счётчик увеличивается один раз на такой HTTP-запрос, даже если затем операция +отклонена из-за прав. Получение токена, неверные/истёкшие/отозванные токены, +запросы без проверки сервисного токена и федеративные токены в него не входят. +Статистика читается при открытии аккаунта; обращения до её включения не восстанавливаются. + +`GET /service-accounts/:id` и ответ выпуска ключа содержат `createdAt`, +`lastUsedAt` (последний обмен), `lastActivityAt` и `requestCount`. Счётчик передаётся +десятичной строкой, сохраняя точность PostgreSQL bigint. Новые ключи начинают с +`"0"`; существующим ключам миграция переносит известное время обмена в последнюю +активность, оставляя счётчик нулевым. Отзыв и отключение сохраняют статистику. +Откат миграции запрещён, если он удалит записанную статистику. + +Просмотр требует section `services/read` или `update`; изменение — `services/update`. Для менеджера все текущие и назначаемые политики сервиса должны входить в личный разрешённый набор администратора. Эта проверка действует и для выпуска/отзыва ключей и изменения федераций. Сервис с закрытой политикой доступен только для просмотра. Подробнее — [ограниченное назначение политик](access.md#ограниченное-назначение-политик). + +Сервисный токен использует права на данные, но не человеческие настройки/профиль. Ранее выданные ключи не отзываются автоматически при сокращении разрешённого набора менеджера; их отзывает администратор отдельно. + +## GitLab CI federation + +Реализован конкретный issuer `https://gitlab.com`. Проверяются RS256, issuer, audience, subject, project/job-project ID и path, защищённая ветка, время действия и jti. Merge-request pipeline не допускается. Повтор assertion блокируется хешем jti в БД. + +Это вход сервиса в Asmblyr, а не универсальный каталог всех OIDC issuer. Федерация доступа Core к Yandex Object Storage — отдельная настройка инфраструктурной учётной записи. + + + +## Asmblyr как OAuth/OIDC provider + +В разделе `oauth` создаются приложения для входа других сервисов через аккаунт Asmblyr. Базовый профиль фиксирован: ID, email, имя и при наличии изображение. Доступ приложения можно ограничить настроенными правилами, включая пользователей/домен email, либо разрешить всем подходящим пользователям. + +Согласие сохраняется для пары пользователь–приложение. Повторный вход с действующим grant не требует повторного согласия для уже разрешённого набора. Пользователь может отозвать приложение в профиле. OAuth-токен приложения не является Core API-токеном. + +Протокол обслуживает `oidc-provider` через `/oauth/*`. Полные protocol endpoints берутся из его discovery, а не из статической REST-матрицы. Административные `/oauth-apps` и `/oauth-interactions` описаны отдельно. + + + +### Доступ через политики + +В приложении на вкладке «Доступ» включите **«Управлять доступом через политики»**. +На вкладке «Интеграция» задайте Audience и каталог разрешений: техническое значение +для сервиса и понятное название для редактора. Сам каталог не выдаёт права. +Затем в редакторе политики → «Приложения» разрешите вход, отметьте разрешения +и назначьте политику пользователям. Разрешения всех политик пользователя объединяются. +Без подходящей политики вход запрещён, в том числе superuser Collaborative. + +При включённом режиме прежние правила по пользователям и доменам не участвуют +в проверке. При выключении снова действуют эти правила; назначенные права политик +сохраняются, но не используются. Перед выключением проверьте прежний режим доступа. +Существующие приложения сохраняют прежнее поведение: миграция не включает новый режим. + +В режиме политик приложение запрашивает только `openid profile email`. +Core добавляет персональные разрешения в подписанный access token: + +```json +{ + "aud": "lavinmq", + "scope": "openid profile email", + "resource_access": { + "lavinmq": { "roles": ["lavinmq.tag:monitoring"] } + } +} +``` + +Сервис должен поддерживать чтение этого claim; универсального преобразования +произвольных политик в формат каждого сервиса нет. Для LavinMQ установите +`resource_server_id = lavinmq`, `audience = lavinmq`, `verify_aud = true` и +`mgmt_scopes = openid profile email`. Удалите общие сервисные scopes из запроса входа. +Теги управления и разрешения на vhost/ресурсы задаются отдельно по правилам LavinMQ. + +На согласии видны названия персональных разрешений. Расширение прав требует нового +согласия. Если набор изменился, пока открыто окно подтверждения, Core возвращает +409: нужно начать вход заново и увидеть актуальные права. При обмене кода Core повторно проверяет доступ и пересекает текущие права +с подтверждёнными: начатый вход не может получить позднее добавленное право. +Изменения учитываются при новой выдаче токена. Уже выданный JWT внешний сервис может +принимать до истечения его срока (5 минут); активное соединение сервис может +закрывать по собственным правилам. Пользователи LavinMQ остаются OAuth-идентичностями, +локальные учётные записи в брокере не создаются. + +REST-поля приложения: `policyManaged` (по умолчанию `false`), `scopes` (каталог), +`scopeLabels` (словарь значение → название). Пропущенные `policyManaged` и +`scopeLabels` при обновлении сохраняют текущую настройку. Удаление значения из +каталога удаляет его из назначений политик; добавление обратно не восстанавливает +старые права. Смена Audience или режима доступа сбрасывает согласия. + +Локальный пример LavinMQ существует; его live-проверка зависит от поднятого контейнера и не выполняется обычным полным набором тестов. Не переносите разрешение HTTP для localhost в production: внешнему issuer нужны HTTPS, постоянные ключи подписи и корректные proxy/origin настройки. + + + +## Личные подключения Google + +[Google Workspace](google-workspace.md) использует отдельный OAuth client для ассистента, личные зашифрованные токены и подтверждение внешней записи. Это не вход через SSO и не выдача доступа приложениям Asmblyr OAuth provider. diff --git a/docs/ru/features/localization.md b/docs/ru/features/localization.md new file mode 100644 index 0000000..7d97a0b --- /dev/null +++ b/docs/ru/features/localization.md @@ -0,0 +1,76 @@ + + +# Переводы + +Язык интерфейса (`ru`, `en`) сохраняется в личных настройках вместе с независимыми +режимом и цветовым стилем. Основа построена на i18next/react-i18next: каталоги +поставляются с приложением, внешний сервис переводов для работы не нужен. +Экземпляры изолированы при серверном рендеринге. + + + +## Кто переводит подписи + +- Интерфейс и системные поля: JSON-каталоги `packages/contracts/locales/`. +- Плагины: собственные `locales/ru.json`, `locales/en.json` и namespace плагина. +- Пользовательские коллекции и поля: раздел «Переводы» в правом редакторе настроек. + Для коллекции сохраняется подпись, для поля — подпись, описание и плейсхолдер. + +Основная подпись остаётся запасным вариантом, если пользователь не заполнил +перевод. Языковые каталоги плагинов используют русский fallback. Технические имена +API, значения записей, enum-значения и шаблоны подписи записей не переводятся. +Переключение языка не перезаписывает исходные настройки схемы. + +## HTTP API + +Core: `GET /translations?locale=en`. На общем домене админки: +`GET /api/translations?locale=en`. SDK: `client.translations.get("en")`. +Язык по умолчанию — русский; другие языки и неизвестные query-параметры дают 400. + +```json +{ + "data": { + "version": 1, + "locale": "en", + "fallbackLocale": "ru", + "core": { "appearance.ocean": "Ocean" }, + "plugins": { "comments": { "panel.title": "Discussion" } }, + "schema": { + "articles": { + "label": "Articles", + "fields": { + "title": { "label": "Title", "description": "", "placeholder": "" } + } + } + } + } +} +``` + +Требуется активный пользователь или сервисный аккаунт. `schema` использует тот же +набор видимых коллекций и полей, что и `GET /collections`: права создания, чтения +и изменения объединяются для метаданных. Это не разрешает читать записи. +Системный ключ присутствует, timestamps — только при доступе к ним. В ответе нет +данных записей, defaults, настроек или секретов. `plugins` содержит статические +публичные каталоги всех активных расширений, в том числе без собственного UI. +Ответ имеет `Cache-Control: private, no-store`. + + + +## Покрытие + +Каталоги RU/EN покрывают встроенные формы и сообщения: редакторы коллекций, +полей и записей, фильтры, права, файлы, профиль и безопасность, интеграции, +ассистент и настройки расширений. Даты и форматированные числа учитывают язык, +точные decimal-значения сохраняют свою точность. Смена языка обновляет и подписи, +пришедшие с сервера. Пользовательские значения, названия политик и сообщения чата +сохраняются в исходном виде; переводы пользовательской схемы задаются явно. + +Плагины переводят подписи своих настроек ключами `settings..label`, +`settings..description` и `settings..options.`. При отсутствии +перевода используется исходная подпись. Новые встроенные тексты добавляются +в оба каталога; проверки контролируют наличие ключей и совпадение параметров. + +Каталоги — обычные JSON-файлы со стабильными ключами; их можно готовить внешними +инструментами и проверять в ревью. Автоматических вызовов AI, платного backend +переводов или автоматической публикации переводов в текущей реализации нет. diff --git a/docs/ru/features/monitoring.md b/docs/ru/features/monitoring.md new file mode 100644 index 0000000..f6bf341 --- /dev/null +++ b/docs/ru/features/monitoring.md @@ -0,0 +1,143 @@ + + +# Мониторинг и Sentry + +Мониторинг опционален и по умолчанию выключен. Раздел `/admin/settings/monitoring` +доступен только суперпользователю. Настройки открываются в правом редакторе и +сохраняются через существующий механизм [подключений и шифрования](connections.md). +Новая установка и существующая база работают без Sentry, DSN и дополнительных миграций. + + + +## Подключение + +В редакторе есть две вкладки: **«Сервер»** для Core/API и **«Интерфейс»** для +браузера. В каждой укажите свой DSN и выберите, что собирать. Можно использовать +один проект Sentry, указав одинаковый DSN в обеих вкладках, или отдельные проекты. +Для серверного проекта выбирайте платформу Node.js, для браузерного — Browser +JavaScript. Тег `component` различает `core` и `browser`. + +Включение мониторинга, окружение, релиз и доля трассировок общие для обеих вкладок. +Чтобы подключить только одну сторону, отключите сбор для другой. По умолчанию +выбраны ошибки обеих сторон; производительность и SQL-операции выключены. При +отсутствии нужного DSN редактор откроет вкладку, которую нужно заполнить. + +| Переключатель | Что собирается | +| ----------------------------- | ----------------------------------------------------------------------------------------------- | +| Ошибки сервера | Неожиданные ошибки обработчиков API с кодом 5xx | +| Ошибки интерфейса | JavaScript errors и unhandled Promise rejections в авторизованной админке | +| Производительность API | Длительность запроса до окончания ответа; локальные p50/p95/p99 и выбранные Sentry transactions | +| Производительность интерфейса | Navigation Timing текущей загрузки и новые same-origin fetch/XHR к основным API админки | +| Время SQL-операций | DB spans внутри выбранных API-трассировок; только тип операции и длительность | + +«Доля трассировок» задаётся от 0 до 100%. Она управляет отправкой производительности +в Sentry, а не выборкой ошибок. Локальные процентили используют все измеренные +запросы, в том числе при 0% трассировок. При выключенном сборе SDK не создаётся; +браузер не загружает его пакет и не устанавливает обработчики. + +Кнопка проверки проверяет формат DSN и доступность сохранённых ключей, без тестового +события. Она не подтверждает доставку в Sentry. Для проверки доставки включите +нужный сбор и проверьте появление событий в своём проекте Sentry. + + + +## p95/p99 и пределы измерений + +Локальная таблица показывает **текущий экземпляр Core**, последние 15 минут и до +2 000 наблюдений на маршрут. При превышении лимита остаются последние наблюдения, +и появляется отметка ограничения. Поддерживается до 128 маршрутов; превышение +также отмечается. Это ограниченная диагностика в памяти, без накопления в БД; +перезапуск или выключение сбора очищает её. При балансировке экземпляров ответы +могут поступать от разных процессов. Общие показатели смотрите в Sentry. + +p95 означает, что 95% измеренных запросов заняли не больше указанного времени; +p99 — 99%. Используется nearest-rank percentile. Админка показывает p95 от 20 +наблюдений, p99 от 100. Это порог отображения, а не гарантия статистической +устойчивости. В Sentry процентили считаются по отправленным трассировкам и выбранному +периоду; малая выборка может плохо отражать редкие задержки. + +Длительность Core не включает внешнюю сеть и браузер; для streaming она включает +передачу всего ответа. Browser Timing измеряется отдельно. Не объединяйте эти +две серии в один показатель. SQL spans не включают ожидание соединения в пуле; +очередь пула показана отдельным текущим счётчиком. RSS/heap отражают текущую память +процесса. p99 event loop берётся с момента включения сбора, с разрешением 20 мс. + + + +## Данные и жизненный цикл + +События проходят общий allowlist. Отправляются тип/место ошибки, очищенный стек +(basename файла, строка, функция), шаблон маршрута, длительности и технические +параметры окружения/релиза. Текст ошибки заменяется общей фразой. Значения URL, +query, заголовки, cookies, тела запросов/ответов, записи, сообщения ассистента, +SQL, bindings, пользователи и breadcrumbs исключены. Имя коллекции и ID в +шаблоне маршрута заменяются параметрами. Логи, Replay и CPU profiling не включены. + +Sentry получает сетевое соединение от Core или браузера и может обрабатывать +сетевые метаданные по своим настройкам. Оператор установки выбирает Sentry +и его правила обработки данных. DSN интерфейса содержит публичный ключ приёма +событий: при включённом браузерном мониторинге его видят авторизованные +пользователи. Отдельный серверный DSN браузеру не выдаётся. DSN не является +административным токеном Sentry; указывать такой токен нельзя. + +Сохранённые DSN шифруются как остальные ключи подключения. API настроек возвращает +только отдельные признаки их наличия. Изменения применяются сразу в экземпляре, обработавшем +сохранение; остальные экземпляры перечитывают конфигурацию каждые 5 секунд, браузер +— каждые 30 секунд и при локальном сохранении. Неактивные вкладки приостанавливают +проверку и возобновляют её при показе. При ошибке конфигурации мониторинг выключается, +а основные функции продолжают работать. Отключение без смены DSN доступно даже при +недоступном KMS; сохранённые ключи при этом остаются в БД. + +Сбор Core исключает health/ready, auth, OAuth, личные подключения, конфигурацию +мониторинга и неизвестные маршруты. Браузерные resource timings ограничены API +items, collections, files, preferences, presence, notifications и assistant. +Автоматического сбора Next.js SSR errors и React component boundaries нет. +Ошибки фона вне HTTP-запроса также не перехватываются этим подключением. + + + +## Конфигурация через env + +Любое непустое значение из списка `SENTRY_*` ниже блокирует группу в админке. +Параметры становятся read-only; изменить их можно в конфигурации сервера. +`SENTRY_ENABLED=false` явно выключает и блокирует группу. + +| Переменная | По умолчанию / значение | +| ---------------------------- | --------------------------------------------------------- | +| `SENTRY_ENABLED` | `true`, если задан DSN; иначе `false` | +| `SENTRY_DSN` | Серверный DSN; HTTPS, публичный ключ, числовой ID проекта | +| `SENTRY_BROWSER_DSN` | DSN интерфейса; при отсутствии используется `SENTRY_DSN` | +| `SENTRY_ENVIRONMENT` | `production`, до 64 символов, без пробелов/слешей/`None` | +| `SENTRY_RELEASE` | Пустая строка; до 120 символов | +| `SENTRY_ERRORS_CORE` | `true` при наличии серверного DSN | +| `SENTRY_ERRORS_BROWSER` | `true` при наличии DSN интерфейса | +| `SENTRY_PERFORMANCE_CORE` | `false` | +| `SENTRY_PERFORMANCE_BROWSER` | `false` | +| `SENTRY_DATABASE_SPANS` | `false`; работает вместе с performance Core | +| `SENTRY_TRACES_SAMPLE_RATE` | `0.1`; число от 0 до 1 | + +Boolean-параметры принимают только `true`/`false`. HTTP DSN допускается только для +loopback при локальной проверке. Не используйте DSN с паролем, query или fragment. +Неверная опциональная конфигурация отключает мониторинг и показывает его недоступность. + +Для совместимости с общей конфигурацией `SENTRY_DSN` используется также для +браузера, если `SENTRY_BROWSER_DSN` не задана. Для сбора только на сервере явно +отключите `SENTRY_ERRORS_BROWSER` и `SENTRY_PERFORMANCE_BROWSER`. Если задана только +`SENTRY_BROWSER_DSN`, сбор серверных ошибок по умолчанию выключен. + +API принимает `secrets.serverDsn` и `secrets.browserDsn`. Ранее сохранённый общий +DSN остаётся резервным значением для каждой стороны до редактирования ключей. +При замене или удалении одного ключа значение другой стороны сохраняется отдельно, +а общий слот удаляется в той же транзакции. `null` удаляет ключ выбранной стороны; +если её сбор включён, сохранение отклоняется. При выключенном мониторинге можно +удалить оба ключа. Совместимый ключ `secrets.dsn` допускается только отдельно от +других ключей: он задаёт общий DSN и очищает отдельные, а `null` удаляет все DSN. +Без изменения ключей шифротекст сохраняется, включая возможность выключить +мониторинг при недоступном KMS. Миграция базы для этого не нужна. + +`GET /monitoring/browser` требует активную человеческую сессию и возвращает только +браузерную конфигурацию. `GET /settings/monitoring` требует human superuser и +возвращает локальные показатели. Оба ответа имеют `private, no-store`. +Сохранение — `PUT /settings/integrations/monitoring`, проверка — +`POST /settings/integrations/monitoring/test`, с общей optimistic revision и правилами +write-only secrets. Контракты приведены в [OpenAPI](../reference/http.md). diff --git a/docs/ru/features/notifications.md b/docs/ru/features/notifications.md new file mode 100644 index 0000000..278d55a --- /dev/null +++ b/docs/ru/features/notifications.md @@ -0,0 +1,72 @@ + + +# Обсуждения и уведомления + +Уведомления открываются колокольчиком в шапке админки. Ассистент остаётся отдельным +виджетом; отключение AI не отключает обсуждения или уведомления. + + + +## Подписка на запись + +Во вкладке «Обсуждение» плагина комментариев нажмите **Следить**. Первый отправленный +комментарий также включает подписку. Кнопка **Вы следите** отключает её; последующие +собственные комментарии не включают явно отключённую подписку заново. + +Новый комментарий другого участника создаёт уведомление подписчикам с текущим +правом чтения записи. Собственные комментарии уведомлений не создают. Редактирование +обновляет выдержку существующего уведомления; удаление комментария удаляет его +уведомления. Email, push и уведомления о простом изменении полей не реализованы. + + + +## Входящие + +Колокольчик показывает автора, короткий текст, коллекцию, ключ записи и время. +Нажатие открывает обсуждение и выделяет адресованный комментарий, включая старые +комментарии вне первой страницы. Уведомление отмечается прочитанным при переходе. +Если комментарий уже недоступен, обсуждение остаётся открытым с пояснением. + +Открытие списка само по себе не отмечает уведомления прочитанными. **Прочитать все** +отмечает собственные уведомления до времени снимка списка; новые события после него +остаются непрочитанными. Статус сохраняется в PostgreSQL и общий для устройств. +Обновление — каждые 30 секунд в видимой вкладке, при возвращении в окно и открытии +колокольчика. Переход использует защиту несохранённых изменений редактора. + +Core хранит последние 200 уведомлений каждого пользователя, по умолчанию возвращает +50 с возможностью показать остальные. Счётчик относится к доступным уведомлениям +в этом окне хранения. Содержимое и счётчик повторно проверяют права на запись, +включая правила строк. Выключенный плагин скрывает свои события, сохраняя данные. +Удаление записи очищает подписки и уведомления; удаление коллекции очищает их по +стабильному ID коллекции. Повторное создание таблицы с тем же именем их не наследует. + + + +## API и SDK + +- `GET /notifications?limit=50`: `{ data, unread, total, readBefore }`. +- `POST /notifications/:id/read`: 204; чужой или недоступный ID — 404. +- `POST /notifications/read-all`: `{ before: readBefore }`, ответ 204. + +Все три endpoint требуют активной человеческой сессии. Сервисный аккаунт может +написать комментарий с обычными правами на запись, но не читать личные входящие +и не управлять человеческими подписками. + +SDK: `client.notifications.list()`, `.read(id)`, `.readAll(result.readBefore)`. +В UI те же пути доступны напрямую в Core под `/api` с браузерной HttpOnly-сессией. + + + +## Контракт плагина + +Плагин объявляет и получает одобрение возможности `notifications`. Только обычный +HTTP-контекст `useAsmblyr(event)` получает `notifications.following`, `follow`, +`publish`, `update` и `remove`. Core связывает источник с namespace пакета, +автора — с текущей идентичностью, получателей — с подписками и правами записи. +Model handlers не получают этот привилегированный контекст. Публикация выполняется +внутри `withRecord`, в одной транзакции с комментарием; внешних сообщений нет. + +Plugin UI получает необязательные `active` и `targetId` в `RecordPanelProps`. +Ссылка записи `?panel=plugin:namespace:panel-id&target=entity-id` выбирает только +существующую панель. Плагин сам проверяет доступ и показывает адресованную сущность. +Хост не содержит специальных условий для комментариев. diff --git a/docs/ru/features/plugins.md b/docs/ru/features/plugins.md new file mode 100644 index 0000000..b5eaae3 --- /dev/null +++ b/docs/ru/features/plugins.md @@ -0,0 +1,82 @@ + + +# Расширения + +Если вы пишете первое расширение, начните с [пошагового руководства](../development/first-extension.md). + +Плагин — npm/workspace-пакет. Рабочие пакеты проекта находятся в `packages`, демонстрационные — в `examples/plugins`. Обнаружение идёт по списку пакетов `asmblyr.plugins` в корневом `package.json`, а не по имени каталога. Подключённый пакет должен быть установлен. Установка из UI пока не реализована. + +В разделе «Настройки → Расширения» показывается список плагинов. Нажатие на строку или название открывает штатный правый редактор с настройками выбранного плагина и разрешёнными возможностями. Закрытие несохранённого черновика требует подтверждения; во время сохранения редактор блокирует закрытие. Плагины без параметров также открываются для просмотра возможностей. + + + +## Контракт + +- `plugin.ts`: `definePlugin({})`, без отдельного реестра роутов. +- `server/api/**/*.get.ts` / `.post.ts`: H3 v2 handlers; имя файла определяет путь/метод. +- `server/hooks/*.ts`: lifecycle hooks. +- `server/settings.ts`: декларация настроек. +- Browser entry: отдельный `./ui`, страницы и field interfaces. +- `defineModelContext` + аннотация: явная публикация handler во внутреннем MCP/ассистенте. + +`@asmblyr-collaborative/kit` — серверный/браузерный контракт расширений. `@asmblyr-collaborative/sdk` — HTTP-клиент, не доступ к Knex. `useItems(event)` всегда использует права caller; `storage.own` — привилегированное хранилище собственных таблиц, перед ним плагин обязан проверять доменный доступ. + +Сгенерированные Kit model handlers экспортируют входные и выходные JSON-контракты +через `GET /schema` с учётом `AccessGate`. CLI создаёт типизированные методы +`client.plugins.[methodId](input)`, которые вызывают исходные HTTP routes. +Права на данные и личное OAuth-подключение проверяются при выполнении. Legacy +actions без выходной схемы в этот клиент не попадают. Подробнее — +[генерация SDK](../reference/cli-guide.md). + + + +## Декларации и установка + +Capabilities заявляются в `asmblyr.manifest.capabilities` и отдельно одобряются для пакета в `asmblyr.pluginPermissions`. По умолчанию нет одобренных возможностей. Это проверка поддерживаемого API Kit; произвольный Node-код она не изолирует. + +Первичная установка таблиц транзакционная, имена `plugin__`. Нельзя занять чужую существующую таблицу. Изменения установленной схемы требуют версионных неизменяемых миграций; отключение пакета не удаляет данные. + +Пакеты этой платформы используют scope `@asmblyr-collaborative`: Core/UI, +Contracts/SDK/Kit/CLI, встроенные Comments/Google Workspace и учебные плагины. +При обновлении старой установки замените имена встроенных пакетов в зависимостях, +`asmblyr.plugins` и ключах `asmblyr.pluginPermissions`, затем выполните +`pnpm install`, `pnpm db:migrate` и сборку перед запуском Core/UI. +Миграция `20261005220000_collaborative_package_names` меняет только npm-имена +известных владельцев в реестре. Она проверяет соответствие namespace и отказывается +от переноса при конфликте владельцев; сторонние пакеты не переименовываются. +Namespace `comments`/`google`, таблицы, настройки, история миграций, URL и ключ +конфигурации `asmblyr` сохраняются. Старый workspace-алиас `@asmblyr/kit` +в Comments нужен неизменяемой ранее выпущенной миграции и сохранён намеренно. + + + +## Названия и видимость + +В списке расширений название Google Workspace берётся из каталогов переводов +плагина. `GET /settings/plugins` также возвращает `title` для начального отображения: +RU `settings.title`, заголовок настроек или техническое имя. Каталог выбранного +языка уточняет его после загрузки переводов. Это работает и для плагинов без UI +или собственных настроек. Технические namespace и npm-имя остаются отдельными +идентификаторами. + +Таблицы плагинов не показываются в редакторе структуры `/admin/collections`, +включая счётчик коллекций. Плагин продолжает использовать их через свои страницы +и API с обычными проверками доступа; скрытие из редактора не меняет права. + + + +## Hooks и уведомления + +Hooks исполняются до commit в транзакции: ошибка откатывает изменение. Внешние запросы/отправку сообщений нельзя считать гарантированно доставленными; для этого нужен outbox, который пока не реализован. Все контекстные операции необходимо await. + +Плагин комментариев использует возможность `notifications` для подписок на запись +и транзакционной публикации событий в отдельный колокольчик. Получателей выбирает +Core; новые права на данные не выдаются. Подробнее — [обсуждения и уведомления](notifications.md). + +[Kit](../reference/kit-guide.md), [Hooks](../reference/kit-hooks.md), [Field interfaces](../reference/kit-fields.md). Исходники и примеры — `packages/kit`, `packages/plugin-comments` и `examples/plugins`. Source scanner и production build index должны давать одинаковые маршруты. + + + +## Граница доверия + +Доверяйте устанавливаемому пакету как коду Core: server plugin может импортировать Node-модули и обращаться к окружению, browser plugin работает в контексте админки. Для недоверенных пакетов нужна отдельная процессная/браузерная изоляция. diff --git a/docs/ru/features/realtime.md b/docs/ru/features/realtime.md new file mode 100644 index 0000000..f2e987d --- /dev/null +++ b/docs/ru/features/realtime.md @@ -0,0 +1,80 @@ +# Real-time collaboration + +Collaborative Live работает для коллекций и записей, доступных текущему +пользователю. Таблица подписывается на коллекцию и обновляется после сохранения +записи. Карточка показывает участников, состояние соединения, изменения и +редактируемые поля. В клиентском интерфейсе данные перечитываются через обычный +HTTP API: событие содержит только идентификатор, имена разрешённых изменённых +полей и номер события истории. Несохранённый ввод остаётся в редакторе. + +## Presence + +SSE-подписка открывает 30-секундную lease присутствия. Core обновляет её пока +соединение активно, удаляет при закрытии и проверяет право чтения при +подключении и при каждом событии. Участники одного человека объединяются; +отображаются имя, аватар и число окон. После аварийного закрытия остаток lease +исчезает не позднее 30 секунд. Ранее доступный HTTP presence API сохранён. +Каждое продление активной SSE-подписки отправляет актуальный состав участников; +так исчезают и окна, чья lease истекла без события выхода. +Компоненты админки разделяют один поток на scope. Новый подписчик сразу получает +текущее состояние соединения и последний состав участников. При обрыве этот +снимок сбрасывается до получения актуальных данных после переподключения. + + + +## Live updates и разрешения + +Core публикует `record.created`, `record.updated`, `record.deleted` из +транзакции записи. PostgreSQL доставляет уведомление только после commit и между +экземплярами Core. Подписка на запись проверяет read коллекции и строки по тем +же правилам, что HTTP `/items`. Имена полей фильтруются по фактически видимым +полям строки; sensitive поля не объявляются. Подписка на коллекцию получает +только `collection.changed`, если изменённая строка доступна читателю. При +условных правах удаление не объявляется, поскольку удалённую строку уже нельзя +проверить. Изменение прав обнаруживается при следующем событии или продлении +lease (до 20 секунд), после чего поток закрывается. + + + +## Field locks и конфликты + +Фокус в редактируемом поле получает lease на 30 секунд. Core проверяет read и +update поля и строки. Вторая сессия получает `409 FIELD_LOCKED`; блокировка +одного поля не мешает редактировать другие. При выходе из поля lease удаляется, +а при обрыве сети истекает автоматически. Клиент повторяет запрос каждые 10 +секунд, пока поле активно. + +Блокировка помогает людям видеть редактирование, но не гарантирует сохранение. +Атомарный draft commit по-прежнему сравнивает `expectedValues` изменяемых полей +и возвращает `409 ITEM_CHANGED`, когда чужая правка затрагивает то же поле. +Редактор оставляет черновик и показывает существующий выбор значений. Обычный +`PATCH` без precondition сохраняет прежний контракт. +Чистая форма принимает новое состояние записи только если к моменту ответа HTTP +у пользователя всё ещё нет несохранённых правок. Вместе с полями обновляется +база `expectedValues`; если ввод начался во время запроса, ответ не заменяет форму. + + + +## Reconnect и SDK + +SDK `client.realtime.connect()` возвращает типизированное соединение. +`subscribe(scope, callback)` возвращает функцию отписки; `on(type, callback)` +и `onState(callback)` возвращают очистку слушателя. `locks.acquire`, `refresh` +и `release` работают с `{ collection, recordId, field, clientId }`. +При обрыве SDK переподключается с backoff и jitter, повторно регистрирует +presence и посылает `collection.changed`, чтобы клиент перечитал данные. +Это событие получают и callback подписки, и обработчики `on("collection.changed")`. +Повторные ID в одном потоке пропускаются. Отписка и `close()` освобождают +соединение. Браузерная админка открывает `/api/realtime/stream` напрямую в Core +с HttpOnly cookie; прямому клиенту Core нужен действующий пользовательский Bearer token. + + + +## Границы + +Поток передаёт события операций через Core, включая Kit, но прямой SQL в +пользовательские таблицы не создаёт событий. Комментарии и изменения прав пока +не имеют собственных live-событий. SSE не хранит очередь событий: после +переподключения клиент перечитывает текущие данные. Presence и locks не +поддерживают сервисные ключи. Workspace в envelope равен `null`, поскольку +рабочие пространства группируют коллекции и не изолируют данные. diff --git a/docs/ru/features/settings.md b/docs/ru/features/settings.md new file mode 100644 index 0000000..78d3c09 --- /dev/null +++ b/docs/ru/features/settings.md @@ -0,0 +1,66 @@ + + +# Настройки админки + +Общая точка входа — `/admin/settings`. Она видна, если есть право хотя бы на одну страницу настроек. Навигация и страницы содержат только разрешённые разделы; Core проверяет право повторно на каждом запросе. + +На небольших экранах разделы выбираются в компактном меню над содержимым. +На широком экране они остаются в боковой навигации; обе версии показывают +только доступные пользователю разделы. «Термины» — словарь для ассистента. + +Библиотека файлов находится в главном меню на `/files` и не дублируется в меню +или обзоре системных настроек. Права `files/read` и `files/update` по-прежнему +назначаются в политиках и видны при просмотре доступа пользователя. Если +доступна только библиотека файлов, вход в `/admin/settings` перенаправляет туда. +Настройка S3/Yandex Object Storage находится в разделе «Подключения». + +Суперпользователю также доступен раздел [подключений и защиты секретов](connections.md) +на `/admin/settings/integrations`: S3/Yandex Object Storage, AI-провайдер и локальное +шифрование либо Yandex KMS. Этот раздел не делегируется правами остальных настроек. + +Опциональный [мониторинг Sentry](monitoring.md) находится отдельно на +`/admin/settings/monitoring`: ошибки, производительность, SQL spans и локальные +p95/p99. Он также доступен только суперпользователю и по умолчанию выключен. + +| Раздел | Ключ в permission | Возможности | +| ---------------- | ----------------- | ------------------------------------------------------------ | +| Пользователи | users | Список, приглашения, просмотр эффективного доступа | +| Политики | policies | Permissions, политики и назначения | +| Расширения | plugins | Список загруженных пакетов, декларации и доступные настройки | +| Ассистент | assistant | Настройки модели/инструкций и телеметрия | +| Термины | terms | Общий справочник смыслов для ассистента | +| Сервисы | services | Аккаунты, ключи и федерации | +| OAuth-приложения | oauth | Приложения, секреты, правила допуска | + +Назначение: откройте политику → блок настроек → выберите для разделов «Нет доступа», «Просмотр» или «Изменение» → сохраните → назначьте политику пользователю. Права на коллекции настраиваются отдельно. + +Пользователь только с правами настроек попадает в настройки без требования доступа к данным. Без данных и разделов видит отдельный экран отсутствия доступа. Личный профиль остаётся на `/settings`. + +Старые URL `/access`, `/services`, `/oauth-apps` перенаправляются в новые разделы. Системные права доступны через `GET /settings/access`; каталог данных для редактора политики берётся через ограниченную проекцию `/settings/options/collections`. + +Управление структурой и рабочими пространствами пока не делегируется этими флажками. Установка пакетов и одобрение plugin capabilities остаются в конфигурации проекта. Для каждого раздела доступны отдельные права `read` и `update`. `update` включает просмотр; разрешения разных политик объединяются. Изменение политик и управление сервисами дополнительно ограничены правилами делегирования ниже. `read` разрешает списки, подробности, телеметрию и ограниченные справочники для селекторов, но не сохранение, приглашения, назначения, выпуск/отзыв ключей, федераций или замену OAuth-секрета. GET не возвращает ранее выданные секреты. + +`GET /settings/access` возвращает `sections` (разделы для просмотра) и `editableSections` (разделы для изменения). Навигация использует первый список; страницы передают режим просмотра в свои формы. В режиме просмотра доступно раскрытие правой панели, условий и вкладок; управляющие действия скрыты, значения защищены от изменения. Сервер проверяет отдельное право изменения независимо от UI. `GET /users/:id/access` показывает оба списка для выбранного пользователя. + +Ответ также содержит `canManagePolicies` (только superuser) и `delegatablePolicyIds` (явный набор готовых политик для назначения). `policies/update` у менеджера разрешает только назначения из этого набора; состав прав остаётся для просмотра. Для сервисов набор ограничивает и назначения, и выпуск ключей/федерации. Superuser не ограничен массивом, который для него пуст. Настройка набора находится в правой панели доступа пользователя; подробнее — [делегирование](access.md#ограниченное-назначение-политик). + +Миграция `20261003040000_policy_delegations.cjs` добавляет личный разрешённый набор, пустой для существующих пользователей. Старые назначения сохраняются; не-superuser больше не меняет состав политик и должен получить явный набор от администратора для новых назначений. Down отказывается удалять непустые настройки делегирования. + +Миграция `20261003030000_settings_read_permissions.cjs` добавляет действие `read` и уникальность `(section, action)`, не меняя существующие grants. Down отказывается удалять поддержку просмотра, пока есть permissions `read`. + +Миграция `20261003010000_settings_permissions.cjs` сохраняет существующие права коллекций и добавляет альтернативную цель section. Down откажется работать, пока существуют section permissions: молча терять назначенные права нельзя. + + + +## Оформление и язык + +Во вкладке «Внешний вид» личных настроек `/settings` режим (`light`, `dark`, `system`) выбирается +отдельно от стиля (`neutral`, `ocean`, `coral`). Каждый цветовой стиль поддерживает +светлый и тёмный режим. Настройки сохраняются в профиле через +`GET/PATCH /users/me/preferences`; частичный PATCH не сбрасывает остальные значения. +Пустой профиль возвращает `theme: null`, `style: "neutral"`, `locale: "ru"`. + +Интерфейс поддерживает основу локализации RU/EN; подробнее — +[переводы и их API](localization.md). Старые адреса `/system-settings/...` +перенаправляются на `/admin/settings/...`. Личные настройки, коллекции, файлы +и страницы расширений сохраняют свои адреса. diff --git a/docs/ru/features/workspaces.md b/docs/ru/features/workspaces.md new file mode 100644 index 0000000..f146db0 --- /dev/null +++ b/docs/ru/features/workspaces.md @@ -0,0 +1,17 @@ + + +# Рабочие пространства и представления + +Workspace объединяет выбранные коллекции для удобства работы. Переключатель находится в шапке sidebar; текущий выбор сохраняется за пользователем. Коллекции можно организовать папками и виртуальной вложенностью в другие коллекции. + +Workspace фильтрует навигацию, но не изолирует данные между арендаторами. Права определяются политиками. Пользователь видит только доступные коллекции; скрытие коллекции в меню не является запретом на API или связь. + + + +## Сохранённые виды + +Виды таблицы имеют область personal, collection или workspace. Личные редактирует владелец, общие — superuser. Определение проверяется против доступной схемы/полей. При потере прав или изменении схемы несовместимый вид не должен возвращать скрытые данные. + +Default выбирается по приоритету personal → workspace → collection. Количество — до 30 видов на scope/коллекцию. Изменения колоночных настроек и профиля используют пользовательские preferences; не все произвольные UI-состояния имеют общий механизм хранения. + +Фильтр вида лишь уточняет выборку: он не задаёт row-level security. Права на изменение самих workspaces остаются у superuser. diff --git a/docs/ru/guide/deployment.md b/docs/ru/guide/deployment.md new file mode 100644 index 0000000..9dec64e --- /dev/null +++ b/docs/ru/guide/deployment.md @@ -0,0 +1,71 @@ + + +# Развёртывание и обновление + +Для локальной разработки начните с [первого запуска](getting-started.md). +Основная поставка использует отдельные образы Core и UI из одного коммита; +PostgreSQL 17+ и S3-совместимое хранилище подключаются отдельно. Готовые сборки +публикуются в `ghcr.io/asmblyr/collaborative-core` и +`ghcr.io/asmblyr/collaborative-ui`; закрепляйте оба образа одной версии по digest. +Одна собственная установка предназначена для одной команды; workspace не создаёт +отдельный tenant. + +| Сценарий | Где начать | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Основная поставка с внешними PostgreSQL/S3 | [`deploy/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.yaml) и [`deploy/env.example`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/env.example) | +| Локальный стенд с PostgreSQL и MinIO | [`deploy/local/compose.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/local/compose.yaml) | +| Совместимый монолитный образ | [`deploy/compose.monolith.yaml`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/compose.monolith.yaml) | + +Точные команды сборки и запуска, настройки портов, GHCR и контейнерные проверки — +в [руководстве `deploy`](https://github.com/Asmblyr/Collaborative/blob/main/deploy/README.ru.md). +Основной Compose выполняет миграции отдельным коротким запуском образа Core +перед стартом приложения. Он публикует общий адрес на loopback-порту 3000 +по умолчанию. Не переносите локальные секреты и HTTP origin в публичную +установку. + + + +## Конфигурация + +Для основной поставки скопируйте `deploy/env.example` в приватный файл вне +репозитория. Core читает `DATABASE_URL` и остальные параметры из окружения; +полный шаблон с комментариями — +[`apps/core/.env.example`](https://github.com/Asmblyr/Collaborative/blob/main/apps/core/.env.example). +Основные группы настроек: + +- `ASMBLYR_SETUP_TOKEN` нужен до создания первого superuser. +- `AUTH_UI_URL` задаёт точный внешний origin UI и обязателен в production. +- `SECRETS_LOCAL_KEY` защищает сохраняемые секреты интеграций. +- S3 можно настроить в системных настройках или через `FILES_STORAGE`, `FILES_BUCKET` + и параметры выбранного провайдера. Bucket создаёт оператор. +- `OPENAI_API_KEY` и `OPENAI_API_MODEL` включают опционального ассистента, + если `ASSISTANT_ENABLED` не установлен в `false`. +- `OAUTH_ISSUER_URL` и `OAUTH_KEYS_FILE` относятся к Asmblyr как OAuth provider; + ключи создаются отдельно и сохраняются между обновлениями. + +Значения и ограничения описаны в env-шаблонах и на страницах +[интеграций](../features/integrations.md), [файлов](../features/files.md) и +[ассистента](../features/assistant.md). Секреты и ключи держите вне Git и образов. + + + +## Публичная установка + +Разместите UI и Core API за HTTPS reverse proxy. PostgreSQL и bucket должны +оставаться приватными; основной Compose публикует порты только на loopback. +Оператор настраивает TLS, IAM, резервные копии и мониторинг. `GET /health` +проверяет процесс, `GET /ready` — БД и обязательные миграции. Скрипты +резервирования локального стенда не обслуживают внешний облачный bucket. + + + +## Обновление и восстановление + +Перед обновлением сохраните согласованную копию PostgreSQL и объектов, проверьте +восстановление на отдельной установке и выполните миграции **до запуска нового Core**. +Старые миграции не редактируются; `db:rollback` может удалить данные и не заменяет +восстановление из копии. Откат образа возможен только при совместимой схеме. +Для дополнительного локального стенда доступны +[команды backup/restore и проверка статуса](../development/operations.md#копии-локальнои-docker-установки). + +Рекомендации по защите установки — в [границах безопасности](../security/overview.md). diff --git a/docs/ru/guide/features.md b/docs/ru/guide/features.md new file mode 100644 index 0000000..0579fb9 --- /dev/null +++ b/docs/ru/guide/features.md @@ -0,0 +1,24 @@ + + +# Карта возможностей + +Карта текущей реализации. Колонка «Ограничения» описывает границы функций; сроки дальнейших изменений не закреплены. + +| Область | Реализовано | Ограничения | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Коллекции | Создание, single/multiple, тип ключа, поля, подписи, формы, индексы, поиск, системные даты и состояние; импорт materialized views только для чтения | Подключение произвольных существующих таблиц и обычных views; перенос между БД как продукт | +| Записи | Таблица, страницы, сортировка, колонки, боковой редактор, адрес записи, пакетные действия, атомарный draft, live invalidation | Внешний SQL не создаёт live-события | +| Связи | M:1, обратные 1:M, M:N через junction, выбор/создание/отвязка, поля связи | Нет обещания произвольной глубины вложенных запросов | +| Фильтры | Группы AND/OR, типовые условия, поля связей, сохранённые виды | Условия permissions: прямые поля, И/ИЛИ и параметры участника запроса | +| Права | Действия/поля/условия коллекции, переиспользуемые permissions, политики M:N, просмотр настроек и ограниченное назначение готовых политик | DDL остаётся у superuser; per-object file ACL | +| Пользователи | Setup, вход по ссылке, пароль/passkey, профиль, сессии, SSO, восстановление администратором | Отдельный MFA flow, самостоятельное email-восстановление, LDAP/SAML | +| Интеграции | Сервисные аккаунты/ключи, GitLab CI federation, собственный OAuth/OIDC provider | Универсальная федерация любых issuer; публичный MCP | +| Файлы | S3/YC, метаданные, ссылки, files/read и update, проверка чтения | Per-object ACL, antivirus/преобразования, reconciliation DB/S3 | +| Пространства | Переключатель, набор коллекций, пользовательский выбор | Не являются tenant isolation | +| Ассистент | Контекст, stream, инструменты, model-handlers, telemetry, общие лимиты и опциональный retention | Денежный бюджет провайдера; доказанная устойчивость к prompt injection | +| Плагины | pnpm/local packages, H3 file routes, типизированный Kit, миграции, hooks, UI/fields/settings, MCP annotations | Песочница, marketplace/install из UI, гарантированная доставка внешних событий | +| SDK | HTTP items list/get/create/update/delete/commit, users.me, presence touch/leave, realtime stream/locks, генерация схем и fluent-запросы через CLI | Не все системные endpoints имеют отдельную клиентскую обёртку | +| Коллаборация | Проверка исходных значений, выбор при конфликте, SSE presence, live updates, TTL-блокировки полей | Нет синхронизации комментариев и прав через live protocol | +| Протоколы | HTTP и SSE; OIDC протокол отдельно | Общий GraphQL API | + +Подробности — на функциональных страницах, требования допуска — в [матрице](../security/access-matrix.md). diff --git a/docs/ru/guide/getting-started.md b/docs/ru/guide/getting-started.md new file mode 100644 index 0000000..8fba8be --- /dev/null +++ b/docs/ru/guide/getting-started.md @@ -0,0 +1,45 @@ + + +# Локальный запуск + +Нужны Node.js 22+, pnpm 11.13.1, Docker Compose. Команды выполняются из корня репозитория. + +На Unix shell используйте `cp -n apps/core/.env.example apps/core/.env` вместо +`Copy-Item` ниже. Существующий файл окружения сохраняйте. + +```powershell +pnpm install --frozen-lockfile +pnpm db:up +if (!(Test-Path apps/core/.env)) { Copy-Item apps/core/.env.example apps/core/.env } +pnpm db:migrate +pnpm dev +``` + +Копируйте пример только при отсутствии своего `.env`. До первой настройки задайте в нём случайный `ASMBLYR_SETUP_TOKEN` длиной не менее 32 символов. Не сохраняйте секрет в Git. + +| Компонент | Адрес | +| ---------------- | ----------------------------- | +| Админка | `http://localhost:3000` | +| Core | `http://127.0.0.1:3001` | +| PostgreSQL | 127.0.0.1:5433 | +| Первая настройка | `http://localhost:3000/setup` | + +В `/setup` введите setup secret, email и пароль от 12 символов. Создаётся первый superuser. После появления пользователя endpoint закрывается; секрет можно удалить из конфигурации. Публичной регистрации по паролю нет. + +`pnpm dev` предварительно собирает SDK, Kit и встроенные плагины. `CORE_URL` в серверной конфигурации UI по умолчанию указывает на локальный Core. `GET /health` проверяет процесс, `/ready` — БД и обязательные миграции. + +`pnpm db:down` останавливает PostgreSQL без удаления volume. Не добавляйте `-v`, если данные нужно сохранить. Перед обновлением существующего проекта выполняйте `pnpm db:migrate`; откат DDL может уничтожить данные. + +Установка Docker Compose, внешние PostgreSQL/S3 и порядок production-обновлений — +в [руководстве по развёртыванию](deployment.md). + + + +## Документация + +```powershell +pnpm docs:dev +pnpm docs:build +``` + +Первый запуск генерирует справочники. Статический сайт получается в `docs/.vitepress/dist`; его можно разместить на любом статическом хостинге. OpenAPI доступен отдельным JSON. Страницы можно печатать средствами браузера; отдельного конвейера PDF-книги пока нет. diff --git a/docs/ru/index.md b/docs/ru/index.md new file mode 100644 index 0000000..82fdbb0 --- /dev/null +++ b/docs/ru/index.md @@ -0,0 +1,64 @@ +# Collaborative + +Открытая платформа для работы с данными в PostgreSQL. Админка объединяет структуру +коллекций, редакторы записей, права команды, файлы и интеграции. +Проект находится в ранней бете; одна команда использует отдельную установку. + +[GitHub](https://github.com/Asmblyr/Collaborative) · [Релизы](https://github.com/Asmblyr/Collaborative/releases) + + + +## Начать + +- [Возможности и ограничения](guide/features.md) +- [Первый запуск](guide/getting-started.md) +- [Docker, production и обновления](guide/deployment.md) +- [HTTP API](reference/http.md) +- [TypeScript SDK](reference/sdk-guide.md) и [CLI](reference/cli-guide.md) + + + +## Работа с продуктом + +Руководства по [данным](features/data.md), [правам](features/access.md), +[пользователям](features/identity.md), [файлам](features/files.md), +[настройкам](features/settings.md), [пространствам](features/workspaces.md), +[мониторингу](features/monitoring.md), +[интеграциям](features/integrations.md), [ассистенту](features/assistant.md), +[обсуждениям и уведомлениям](features/notifications.md), +[локализации](features/localization.md) и [расширениям](features/plugins.md) +описывают текущие контракты и их границы. +[Личные подключения Google Drive и Sheets](features/google-workspace.md) описаны отдельно. +[Совместная работа в реальном времени](features/realtime.md) описывает +присутствие, изменения записей, блокировки полей и ограничения транспорта. + +| Задача | Где читать | +| --------------------------------------------------- | --------------------------------------------------------- | +| Коллекции, поля, связи, записи, фильтры и поиск | [Данные](features/data.md) | +| Виды таблицы и рабочие пространства | [Пространства](features/workspaces.md) | +| Пользователи, вход и сессии | [Авторизация](features/identity.md) | +| Политики, разрешения и условия строк | [Права](features/access.md) | +| Файлы и S3 | [Файлы](features/files.md) | +| Аутентификация API, страницы, ошибки и примеры кода | [HTTP](reference/http.md) и [SDK](reference/sdk-guide.md) | + + + +## Разработка и эксплуатация + +- [Локальная разработка и проверки](development/local-development.md) +- [Архитектура](development/architecture.md) +- [Архитектура Collaborative Live](architecture/realtime.md) +- [Первое расширение](development/first-extension.md) +- [Архитектура ассистента и model context](development/assistant-architecture.md) +- [Опубликованные пакеты](reference/packages.md) +- [Сопровождение документации](development/documentation.md) +- [Эксплуатация и восстановление](development/operations.md) +- [Границы безопасности](security/overview.md) +- [Визуальная идентичность и публичные assets](assets/brand/README.md) +- [Участие в проекте](https://github.com/Asmblyr/Collaborative/blob/main/CONTRIBUTING.ru.md) + +Руководства по Kit и SDK поддерживаются в исходных пакетах и копируются в +`reference` при генерации сайта. Технические детали размещаются рядом с +контрактом; эта главная страница служит маршрутом к ним. Документация доступна +на английском (по умолчанию) и русском. Переключатель языка открывает ту же +страницу; контракты API и система переводов админки остаются общими. diff --git a/docs/ru/reference/cli-guide.md b/docs/ru/reference/cli-guide.md new file mode 100644 index 0000000..63dcb06 --- /dev/null +++ b/docs/ru/reference/cli-guide.md @@ -0,0 +1,79 @@ + + +# @asmblyr-collaborative/cli + +Подключает TypeScript-проект к Collaborative и генерирует типы коллекций/плагинов +по доступу вошедшего пользователя. Нужен Node.js 22+. Установите CLI рядом с SDK: + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +npx asm connect --url https://asmblyr.example.test +``` + +В репозитории соберите пакеты и используйте workspace-команду: + +```sh +pnpm build:packages +pnpm exec asm connect --url https://asmblyr.example.test +pnpm exec asm schema pull +pnpm exec asm generate +pnpm exec asm schema check +pnpm exec asm schema check --offline +``` + +Без credentials сетевые команды открывают вход и согласие в админке. Можно указать +корень Core API, UI или UI /api; CLI при необходимости обнаружит API. +С --no-browser откройте показанный URL самостоятельно. Согласие выдаёт schema:read +на 10 минут, с привязкой к человеческой сессии и PKCE S256. Читать/изменять записи +оно не позволяет. Токен остаётся только в памяти команды; следующий онлайн-вызов +снова требует согласия. Отзыв сессии отзывает доступ. AUTH_UI_URL должен указывать +на доступный origin админки. + +Для CI используйте ASMBLYR_ACCESS_TOKEN или --token-stdin и явный Core API/UI /api. +Credentials не сохраняются в генерируемых файлах. Требуется HTTPS, кроме localhost; +HTTP-редиректы отклоняются. + +Connect создаёт asmblyr.config.json, asmblyr.schema.json и asmblyr.schema.ts, +не перезаписывая существующие файлы. Schema pull обновляет подключение. +--config, --schema-file и --types-file выбирают относительные пути проекта. +Asm generate (также asm schema generate) пересоздаёт TypeScript по проверенному +хешем снимку без сети и токенов. + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; +import { schema } from "./asmblyr.schema.js"; + +const client = createClient({ + baseUrl: "https://asmblyr.example.test", + accessToken: process.env.ASMBLYR_ACCESS_TOKEN, + schema, +}); +const articles = await client.Articles.select((a) => [a.id, a.title]) + .where((a) => a.status.eq("published").and(a.price.gte("1000.00"))) + .orderBy((a) => a.created_at.desc()) + .limit(20) + .exec(); +``` + +Пример предполагает наличие указанных коллекций/полей. Рабочий клиент использует +отдельные API-креды или браузерную сессию, а не schema-токен CLI. +Read/Create/Update различают обязательные, readonly-поля и закрытые варианты. +Псевдонимы выводятся из технических имён: articles → Articles. +Collection("articles") тоже поддерживает подсказки. Проекция содержит выбранные +поля; значения остаются optional из-за прав. Decimal/bigint и даты передаются +строками API. Особый TS-трансформер или плагин сборщика не нужен. + +Kit model handlers дают методы вроде client.plugins.calculator.calculate(input), +вызывающие исходный HTTP handler. Core проверяет схемы и AccessGate; при вызове +может потребоваться личное OAuth-подключение. Старые действия без выходной схемы +пропускаются. Браузерные типы/клиент не импортируют Node-only генератор. + +Schema check завершится с кодом 1 при изменении удалённой схемы или типов. +--offline сверяет сохранённый снимок и исходник. Снимок содержит доступные типы, +права и JSON-контракты плагинов, без defaults, условий политик, записей и секретов. +Имена, варианты и аннотации тоже могут быть приватными: публикуйте их только при +допустимости для проекта. Генерируйте с тем же доступом, что у рабочего клиента. +Актуальные права и условия строк всегда проверяет Core. + +[SDK](sdk-guide.md) · [Подготовка пакетов](packages.md). diff --git a/docs/ru/reference/http.md b/docs/ru/reference/http.md new file mode 100644 index 0000000..6aa0462 --- /dev/null +++ b/docs/ru/reference/http.md @@ -0,0 +1,46 @@ + + +# HTTP API и OpenAPI + +Публичный API: `http://localhost:3000/api`, обслуживается непосредственно Core. +Самостоятельный Core на `http://localhost:3001` поддерживает те же маршруты как +без префикса, так и с `/api`. Роут плагина: `/api//...` на общем домене. +Браузер использует HttpOnly-сессию; SDK и внешние приложения — Bearer-токены. + +- Открыть интерактивный справочник +- [Скачать OpenAPI 3.1 JSON](/ru/openapi.json) +- [Матрица маршрутов и доступа](routes.md) +- [HTTP SDK](sdk-guide.md) + +Для первого запроса и типизированного клиента начните с +[подключения SDK](sdk-guide.md#подключение-клиента). Там же описаны +[фильтры и пагинация](sdk-guide.md#реализованные-методы), +[сохранение связей](sdk-guide.md#запись-и-сохранение-связеи) и +[ошибки](sdk-guide.md#ошибки-и-отмена). Поведение коллекций и пределы поиска +описаны в [данных](../features/data.md), права — в +[матрице доступа](../security/access-matrix.md). + +Справочник работает без CDN. Выполнение запросов отключено, чтобы просмотр документации не менял данные. Внешний Swagger validator и сохранение авторизации отключены. + + + +## Полнота + +В спецификации перечислены все статические REST-маршруты Core. Для ключевых операций items/auth/settings описаны JSON-контракты; остальные помечены `x-contract-level: route-only` и содержат путь, метод, параметры пути, требования доступа и source file. Они не претендуют на полное описание тела/ответа. Генерировать полный SDK по этой спецификации пока рано. + +`ALL /oauth/*` вынесен в `x-delegated-routes`: реальные protocol endpoints публикует discovery oidc-provider. Динамические маршруты установленных плагинов определяются их файлами и build index; в статический Core OpenAPI не добавляются автоматически. + + + +## Авторизация и ошибки + +Прямой API использует `Authorization: Bearer `. Основной access token непрозрачен; OAuth token внешнего приложения не подходит. Системные настройки требуют человека и соответствующий section grant. Bearer security scheme описывает передачу токена, а не достаточность прав. + +Обычно 400 — некорректный ввод, 401 — нет действительной учётной записи, 403 — недостаточно прав, 404 — ресурс не найден/не раскрывается, 409 — конфликт, 429 — лимит, 503 — зависимость недоступна. Не все endpoints имеют одинаковую обёртку ответа: auth token pair передаётся напрямую, items/users обычно через data. + +Ошибки Core имеют единый JSON-формат `{ code, message, details, requestId }`. +`details` сейчас пустой объект; закрытые значения записи и внутренние ошибки в +него не попадают. Сохраняйте `requestId` при диагностике. Прокси UI может +сформировать собственную ошибку, если Core вообще недоступен. + +Схема данных коллекции динамическая. Даже успешный create/update может вернуть `data: null` без read grant. Типы больших ID/total остаются строками. Подробный клиентский контракт — в SDK. diff --git a/docs/ru/reference/kit-capabilities.md b/docs/ru/reference/kit-capabilities.md new file mode 100644 index 0000000..8242084 --- /dev/null +++ b/docs/ru/reference/kit-capabilities.md @@ -0,0 +1,98 @@ + + + + +# Возможности плагина + +Пакет перечисляет используемые возможности в `package.json`: + +```json +{ + "asmblyr": { + "manifest": { + "version": 1, + "namespace": "comments", + "capabilities": [ + "identity.profile", + "items.read", + "collections.manage", + "storage.own", + "hooks.items", + "hooks.collections", + "settings" + ] + } + } +} +``` + +Владелец проекта разрешает их по **имени npm-пакета** в корневом `package.json`: + +```json +{ + "asmblyr": { + "plugins": ["@asmblyr-collaborative/plugin-comments"], + "pluginPermissions": { + "@asmblyr-collaborative/plugin-comments": [ + "identity.profile", + "items.read", + "collections.manage", + "storage.own", + "hooks.items", + "hooks.collections", + "settings" + ] + } + } +} +``` + +Core сверяет все включённые пакеты до импорта их серверных модулей. +Неизвестные, повторяющиеся или неразрешённые возможности останавливают запуск. +Отсутствующий список означает отсутствие дополнительных возможностей. +Разрешение, которого нет в декларации самого пакета, не выдаётся. +При обновлении пакета новые возможности требуют изменения конфигурации проекта; +изменения деклараций и разрешений применяются после перезапуска Core. + +| Возможность | Что разрешено | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `identity.profile` | `actor.displayName` текущего пользователя или сервиса. Email, токены и чужие профили не выдаются. | +| `items.read` | `items.list/get` в пределах прав вызывающего пользователя. | +| `items.write` | `items.create/update/delete` в пределах прав пользователя. Без `items.read` ответ записи содержит `data: null`. `commit` требует обе возможности. | +| `collections.manage` | Установка деклараций и выполнение миграций собственных коллекций. Нет произвольного SQL или изменения чужих таблиц. | +| `storage.own` | Доступ к собственным коллекциям через `storage`. Предметные условия проверяет плагин. | +| `hooks.items` | Обработчики `items.create/update/delete`. | +| `hooks.collections` | Обработчик `collections.delete`. | +| `settings` | Декларация настроек и получение значений своего пакета. | +| `notifications` | Подписка текущего человека на запись; публикация, обновление и удаление inbox-событий своего namespace. Получателей и доступ проверяет Core. Model handlers этот контекст не получают. | + +Для обсуждений добавьте `notifications` одновременно в manifest комментариев и +разрешения проекта. См. [обсуждения и уведомления](https://github.com/Asmblyr/Collaborative/blob/main/docs/ru/features/notifications.md). +`context.notifications.publish({ collection, item, eventId, panelId, targetId, preview })` +вызывается внутри `context.withRecord`, рядом с записью события в storage, и должна +быть awaited. `eventId` устраняет повторную доставку в пределах сохранённого inbox; +`preview` — обычный текст до 300 символов. Произвольных получателей или URL нет. + +Базовые `actor.id` и `actor.kind` доступны аутентифицированному endpoint без +дополнительных возможностей: они нужны для авторства операций. +Проверка возможностей действует и для superuser. Возможность `items.write` +не добавляет пользователю права записи. HTTP и MCP используют те же ограничения; +MCP дополнительно учитывает публикацию коллекций и `readOnly` действия. +Привилегированный `storage` model handlers не получают. + +Администратор видит включённые пакеты и разрешения в **Система → Плагины**. +Эта страница не изменяет разрешения: они остаются частью конфигурации проекта. + + + +## Граница контроля + +Это контракт доверенных пакетов, исполняемых в процессе Core. Он контролирует +выданные Core возможности: Kit, установку схемы, hooks и настройки. Произвольный +Node-код пакета технически может импортировать сетевые и файловые библиотеки; +декларация не является песочницей. UI пакета также исполняется в контексте админки. +Устанавливайте только доверенные пакеты. Изолированное выполнение — отдельный этап. + +Секреты не хранятся в обычных настройках плагина. Для них нужен отдельный контракт +хранения и выдачи. Объекты запроса, `items`, `storage` и контекст hook нельзя +сохранять в глобальных переменных или использовать в фоновых задачах. diff --git a/docs/ru/reference/kit-fields.md b/docs/ru/reference/kit-fields.md new file mode 100644 index 0000000..2c482e7 --- /dev/null +++ b/docs/ru/reference/kit-fields.md @@ -0,0 +1,104 @@ + + + + +# Редакторы полей из плагинов + +Первый этап поддерживает обычные поля хранения `text`. Плагин предоставляет +ввод, необязательное отображение значения и настройки каждого поля. Его +регистрация использует существующий браузерный export `./ui`: + +```ts +export default defineUiPlugin({ + fieldInterfaces: [ + { + id: "picker", + title: "Цвет", + types: ["text"], + editor: ColorEditor, + display: ColorDisplay, + settings: ColorSettings, + }, + ], +}); +``` + +Рабочий пример: [examples/plugins/color](plugin-color.md). Серверный +`plugin.ts` остаётся `definePlugin({})`; таблицы и HTTP endpoints этому плагину +не нужны. Включение пакета, сборка, source/built discovery и общие компоненты +работают так же, как у страниц и вкладок. + + + +## Контракт компонентов + +Типы доступны из `@asmblyr-collaborative/kit/ui`: + +- `FieldEditorProps`: `id`, `label`, `value`, `options`, `disabled`, `required`, + `placeholder`, `describedBy`, `onChange(value)`. +- `FieldDisplayProps`: `value: string | null`, `options`. +- `FieldInterfaceSettingsProps`: `options`, `disabled`, `onChange(options)`. + +`options` — JSON-объект. Плагин должен читать пустые, старые и неизвестные +настройки с подходящими значениями по умолчанию. Компонент настроек сохраняет +изменения через callback; настройки принадлежат конкретному полю. + +Редактор получает строковый черновик, пустая строка означает отсутствие ввода. +Преобразование в `null`, пропуск default при создании, проверка обязательности +и отправка в API принадлежат форме Core. Нельзя сохранять запись из редактора +или подменять callback собственным хранилищем. Плагин вызывает `onChange` только +в ответ на действие пользователя, учитывает `disabled` и не меняет значение +при монтировании. Права на чтение и изменение полей проверяются хостом и API. + +Для проверок ввода используйте обычные `required`, `pattern`, `setCustomValidity` +на Input/Textarea. Форма проверяет их перед сохранением изменённых полей и +показывает ошибку у нужного поля. Серверные ограничения остаются обязательными: +UI-проверки не являются ограничением API или способом авторизации. + +Компоненты shadcn импортируются из `@asmblyr-collaborative/kit/ui/`. Хост передаёт +контейнер нативного диалога через `PortalContainerContext`: вложенные Select +остаются внутри окна. Компоненты должны поддерживать серверный рендер React; +доступ к DOM допустим в обработчиках и effects, а не при импорте/рендере. + + + +## Хранение и отключение + +В существующем JSONB presentation сохраняется: + +```json +{ + "interface": "auto", + "extension": { + "id": "color:picker", + "options": { "palette": ["#0d9488", "#3b82f6"], "allowCustom": false } + } +} +``` + +ID формируется как `:`. Core проверяет формат, совместимый +тип и ограниченный JSON: до 8 КиБ, 8 уровней, 1000 узлов. URL/импорты из +метаданных не исполняются. Настройки видны читателям поля и не подходят для +секретов. Новая миграция базы не требуется. + +Настройки сохраняются и для отсутствующего пакета. UI использует только +собранные расширения, включённые в Core. Если расширение отсутствует, ввод +возвращается к стандартному полю с уведомлением, отображение — к обычному +тексту. Ошибка клиентского рендера компонента также включает fallback. Черновик +остаётся в хосте. Повторное включение пакета возвращает оформление. + +Смена редактора в настройках — явное действие: она удаляет ссылку на предыдущий +редактор и его options, не меняя записанные значения. Настройка плагина имеет +приоритет над встроенным display; при её выборе UI сбрасывает старый display. + + + +## Границы этапа + +- Один редактор может включать свой display; независимый выбор display плагина + пока не реализован. +- Расширение не определяет SQL-тип, серверную валидацию или отдельные права. +- Цвет поддерживает HEX `#RRGGBB`. `allowCustom` управляет выбором в UI; + через API поле остаётся строкой с обычными ограничениями Core. +- События, внешние справочники, редакторы связей и асинхронные операции + редактора относятся к следующим этапам. diff --git a/docs/ru/reference/kit-guide.md b/docs/ru/reference/kit-guide.md new file mode 100644 index 0000000..48eb83c --- /dev/null +++ b/docs/ru/reference/kit-guide.md @@ -0,0 +1,467 @@ + + +# @asmblyr-collaborative/kit + +Типизированный API для описания плагинов Asmblyr. + +[Возможности и разрешения](kit-capabilities.md) · [Hooks и настройки](kit-hooks.md) + +Плагины также могут предоставлять [редакторы текстовых полей](kit-fields.md) +с настройками и отображением в таблице. Пример — `examples/plugins/color`. + +`@asmblyr-collaborative/kit` содержит серверный контекст, H3-обработчики и сборщик плагинов. +HTTP-клиент для браузера и Node находится в отдельном [@asmblyr-collaborative/sdk](sdk-guide.md). +Общие типы запросов и ответов принадлежат `@asmblyr-collaborative/contracts`; +kit переэкспортирует типы, необходимые авторам обработчиков. + +Обязательный `plugin.ts` в корне пакета пока остаётся пустым описанием: + +```ts +import { definePlugin } from "@asmblyr-collaborative/kit"; + +export default definePlugin({}); +``` + +Маршруты обнаруживаются по файлам в `server/api`. Перечислять их в `plugin.ts` +не нужно. Дополнительные возможности этого файла определим отдельно. + +Тот же принцип действует для ассистента и MCP: `defineModelContext` связывает +существующий h3-обработчик с `defineModelAnnotation`. Сборщик выводит схемы из типов +TypeScript; описание хранится рядом с обработчиком. Контракт, пример и правила проверки — +в [действиях плагинов](../development/plugin-actions.md). + + + +## Обработчик + +Файл `server/api/comments/status.get.ts`: + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; + +export default defineHandler((event) => { + const { actor, logger } = useAsmblyr(event); + logger.info("Status requested", { actorId: actor.id }); + return { data: { status: "scaffold" } }; +}); +``` + +- `defineHandler` — реэкспорт настоящего обработчика H3 **2.0.1-rc.32**. На входе + `H3Event`; HTTP-утилиты можно импортировать из `h3`, добавив ту же закреплённую + версию в зависимости своего пакета. H3 v1 с этим контрактом несовместим. +- `useAsmblyr(event)` возвращает проверенный контекст: `actor` (`id`, `kind`, + необязательный `displayName`), `requestId`, `logger`, `items` и разрешённые возможности пакета. + `storage` требует namespace и `storage.own`; `displayName` — `identity.profile`. Вызов без контекста Core завершается ошибкой. + Возможность `notifications` добавляет подписки и публикацию событий своего + namespace в личные входящие. Core выбирает получателей и проверяет права на запись. + См. [контракт возможностей](kit-capabilities.md). +- Core проверяет access token пользователя или сервисного аккаунта до чтения тела + и запуска обработчика. Анонимных endpoints нет. `Authorization` в `event.req` + не передаётся; Core также удаляет cookies сессии и провайдеров из запроса к плагину. +- Параметры и query читаются функциями H3: `getRouterParam`, `getQuery`, + `getValidatedQuery`. Для декодирования параметра используйте `{ decode: true }` + один раз. Тело читается через `event.req.json()`, `.text()`, `.formData()` или + `readValidatedBody`. Generic-параметр сам по себе не проверяет входные данные. +- Можно возвращать объект, строку, бинарные данные, `Response`, поток или Promise. + Статус и заголовки задаются через `event.res` либо в `Response`. + Строка теперь отправляется как текст; для JSON-строки нужен `Response.json(value)`. + `null`/`undefined` означают пустое тело. Объекты остаются JSON. +- `HTTPError` из H3 поддерживается. Сохранён и `EndpointError` для ошибок с кодом + Asmblyr. Ошибки имеют общий формат `{ code, message, requestId }`; подробности + серверных ошибок остаются в журнале. Клиентские HTTPError сохраняют свои headers. +- Вход не означает доступ к данным коллекций. `items` проверяет право нужного + действия на коллекцию и доступ к её полям через общую с `/items` логику Core. +- `useItems(event)` — общий доступ к этому сервису в обычном endpoint и в model + handler. В model handler `useActionContext(event)` также даёт actor, superuser и + signal; полного контекста `useAsmblyr` и привилегированного storage там нет. + `AccessGate.authenticated` разрешает вход, а права данных проверяет каждый метод + items. У MCP дополнительно учитывается публикация всех затронутых коллекций. + +Это API доверенных серверных пакетов, выполняемых в процессе Core, без песочницы. + + + +## Чтение данных + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; + +export default defineHandler(async (event) => { + const { items } = useAsmblyr(event); + return items.list("articles", { + fields: ["id", "title"], + limit: 20, + page: 1, + sort: "title", + direction: "asc", + }); +}); +``` + +Одна запись: `await items.get("articles", 4, { fields: ["title"] })`. +`items` из kit вызывает общий сервис Core внутри процесса, без дополнительного HTTP-запроса. + +| Метод | Результат | +| ------------------------------------- | ------------------------------------------------------------------ | +| `items.list(collection, options?)` | `{ data, labels, page: { number, size, total, sort, direction } }` | +| `items.get(collection, id, options?)` | `{ data, label }`; отсутствующая запись — ошибка 404 | + +Контракты `ItemsReader`, `ItemListOptions`, `ItemReadOptions`, `ItemListResult`, +`ItemResult`, `ItemRecord` и типы фильтров экспортируются из `@asmblyr-collaborative/kit`. + +- `collection` и `fields` используют технические имена. Выбор полей относится + к физическим столбцам, включая внешние ключи и системные даты. Разворачивания + связанных записей и выбора виртуальных полей через `fields` пока нет. +- Без `fields` возвращаются все доступные поля. `fields: []` возвращает только + первичный ключ; ключ всегда включён. `*` указывать не нужно. Недоступное поле — + ошибка 403, неизвестное — 400. Проекция выполняется в SQL. +- Сортировка, поиск и фильтры проверяются по полному набору разрешённых полей, + независимо от `fields`. Например, можно вернуть только `id`, сортируя по + доступному `title`. `labels`/`label` тоже могут использовать доступные поля вне + проекции, но не закрытые поля своей или связанной коллекции. +- `page` начинается с 1, `limit` — от 1 до 100 (по умолчанию 100), `total` — строка. + Это те же ограничения, что у HTTP API. Без `sort` используется первичный ключ. +- `id` — строка или безопасное целое JS. Большие `bigserial` передавайте строками; + в результате они также остаются строками. Поля имеют тип `unknown`, поскольку + схемы динамические. Календарный `date` всегда остаётся строкой `YYYY-MM-DD`, + обычный `bigint` — точной десятичной строкой. Внутри обработчика `datetime` может быть `Date`; + при JSON-ответе они сериализуются в ISO-строки, как в `/items`. +- Поддерживаются `q` и объект `filter` с теми же операторами и проверками, что у + HTTP API. Значения условий — строки или массивы строк, включая числа и boolean. + Фильтры по связям ограничены одним переходом и требуют прав на обе стороны. + Действуют общие лимиты: 8192 символа JSON, 3 уровня групп, 20 условий, 30 узлов. +- Права загружаются один раз при входе в endpoint. Изменение политик и отзыв + сессии/сервисного ключа учитываются в следующем запросе. Не сохраняйте `items` + в глобальной переменной и не передавайте в фоновые задачи: это контекст запроса. +- Переопределение actor, `superuser` или прав не предусмотрено. Неизвестные опции + чтения отклоняются. В kit нет доступа к Knex, токенам или системным + таблицам через `items`. Код плагина остаётся доверенным кодом процесса. +- Условия permissions проверяются Core: до count/пагинации, отдельно от фильтра + вызывающей стороны. Поля проецируются по сработавшим веткам; изменения проверяют + исходную и итоговую запись. См. [права и границы первой версии](../features/access.md). + Пользовательский `filter` сужает выдачу и не заменяет такую политику. + Статусы записей автоматически не отфильтровываются: как в `/items`, нужное + условие передаёт вызывающая сторона. + +Пример фильтра по доступному полю связанной коллекции: + +```ts +const result = await items.list("articles", { + fields: ["title"], + filter: { + logic: "and", + children: [{ field: "author_id.name", op: "eq", value: "Анна" }], + }, +}); +``` + +Ошибки чтения проходят через общую обработку Core: 400 — некорректный запрос, +403 — нет разрешения, 404 — коллекция или запись не найдена. До запуска endpoint +невалидный access token даёт 401. Ошибка не превращается в пустую выдачу. + +HTTP API использует ту же проекцию: `GET /items/articles?fields=id,title` и +`GET /items/articles/4?fields=title`. Без параметра формат ответа сохранён. + + + +## Запись данных + +```ts +const { items } = useAsmblyr(event); +const result = await items.create("articles", { title: "Статья" }); +await items.update("articles", 4, { title: "Новое название" }); +await items.delete("articles", 4); +``` + +Контекст предоставляет `ItemsService`, включающий чтение и запись. `ItemsReader` +остаётся отдельным типом для функций, которым требуется только чтение. +Входные значения — JSON-объекты; правила полей и отношений проверяет Core. + +- `create`/`update` возвращают `{ data }` только с читаемыми полями. Если права + чтения нет, успешный результат — `{ data: null }`. `delete` возвращает `void`. +- Core использует общую с HTTP API логику в `items/writer.ts`: права, проверку + связанной коллекции, валидацию, запись и проекцию результата. +- Каждая операция и её история выполняются в транзакции. Автор истории берётся + из текущей сессии пользователя или сервисного аккаунта. ID операции истории + генерирует Core; это UUID, отдельный от HTTP `requestId`. +- Несколько вызовов не образуют одну транзакцию. Для записи со связями есть + `items.commit(collection, draft)`, использующий существующий механизм черновиков. + Возвращает `{ data: { id } }`. Сбой откатывает весь черновик, включая историю. +- `ItemCommitDraft` содержит values/references/relations/records, до 100 изменений + и глубины 5. Для изменения существующей записи через commit нужно и чтение. + `expectedValues` в каждой изменяемой записи включает исходные значения всех + обновляемых полей, включая FK. Core проверяет их под блокировкой строки; + несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. + Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. + Полный формат общий с [HTTP SDK](sdk-guide.md#запись-и-сохранение-связеи). +- Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, + 409 — конфликт связи/ключа или вторая запись в single-коллекции. + +Собственные коллекции плагина устанавливаются Core при первом запуске. +Формат описания и ограничения обновления приведены ниже. + + + +### Пример POST с проверкой тела + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; +import { readValidatedBody } from "h3"; + +export default defineHandler(async (event) => { + const body = await readValidatedBody(event, (value: unknown) => { + if (!value || typeof value !== "object" || !("text" in value)) return false; + if (typeof value.text !== "string" || !value.text.trim()) return false; + return { text: value.text.trim() }; + }); + return { data: { text: body.text, actorId: useAsmblyr(event).actor.id } }; +}); +``` + + + +### Граница HTTP + +Fastify продолжает маршрутизировать запросы. Изолированный parser читает исходные +байты тела с лимитом **1 МиБ**; H3 разбирает их один раз. Multipart поддерживается +в этом лимите, потоковая загрузка больших файлов пока не реализована. +Ответы идут потоком напрямую через Fastify без преобразования в JSON и без полной +буферизации. Отключение клиента передаётся в `event.req.signal`. + +Core сохраняет content-type, статусы, cookies плагина и end-to-end headers. +Имена cookies с префиксом `asmblyr_` и настроенным `SESSION_COOKIE_PREFIX` +зарезервированы; плагин не читает и не заменяет cookies сессии или провайдеров. +Редирект передаётся браузеру без серверного перехода с токеном Core. +В `Location` следует использовать публичный адрес, например `/api/comments/status`. +Промежуточного UI-прокси и его таймаута нет; клиент управляет отменой запроса. +Все ответы плагинов имеют `Cache-Control: no-store`. + +Это интеграция обработчиков H3, а не полноценный Nitro-сервер. `event.app`, +сырой Node req/res, WebSocket upgrades и фоновый lifecycle пока не предоставляются. +`plugin.ts` по-прежнему описывает пакет Asmblyr; `definePlugin` из `h3` — другое API. + + + +## Адреса + +Путь определяется относительно `server/api`, метод — суффиксом файла: + +| Файл внутри `server/api` | Публичный маршрут на адресе админки | +| ------------------------------------ | ----------------------------------- | +| `comments/index.get.ts` | `GET /api/comments` | +| `comments/index.post.ts` | `POST /api/comments` | +| `comments/[id].delete.ts` | `DELETE /api/comments/:id` | +| `comments/[id]/replies/index.get.ts` | `GET /api/comments/:id/replies` | + +Core использует те же пути без общего префикса `/api`. Имя npm-пакета в URL не +подставляется. Поддерживаются `.get.ts`, `.post.ts`, `.put.ts`, `.patch.ts` и +`.delete.ts`. Файл без суффикса метода, например `comments/status.ts`, обслуживает +все пять методов. Каждый файл должен экспортировать обработчик по умолчанию. + +Вспомогательные функции размещаются вне `server/api`. Файлы `.d.ts` не считаются +маршрутами. Необязательные параметры, catch-all, составные параметры в сегменте +и отдельные HEAD/OPTIONS handlers пока не поддерживаются. + +Первый сегмент пути принадлежит плагину. Core отклоняет пересечения с собственными +разделами, другими плагинами и дубли маршрутов. Сборка также отклоняет неоднозначные +файлы, например `[id].get.ts` и `[key].get.ts` в одном каталоге. +Браузер вызывает Core через `/api` на общем домене с текущей HttpOnly-сессией. +Core проверяет сессию и Origin для изменений. + + + +## Коллекции плагина + +В `server/collections/entries.ts` плагина comments есть пример с +`defineCollection()` из kit. Core проверяет описание и при первом запуске создаёт +таблицу вместе с метаданными. Файлы лежат непосредственно в `server/collections`, +имя файла совпадает с локальным именем коллекции. Подкаталоги и симлинки запрещены. + +```ts +import { defineCollection } from "@asmblyr-collaborative/kit"; + +export default defineCollection({ + name: "entries", + primaryKey: { name: "id", type: "uuid" }, + timestamps: { createdAt: true, updatedAt: true }, + presentation: { displayName: "Комментарии", hidden: true }, + fields: { + body: { + type: "text", + required: true, + nullable: false, + presentation: { label: "Комментарий", interface: "textarea" }, + }, + }, +}); +``` + +- `name` — локальное имя внутри плагина. Namespace задаётся в манифесте, + а Core формирует имя таблицы: `plugin__`, например `plugin_comments_entries`. +- `primaryKey` и `timestamps` используют общие с Core типы из contracts. + Ключ и системные даты не нужно повторять в `fields`. +- Ключи объекта `fields` — технические имена полей. Порядок объявления задаёт + порядок создания столбцов. Метаданные полей сохраняются при установке. +- `required` — обязательность непустого значения в API; `nullable` — допустимость + SQL NULL в БД. Оба параметра явные и независимые: `true` / `true` допустимо. +- Типы полей соответствуют Core. Тип значения `defaultValue` зависит от поля; + `date` и `bigint` используют строки, включая `CollectionRow` и входы storage. + для `file` / `files` дефолт не поддерживается. `searchable` доступен для `text` + и `email`; создание индекса это свойство не описывает. +- `presentation` поля использует существующий `FieldPresentation` из contracts; + можно указать только нужные настройки. У коллекции пока доступны `displayName` + и `hidden`. Скрытие из навигации не заменяет права или защиту структуры. +- `defineCollection` возвращает описание без побочных действий. Это не runtime- + валидатор: допустимость имён, UUID/дат/чисел, сочетаний настроек интерфейса и + конфликтов полей проверяет загрузчик Core через общие валидаторы коллекций. + +Пакет с коллекциями задаёт `asmblyr.manifest.namespace`, например `comments`, +и экспортирует `"./collections": "./dist/collections.json"`. Namespace — стабильное +имя из 1–31 строчных латинских букв, цифр и подчёркиваний, начиная с буквы. +Он уникален между установленными пакетами; `asmblyr` и префиксы `asmblyr_` / `plugin_` +зарезервированы. Полное имя таблицы должно умещаться в 63 символа PostgreSQL. +Сборщик создаёт индекс деклараций. В dev Core сканирует исходники локального пакета, +в production читает индекс и скомпилированные модули. `plugin.ts` остаётся пустым. + +Core хранит namespace и npm-владельца в `asmblyr_plugins`, а локальное имя, +физическое имя и нормализованное описание — в `asmblyr_plugin_collections`. +Реестр создаётся обычной миграцией Core (`pnpm db:migrate`). При старте установка +всех включённых коллекций проходит в одной транзакции под общей блокировкой. +Чужие таблицы и метаданные не присваиваются. Повторный запуск сохраняет данные. + +Изменения установленного описания должны быть воспроизведены явными миграциями +из `server/migrations`. Несовпадение итогового описания останавливает запуск. +Новые коллекции добавляются автоматически. Исчезнувшая +таблица не создаётся заново поверх реестра. Прямые изменения структуры через SQL +не синхронизируются автоматически. Отключение плагина сохраняет таблицы и владельца. + +Префикс `plugin_` зарезервирован в редакторе структуры и API независимо от того, +включён ли пакет. Обычные пользователи и superuser не меняют структуру и настройки +таких коллекций. Строки доступны через `/items` и kit по обычным правам, включая +историю изменений; отключение кода плагина само по себе эти права не отзывает. +MCP при установке выключен. Связи между коллекциями плагина пока не поддержаны +декларациями. Формат пока экспериментальный. + + + +## Миграции, хранилище и UI + +Полный контракт и ограничения описаны в [жизненном цикле плагина](kit-lifecycle.md). +Пример со всеми тремя возможностями — `packages/plugin-comments`. + + + +## Пакет и подключение + +Имя, версия и зависимости плагина остаются в его `package.json`. +`asmblyr.manifest.version` задаёт версию формата манифеста, а не версию kit. + +Пакет плагина экспортирует собранный модуль по `exports["."].default`, типы по +`exports["."].types`, метаданные по `exports["./package.json"]` и сгенерированный +`dist/routes.json` по `exports["./routes"]`. Пакеты с namespace также экспортируют +`dist/collections.json` по `exports["./collections"]`. +Пример настройки находится в `packages/plugin-comments/package.json`. + +Пакеты устанавливаются как зависимости корневого проекта и явно включаются +в его `package.json`: + +```json +{ + "asmblyr": { + "plugins": ["@asmblyr-collaborative/plugin-comments"] + } +} +``` + +Core проверяет имена, версии манифестов и наличие обработчиков всех включённых +пакетов перед импортом. Ошибки включённого плагина останавливают запуск. +Сканирование происходит только внутри включённых пакетов; установка сама по себе +не включает плагин. + + + +## Разработка и сборка + +В разработке `pnpm dev` использует исходники включённых локальных пакетов из +workspace-пакетов внутри проекта. Изменение, добавление и удаление `.ts`-файлов автоматически перезапускает +Core и обновляет маршруты. Это перезапуск процесса, а не замена обработчика на лету. + +Сборка плагина выполняется командой kit `asmblyr-plugin build` в каталоге пакета; +в comments она задана в script `build`. TypeScript использует `rootDir: "."`, +`outDir: "dist"`, включая `plugin.ts` и `server/**/*.ts`. +Для типов Web API добавлены `DOM` и `DOM.Iterable` в `lib`. В kit и примере включён +`skipLibCheck`: декларации выбранной RC содержат несовместимости с TypeScript 5.9 +и ссылки на опциональные зависимости. Проверка собственного кода остаётся строгой; +при обновлении H3 это ограничение нужно пересмотреть. +Сборщик проверяет имена маршрутов, default exports и типы, очищает собственный +`dist`, создаёт JavaScript, декларации типов и список маршрутов `dist/routes.json`. + +В production и для установленных npm-пакетов Core читает готовый список и загружает +JavaScript. Сканирования исходников в этом режиме нет. Удалённые исходные маршруты +не сохраняются в следующей сборке. + +В репозитории Kit и comments связаны как workspace-пакеты. `pnpm build:plugins` собирает оба; +`pnpm dev` выполняет начальную сборку автоматически. `pnpm build` сначала собирает +пакеты, затем Core и UI. Публикация пакетов остаётся отдельным шагом. + +Проверка kit и первого плагина из корня репозитория: + +```sh +pnpm build:plugins +pnpm --filter @asmblyr-collaborative/kit --filter @asmblyr-collaborative/plugin-comments typecheck +``` + + + +## Локализация расширения + +Необязательные плоские JSON-каталоги `locales/ru.json` и `locales/en.json` +располагаются рядом с `package.json`. Включайте `locales/` в опубликованный пакет +вместе с `dist/`. Core читает их одинаково в source и built режиме и добавляет +в защищённый `GET /translations`. Каталоги содержат только публичные подписи: не +добавляйте в них настройки, ключи или пользовательские данные. + +UI использует тот же каталог через `defineUiPlugin({ translations: { ru, en }, ... })`. +Для JSON-импортов включите `resolveJsonModule` в tsconfig. У страниц, панелей +и интерфейсов поля можно указать `titleKey`; `title` остаётся запасной подписью. + +```tsx +import { usePluginTranslations } from "@asmblyr-collaborative/kit/ui/i18n"; + +function Panel() { + const { t, locale } = usePluginTranslations("comments"); + return

{t("panel.title", "Discussion")}

; +} +``` + +Хост предоставляет изолированный i18next provider на текущем языке профиля, +пространство имён `plugin.` и русский fallback. React +экранирует текст при отображении. Не импортируйте серверный entry point в UI. +Ключи `collection..label` и +`field...label|description|placeholder` задают подписи +собственных таблиц в UI и API. Для настроек используются `settings.title`, +`settings.description` и `settings..label|description`. +Имена коллекций/полей, значения записей и HTTP-контракты не переводятся. + + + +## Личные внешние подключения + +Model handler с `connection: "google"` в `defineModelAnnotation` виден ассистенту только при активном подключении текущего человека. Пакет запрашивает capability `connections.google`, установка явно одобряет её в `asmblyr.pluginPermissions`. + +`useActionContext(event).connections?.google` предоставляет owner-bound операции `list`, `readText`, `sheet`, `cells`, `proposeWrite`. Контракты `PersonalConnections`, `GoogleWriteInput`, `GoogleFileList`, `GoogleText`, `GoogleSheet`, `GoogleCells` экспортируются Kit. Нет raw token, произвольного URL или метода подтверждения. `proposeWrite` только сохраняет предложение; реальную запись подтверждает человек в UI Core. HTTP и внутренний MCP используют одинаковый handler. Пример — `packages/plugin-google-workspace`; ограничения и настройка — [Google Workspace](../features/google-workspace.md). + + + +## Контракты потребителей SDK + +Собранные defineModelContext handlers содержат JSON-схемы входа/выхода. +Core экспортирует доступные методы через GET /schema по их AccessGate и исходному +HTTP-адресу. SDK создаёт client.plugins.namespace[methodId](https://github.com/Asmblyr/Collaborative/blob/main/packages/kit/input) с выведенным +результатом. ID соединяет сегменты пути дефисами; дополнительный реестр не нужен. + +Контракт описывает ограниченное JSON-подмножество Kit. Он не выдаёт права данных +и не гарантирует наличие личного OAuth-подключения. Выполнение проверяет исходный +gate и текущие права. Старые defineAction без выходной схемы не входят в типизированный API. + +[Проверки и публикация](packages.md). diff --git a/docs/ru/reference/kit-hooks.md b/docs/ru/reference/kit-hooks.md new file mode 100644 index 0000000..6293453 --- /dev/null +++ b/docs/ru/reference/kit-hooks.md @@ -0,0 +1,119 @@ + + + + +# Hooks и настройки + +`plugin.ts` остаётся `definePlugin({})`. Core обнаруживает дополнительные файлы +автоматически. Возможности объявляются и разрешаются по [контракту capabilities](kit-capabilities.md). + + + +## Транзакционные hooks + +Каждый файл непосредственно в `server/hooks/` экспортирует один обработчик: + +```ts +import { defineHook } from "@asmblyr-collaborative/kit"; + +export default defineHook("items.delete", async (event, context) => { + context.logger.info("Record removed", { + collection: event.collection, + itemId: event.itemId, + }); +}); +``` + +Пример очистки с пагинацией: `packages/plugin-comments/server/services/cleanup.ts`. +Не создавайте реестр hooks в `plugin.ts`. + +| Событие | Данные | +| -------------------- | -------------------------------------- | +| `items.create` | `collection`, `collectionId`, `itemId` | +| `items.update` | `collection`, `collectionId`, `itemId` | +| `items.delete` | `collection`, `collectionId`, `itemId` | +| `collections.delete` | `collection`, `collectionId` | + +`collectionId` — стабильный ID экземпляра коллекции; имя может впоследствии +принадлежать другой коллекции. `itemId` — строковое представление сохранённого ключа. +Скрытые значения полей в событие не попадают. Контекст предоставляет actor, +requestId, logger, reader `items`, разрешённый `storage` и настройки пакета. +Reader использует права исходного пользователя; удалённую запись уже не прочитать. + +Событие вызывается после мутации и записи истории, **до commit**. Hook выполняется +в той же транзакции, включая свою работу с `storage`. Ошибка прерывает операцию и +откатывает данные, историю и изменения предыдущих hooks. Обработчики выполняются +последовательно: порядок пакетов в конфигурации, затем имена файлов по порядку +кодовых единиц строки. Source и production используют один порядок. +Нельзя рассчитывать, что внешний HTTP-вызов или отправка уведомления откатятся +вместе с БД. Такие эффекты и гарантированная доставка после commit пока не поддержаны. +Каждый вызов сервиса обязательно нужно `await`-ить. + +HTTP items, Kit, MCP, массовые изменения, операции связей и сохранение черновика +используют общую границу мутаций. Нет событий для update без изменений, прямого +SQL, каскадных действий PostgreSQL и записей в `asmblyr_`/`plugin_`. Исключение +внутренних коллекций предотвращает рекурсию при очистке собственного хранилища. +Отключённый плагин не получает события; его данные сохраняются. + +Production export: `"./hooks": "./dist/hooks.json"`. +Сборщик создаёт индекс; вручную поддерживать его не нужно. +В `server/hooks` допустимы только непосредственные `.ts`-файлы без symlinks; +вспомогательный код размещается в `server/services` или другой соседней папке. + + + +## Работа с данными, привязанными к записи + +В обычном endpoint `context.withRecord(collection, id, async context => ...)` +проверяет право чтения, удерживает разделяемую блокировку записи и выполняет callback +в транзакции. Используйте **контекст callback** для своего `storage`: он участвует +в той же транзакции. Это позволяет сохранять комментарий и исключить одновременное +удаление его целевой записи. Метод требует `items.read`; в model handlers его нет. +В callback не выполняйте длительную внешнюю работу и не сохраняйте контекст наружу. + + + +## Настройки + +Файл `server/settings.ts`: + +```ts +import { defineSettings } from "@asmblyr-collaborative/kit"; + +export default defineSettings({ + title: "Комментарии", + fields: { + allowNewComments: { + type: "boolean", + label: "Разрешить новые комментарии", + default: true, + }, + }, +}); +``` + +Использование в endpoint, model handler или hook: + +```ts +const options = useSettings(context, settings); +// options.allowNewComments имеет тип boolean. +``` + +Поддержаны boolean, строка с maxLength, число с min/max/integer, select со списком +вариантов. Core проверяет декларацию, default и каждое сохраняемое значение. +Неизвестные поля, пропущенные значения и неявное приведение типов не допускаются. +До 40 полей и 32 000 символов JSON значений. Это обычные настройки, не секреты. + +Production export: `"./settings": "./dist/server/settings.js"`. +Настройки доступны через `/settings/plugins/:namespace` и форму **Настройки → Расширения**: +чтение требует `plugins/read` (либо `plugins/update`), запись — `plugins/update`; +superuser также имеет доступ. PUT принимает `{ values, revision }`; устаревшая +revision возвращает 409, сохраняя черновик в UI. `null` обозначает ещё не сохранённые +значения по умолчанию. Кнопка «По умолчанию» меняет черновик до явного сохранения. + +Значения хранятся в `asmblyr_settings` под ключом `plugin:`. +Изменения действуют для новых запросов без перезапуска; в журнале безопасности +сохраняются автор, namespace и имена изменённых настроек, без копирования значений. +Новые поля получают default, удалённые игнорируются. Несовместимое изменение типа +или диапазона существующей настройки требует предварительного обновления её значения: +Core отклоняет неверное сохранённое значение и не исправляет его молча. diff --git a/docs/ru/reference/kit-lifecycle.md b/docs/ru/reference/kit-lifecycle.md new file mode 100644 index 0000000..ecfeaab --- /dev/null +++ b/docs/ru/reference/kit-lifecycle.md @@ -0,0 +1,200 @@ + + + + +# Миграции, хранилище и UI плагина + + + +## Миграции + +Файлы `server/migrations/YYYYMMDDHHmmss_name.ts` экспортируют `defineMigration()`. +Нужен export `"./migrations": "./dist/migrations.json"` в package.json. + +```ts +import { defineMigration } from "@asmblyr-collaborative/kit"; + +export default defineMigration({ + operations: [ + { + type: "addField", + collection: "entries", + name: "author_id", + field: { type: "uuid", required: false, nullable: true }, + }, + { + type: "addIndex", + collection: "entries", + name: "by_author", + fields: ["author_id"], + }, + ], +}); +``` + +Одновременно обновите актуальное описание в `server/collections`. Миграции содержат +неизменяемые JSON-планы: контрольная сумма не зависит от компиляции TS в JS. +Не импортируйте изменяемое актуальное описание в старую миграцию: её план должен +оставаться прежним. Есть `addField` и обычный составной B-tree `addIndex`. +SQL, `drop`, смены типов, переименований, backfill и автоматического down пока нет. +NOT NULL без default на заполненной таблице завершится ошибкой PostgreSQL и откатом. + +Core под общей блокировкой запускает новые планы по имени файла. Операции принимают +только локальные имена собственных коллекций. Реестр `asmblyr_plugin_migrations` +хранит имя, SHA-256 плана, время и признак baseline. Изменение/удаление выполненного +плана и добавление плана раньше уже применённого запрещены. Ошибка операции или +несовпадение итоговой декларации откатывает всю установку: DDL, метаданные и журнал. + +При новой установке таблица создаётся по актуальному описанию, addField проверяется, +индексы создаются, планы отмечаются как baseline. Новая таблица в существующем +плагине также получает текущую схему. Удаление деклараций пока не поддержано. +Данные не удаляются при отключении. Down реестра запрещён при наличии истории. +Прямые изменения схемы через SQL автоматически не согласуются с декларациями. + + + +## Собственное хранилище + +`storage` доступен внутри аутентифицированного обработчика пакета с namespace +и разрешённой возможностью `storage.own`. +Методы `list/get/create/update/delete` имеют формат `items`, но принимают локальное +имя, например `storage.list("entries")`. Область доступа ограничена коллекциями +этого пакета. Нет SQL, Knex, `commit`, смены actor или получения чужого namespace. +Применяются валидация Core, история с реальным автором и транзакции записи. + +Это **право доверенного серверного плагина на своё хранилище**. Обычный `items` +по-прежнему использует права пользователя. Плагин обязан проверить предметные +условия до `storage`: comments сначала читает обсуждаемую запись через `items.get`, +потом обращается к своим комментариям. Grants на таблицу комментариев не нужны. + +Прямой `/items/plugin_…` остаётся административным путём с обычными grants; выдача +таких grants разрешает работу с таблицей в обход специализированного endpoint. +Для comments выдавать их участникам не нужно. Код плагина доверенный и выполняется +в процессе Core, без песочницы. Пользовательские таблицы через storage недоступны. + + + +## Типизированное хранилище + +Для типизированного доступа к собственной коллекции используйте её декларацию: + +```ts +import { useStorage } from "@asmblyr-collaborative/kit"; +import entries from "./server/collections/entries.js"; + +const comments = useStorage(context, entries); +const row = await comments.get(id); +// row.body, row.author_id и остальные поля выведены из entries. +``` + +`get/create/update` возвращают саму строку, `list` — `{ data, labels, page }`. +Имена и типы полей, обязательность при создании, nullable и ручной первичный ключ +выводятся из `defineCollection`. Дефолты позволяют пропускать поле при создании. +Генерируемые ключи и системные даты не принимаются в типизированной записи. +Даты нормализуются в ISO-строки, decimal/bigint сохраняются строками. Несовпадение +типов прочитанной строки с декларацией даёт ошибку вместо неявного преобразования. +Проверки записи и область доступа остаются у Core. Этот API читает полные строки; +для проекций используйте исходный `context.storage`. Не сохраняйте его между запросами. + + + +## Вкладки редактора записи + +`ui/index.ts` экспортирует `defineUiPlugin({ recordPanels: [...] })` из +**`@asmblyr-collaborative/kit/ui`**. Package.json экспортирует его сборку как `./ui`. +Корневой entry kit и `plugin.ts` в браузер не импортируются. +У вкладки есть `id`, `title`, React `component` и необязательный `supports(record)`. + +Host передаёт `RecordPanelProps`: + +- `record`: техническое имя коллекции, сохранённый ID и отображаемое имя; +- `request(path, init)`: запрос через сессию UI в `/api//…`; +- `onStateChange({ dirty, busy })`: защита текста и выполняющейся записи от закрытия. + +Компоненты shadcn импортируются напрямую из `@asmblyr-collaborative/kit/ui/`. +Админка и плагины используют одну реализацию из Kit, включая полные props и ref. +Дополнительный provider или prop `components` не требуется. +Подробнее: [общие компоненты UI](kit-ui.md). + +Вкладка монтируется при первом открытии и сохраняется при смене вкладок. +Сбой рендера изолируется от формы записи. Новой несохранённой записи вкладки не +предоставляются. Активные пакеты сверяются с аутентифицированным `/extensions`. + +Next при запуске/сборке создаёт статические импорты из exports включённых пакетов +и источники Tailwind для их UI. Локальная разработка читает `ui/index.ts`, production +и npm — собранный `./ui`. Изменение списка пакетов требует перезапуска dev-сервера; +production требует пересборки UI. Изменения TSX используют обычный Fast Refresh. +React — peer dependency. Для UI используйте `.ts`/`.tsx` в относительных импортах +и `rewriteRelativeImportExtensions: true` в tsconfig: TypeScript заменяет их на +`.js` при сборке. Добавьте `ui/**/*` и `shared/**/*` в include. + +Первый полный пример — `packages/plugin-comments`. Серверные пакеты нельзя импортировать в UI. + + + +## Отдельные страницы + +Второй пример — `examples/plugins/overview`. Браузерный entry может экспортировать +страницы, вкладки записи или оба вида расширений: + +```ts +import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; +import { OverviewPage } from "./pages/overview-page.tsx"; + +export default defineUiPlugin({ + pages: [{ id: "home", title: "Обзор", component: OverviewPage }], +}); +``` + +Маршрут: `/extensions//`. Host добавляет страницу в группу +«Приложения» бокового меню, отображает её название в шапке и использует текущий +layout. ID проверяются на допустимость в URL и уникальность внутри каждого вида +расширений. Страница и вкладка одного пакета могут иметь одинаковый ID. + +`PluginPageProps` предоставляет `request: PluginRequest` — тот же ограниченный +namespace HTTP-клиент, что у вкладок. `Button` и `Textarea` доступны из kit. +Общая оболочка изолирует ошибки отрисовки от остального интерфейса. +Список активных пакетов загружается для layout через аутентифицированный +`/extensions` и используется страницами, меню и вкладками записи. + +Пользователь должен войти и иметь доступ к админке по её существующим правилам. +Отдельные разрешения на UI-страницы пока не реализованы; API каждого плагина обязан +сам проверять предметные права. Отсутствующий или отключённый плагин не отображается. +Включение пакета требует сборки/перезапуска UI и Core. + +Пока поддержаны фиксированные страницы без вложенных и динамических маршрутов. +Страница получает также `preparedAction` и `onStateChange`. +`useAction(contract, props, { path, initialInput })` использует обычный `request` +и связывает схему формы с серверным действием, подготовленным результатом +и признаками dirty/busy для host. Обзор только читает собственный контекст пользователя. +Редакторы полей описаны в [FIELDS.md](kit-fields.md). + + + +## Действия и ассистент + +Первый пример — `examples/plugins/calculator`. TypeScript-интерфейсы описывают поля и результат. +H3 handler в `server/api/calculator/calculate.post.ts` обёрнут в +`defineModelContext(defineHandler(...), annotate)`. +`defineModelAnnotation` описывает назначение и `middleware: AccessGate.authenticated`. +Сборщик и source loader создают `.asmblyr/models` для runtime-валидации и UI; +для импорта JSON формой нужен `resolveJsonModule: true` в tsconfig. +Сканер подключает HTTP-маршрут по файлу; Core публикует явно отмеченные действия +во внутреннем MCP. `plugin.ts` остаётся `definePlugin({})`. +HTTP и MCP вызывают один handler с одним ограниченным контекстом: +`useActionContext(event)` предоставляет actor, superuser, signal и items. +`useItems(event)` работает и в обычных endpoints: проверяет права действия и полей. +Для MCP также проверяется доступность коллекций и связей в MCP. Привилегированного +storage в model handler нет. `readOnly: true` запрещает все методы записи items. +Форма использует `useAction(generatedModel, props, options)`; +схемы браузера и сервера получены из одних типов, серверный handler в UI не попадает. +Подробности и текущие ограничения — [действия плагинов](../development/plugin-actions.md). + + + +## Возможности, hooks и настройки + +Пакет объявляет capabilities в manifest; проект разрешает их через +`asmblyr.pluginPermissions`. Правила и ограничения: [CAPABILITIES.md](kit-capabilities.md). +Транзакционные обработчики в `server/hooks` и настройки в `server/settings.ts` +описаны в [HOOKS.md](kit-hooks.md). Регистрация в `plugin.ts` не требуется. diff --git a/docs/ru/reference/kit-ui.md b/docs/ru/reference/kit-ui.md new file mode 100644 index 0000000..cd75c22 --- /dev/null +++ b/docs/ru/reference/kit-ui.md @@ -0,0 +1,88 @@ + + + + +# Общие компоненты UI + +Регистрация собственных редакторов полей и их настроек описана в +[FIELDS.md](kit-fields.md). Они используют эти же компоненты и тему хоста. + +Kit содержит исходники общих shadcn-компонентов в `src/ui`. Админка и плагины +импортируют одну реализацию. Компоненты работают без `PluginUiProvider`; +обычные props, события, ref, `asChild` и варианты shadcn доступны напрямую. + +```tsx +import { Button } from "@asmblyr-collaborative/kit/ui/button"; +import { Input } from "@asmblyr-collaborative/kit/ui/input"; +import { Textarea } from "@asmblyr-collaborative/kit/ui/textarea"; +import { Checkbox } from "@asmblyr-collaborative/kit/ui/checkbox"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@asmblyr-collaborative/kit/ui/tabs"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@asmblyr-collaborative/kit/ui/select"; +``` + +`buttonVariants` экспортируется из `@asmblyr-collaborative/kit/ui/button` для ссылок и других +элементов, которым нужен стиль кнопки. `@asmblyr-collaborative/kit/ui` содержит регистрацию +плагина и контракты его UI. Серверный entry `@asmblyr-collaborative/kit` в браузер не импортируется. +React и ReactDOM — совместимые peer dependencies: плагины используют экземпляры +хоста. Серверные потребители Kit не обязаны импортировать UI. + + + +## Стили и сборка + +Хост использует Tailwind 4, анимации shadcn и общие переменные темы. В его глобальном +CSS уже подключено: + +```css +@import "@asmblyr-collaborative/kit/ui/styles.css"; +``` + +Этот entry указывает Tailwind на собранные компоненты внутри пакета, поэтому их +классы доступны и из установленного пакета. Плагину повторять импорт не нужно; +его собственные классы хост сканирует через сгенерированный `plugin-ui.css`. +Цвета, радиусы и светлая/тёмная тема принадлежат хосту. + +После изменения Kit выполните из корня `pnpm --filter @asmblyr-collaborative/kit build`. +`pnpm dev` и `pnpm build` также собирают Kit перед приложениями. + + + +## Выпадающие списки внутри нативного dialog + +Radix-портал должен находиться в том же диалоге, чтобы не попасть под его backdrop. +В `SelectContent` можно передать `container={dialogElement}`. Для вложенных контролов +есть общий контекст из `@asmblyr-collaborative/kit/ui/portal-container`: + +```tsx + + {children} + +``` + +`SelectContent` использует явно указанный контейнер или унаследованный контекст. +Popover админки использует этот же контекст. За пределами диалога контейнер не нужен. + + + +## Изменение и добавление компонентов + +Общие Button, Input, Textarea, Select, Checkbox и Tabs меняются в Kit. +Сохраняйте `"use client"` на интерактивных модулях и проверяйте компоненты внутри +нативного диалога. Button и `buttonVariants` остаются доступными серверным компонентам. + +`apps/ui/components.json` пока обслуживает остальные локальные компоненты админки. +Не запускайте `shadcn add --overwrite` для уже перенесённых компонентов в `apps/ui`: +это создаст вторую реализацию. При переносе следующего компонента добавьте явный +export в Kit, перенесите необходимые зависимости и замените импорты потребителей. +Импорты внутри Kit не должны зависеть от `@/` или файлов админки. diff --git a/docs/ru/reference/packages.md b/docs/ru/reference/packages.md new file mode 100644 index 0000000..b8235b2 --- /dev/null +++ b/docs/ru/reference/packages.md @@ -0,0 +1,63 @@ + + +# Пакеты SDK + +В npm доступны четыре MIT-пакета версии 0.1.0-beta.1: + +| Пакет | Назначение | Файлы | +| -------------------------------- | ----------------------------------------------------------------- | ----------------- | +| @asmblyr-collaborative/contracts | Общие HTTP-контракты и каталоги переводов | src, locales | +| @asmblyr-collaborative/sdk | HTTP-клиент, fluent-запросы, методы плагинов, отдельный генератор | dist | +| @asmblyr-collaborative/cli | asm connect, generate, schema pull/check | src | +| @asmblyr-collaborative/kit | Плагины, model-контракты, UI и сборщик | dist, bin, styles | + +Архивы также содержат package metadata, README и MIT-лицензию. Явные списки файлов +исключают тесты, локальную конфигурацию и промежуточную работу. Используйте тег beta. +Первая бета также находится под latest; это не означает стабильный релиз. + +```sh +pnpm install --frozen-lockfile +pnpm build:packages +pnpm packages:check +``` + +Проверка сохраняет архивы в игнорируемой .local-data/packages, проверяет содержимое +и устанавливает их в одноразовый проект вне workspace. Локальные overrides связывают +упакованные зависимости; настоящие package metadata содержат обычные версии. +Проверяются генерация CLI, offline-сверка, стандартная компиляция TypeScript, +fluent/плагинные вызовы и экспорты Kit. Проверка не обращается к установке Collaborative +и ничего не публикует. Для внешних зависимостей нужен npm registry. Убедитесь, что +CI запускает эту проверку. + +Для потребителя: + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +npx asm connect --url https://asmblyr.example.test +npx asm generate +npx asm schema check --offline +``` + +Для локальной разработки используйте workspace-команды или проверенные архивы. +[CLI](cli-guide.md) описывает согласие в браузере, CI-креды и файлы, +[SDK](sdk-guide.md) — рабочий клиент. Особый компилятор или плагин сборщика не нужен. + + + +## Публикация + +Это отдельное действие сопровождающего. Выполните проверки и сборку, изучите архивы, +опубликуйте contracts, затем sdk/kit, затем cli с тегом beta. Обновляйте четыре версии +и lockfile вместе до упаковки. Не публикуйте бету как latest. + +Для автоматизации настройте trusted publisher каждого пакета на точный GitHub +Actions workflow с id-token:write. Механизм npm требует Node 22.14+ и npm 11.5.1+ +и создаёт provenance для поддержанных запусков. Ручной publish-packages.yml +проверяет и публикует согласованную бету по зависимостям из main; он не запускается +при push/PR. Настройте доверие к Asmblyr/Collaborative и этому имени workflow для +всех четырёх пакетов. Настройка доверия registry здесь не подтверждена; доступом +организации управляет сопровождающий. + +Частично опубликованный выпуск автоматически не откатывается: версии неизменяемы. +Перед повтором проверьте npm. См. [trusted publishers](https://docs.npmjs.com/trusted-publishers/). diff --git a/docs/ru/reference/plugin-color.md b/docs/ru/reference/plugin-color.md new file mode 100644 index 0000000..afd10f7 --- /dev/null +++ b/docs/ru/reference/plugin-color.md @@ -0,0 +1,29 @@ + + + + +# Цвет + +Пример расширения редактора обычного текстового поля. + +1. Откройте настройки текстового поля → **Отображение**. +2. В списке **Редактор** выберите **Цвет · расширение**. +3. Настройте палитру и возможность вводить произвольный цвет. +4. Сохраните поле. В карточке появится палитра, в таблице — цвет и HEX. + +Каждое поле хранит свою палитру. Поддержаны 1–24 цвета в формате `#RRGGBB`, +очистка значения, обязательность и блокировка на время сохранения. Старое +значение вне палитры сохраняется, пока пользователь сам его не заменит. +Некорректная строка отображается как текст, без выполнения HTML или CSS. + +`plugin.ts` — пустой серверный контракт. `ui/index.ts` регистрирует вклад +`color:picker`; editor, display, settings и чтение options находятся рядом +в отдельных небольших файлах. Импортов из Core или UI нет. + +Сборка: `pnpm --filter @asmblyr-collaborative/plugin-color build`. Пакет не включён по умолчанию; подключение описано в [руководстве примеров](https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/README.ru.md). После изменения состава пакетов перезапустите +Core и UI. При отключении плагина поле остаётся доступно как обычный текст. + +Настройка палитры не ограничивает HTTP API: формат и палитра являются UI. +Сервер применяет обычные права и ограничения текстового поля. + +Полный контракт: [Kit / Редакторы полей](kit-fields.md). diff --git a/docs/ru/reference/routes.md b/docs/ru/reference/routes.md new file mode 100644 index 0000000..bd7b623 --- /dev/null +++ b/docs/ru/reference/routes.md @@ -0,0 +1,234 @@ +# Матрица HTTP-маршрутов + +Сгенерировано из Core и проверенного каталога доступа. 226 деклараций. + +Это описание границ; их исполнение проверяют интеграционные тесты. Динамические маршруты плагинов и внутренние endpoints oidc-provider не перечисляются отдельно. + +| Метод и путь | Доступ | Источник в репозитории | +| --- | --- | --- | +| `GET /assistant/conversations` | Активный человек с доступом к чату; только собственные сессии | `apps/core/src/assistant/history-routes.ts` | +| `POST /assistant/conversations` | Активный человек с доступом к чату; создаёт собственную сессию с пустым контекстом | `apps/core/src/assistant/history-routes.ts` | +| `DELETE /assistant/conversations/:id` | Активный человек с доступом к чату; только собственная неактивная сессия | `apps/core/src/assistant/history-routes.ts` | +| `GET /assistant/conversations/:id` | Активный человек с доступом к чату; только собственная история, включая superuser | `apps/core/src/assistant/history-routes.ts` | +| `POST /assistant/filter/validate` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/messages` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/messages/:id/cancel` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | +| `POST /assistant/selection/validate` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | +| `GET /assistant/status` | Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно | `apps/core/src/assistant/routes.ts` | +| `POST /auth/browser/google/start` | Активный пользователь, точный Origin AUTH_UI_URL; proof в HttpOnly cookie. | `apps/core/src/auth/browser/google.ts` | +| `POST /auth/browser/invitations/claim` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/login` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/logout` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/passkeys/login` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/browser/passkeys/options` | Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию. | `apps/core/src/auth/browser/routes.ts` | +| `POST /auth/cli/authorize` | Активная человеческая сессия подтверждает одноразовый код PKCE S256 на 60 секунд для точного loopback callback. | `apps/core/src/auth/cli/routes.ts` | +| `GET /auth/cli/config` | Публичное обнаружение CLI; только настроенный URL согласия, без credentials. | `apps/core/src/auth/cli/routes.ts` | +| `POST /auth/cli/token` | Одноразовый код, PKCE verifier и точный callback. Только schema:read на 10 минут; действует лимит запросов. | `apps/core/src/auth/cli/routes.ts` | +| `POST /auth/federation-token` | Подписанный GitLab CI assertion и активная федерация | `apps/core/src/services/federation-routes.ts` | +| `POST /auth/invitations/accept` | Одноразовый invitation token и новый пароль | `apps/core/src/auth/routes.ts` | +| `POST /auth/invitations/claim` | Одноразовый invitation/recovery token; создаёт человеческую сессию. Recovery отзывает прежние сеансы и способы входа | `apps/core/src/auth/routes.ts` | +| `POST /auth/login` | Проверка email и пароля; ограничение частоты | `apps/core/src/auth/routes.ts` | +| `POST /auth/logout` | Refresh token в теле либо Bearer сессии | `apps/core/src/auth/routes.ts` | +| `GET /auth/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/routes.ts` | +| `POST /auth/passkeys/login` | Одноразовый challenge, подпись сохранённого passkey, точный origin/RP ID и user verification; активный пользователь | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /auth/passkeys/options` | Публичный WebAuthn challenge, пять минут; общий credential rate limit | `apps/core/src/auth/passkeys/routes.ts` | +| `GET /auth/providers` | Публичные ID и подписи провайдеров | `apps/core/src/auth/sso/routes.ts` | +| `POST /auth/refresh` | Действительный refresh token; ротация и защита от повторного использования | `apps/core/src/auth/routes.ts` | +| `POST /auth/service-token` | Действительный сервисный ключ | `apps/core/src/services/routes.ts` | +| `POST /auth/setup` | Setup secret и отсутствие созданных пользователей; ограничение частоты | `apps/core/src/auth/routes.ts` | +| `GET /auth/setup/status` | Публичный статус первой настройки | `apps/core/src/auth/routes.ts` | +| `POST /auth/sso/:provider/callback` | Проверка SSO flow, state/PKCE и ответа провайдера | `apps/core/src/auth/sso/routes.ts` | +| `POST /auth/sso/:provider/start` | Начало SSO; привязка требует действующей пользовательской сессии | `apps/core/src/auth/sso/routes.ts` | +| `GET /collections` | Активный principal; только доступные коллекции и поля | `apps/core/src/collections/routes.ts` | +| `POST /collections` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `DELETE /collections/:name` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | +| `PUT /collections/:name/display` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/fields` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `DELETE /collections/:name/fields/:field` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | +| `PATCH /collections/:name/fields/:field` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/fields/:field/configuration` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/fields/:field/configuration` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/fields/:field/impact` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | +| `PUT /collections/:name/fields/:field/presentation` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/fields/:field/search` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PATCH /collections/:name/folder` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/form` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/impact` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/lifecycle-routes.ts` | +| `DELETE /collections/:name/materialized-view` | Человек-superuser; отключает MV из каталога с проверкой зависимостей, удаляет метаданные/права; сохраняет PostgreSQL объект и данные | `apps/core/src/collections/materialized-routes.ts` | +| `PATCH /collections/:name/navigation` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `POST /collections/:name/relations` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PUT /collections/:name/relations/:field/search` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PATCH /collections/:name/settings` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `GET /collections/:name/terms` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | +| `PUT /collections/:name/terms` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | +| `DELETE /collections/:name/terms/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | +| `PUT /collections/:name/terms/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/terms/routes.ts` | +| `DELETE /connections/google` | Активная человеческая сессия; удаляет собственные токены, flows и предложения, пытается отозвать Google grant | `apps/core/src/connections/routes.ts` | +| `GET /connections/google` | Активная человеческая сессия; состояние только собственного подключения, без токенов | `apps/core/src/connections/routes.ts` | +| `GET /connections/google/callback` | Редирект результата; подключение требует активную сессию, одноразовые state и browser proof, совпадение владельца. | `apps/core/src/auth/browser/google.ts` | +| `POST /connections/google/callback` | Активная человеческая сессия; собственный flow, browser proof, state/nonce/PKCE и та же конфигурация клиента | `apps/core/src/connections/routes.ts` | +| `POST /connections/google/start` | Активная человеческая сессия; одноразовый OAuth flow владельца с browser proof и PKCE | `apps/core/src/connections/routes.ts` | +| `DELETE /connections/google/writes/:id` | Активная человеческая сессия; отмена собственного ожидающего предложения | `apps/core/src/connections/routes.ts` | +| `GET /connections/google/writes/:id` | Активная человеческая сессия; собственное неистекшее предложение и то же активное подключение | `apps/core/src/connections/routes.ts` | +| `POST /connections/google/writes/:id/confirm` | Активная человеческая сессия; однократное подтверждение собственного предложения, перепроверка подключения и исходных данных; не MCP-инструмент | `apps/core/src/connections/routes.ts` | +| `GET /extensions` | Активный principal; метаданные включённых UI-плагинов | `apps/core/src/plugins/ui-routes.ts` | +| `GET /extensions/:namespace/drafts/:id` | Активный principal; подготовленный черновик только своего principal и namespace, с TTL | `apps/core/src/plugins/action-draft-routes.ts` | +| `GET /files` | Человек + files/read либо update; superuser bypass. Общая библиотека команды | `apps/core/src/files/routes.ts` | +| `POST /files` | Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок | `apps/core/src/files/routes.ts` | +| `DELETE /files/:id` | Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок | `apps/core/src/files/routes.ts` | +| `GET /files/:id` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | +| `PATCH /files/:id` | Человек + files/update; superuser bypass. Изменение title/description и visibility private/public одного ready-файла; публикация и отзыв ссылки записываются в историю. | `apps/core/src/files/routes.ts` | +| `GET /files/:id/content` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | +| `GET /files/:id/events` | Человек + files/read либо update; superuser bypass. Общая библиотека команды | `apps/core/src/files/routes.ts` | +| `GET /files/resolve` | Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты | `apps/core/src/files/routes.ts` | +| `GET /filter-presets/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | +| `POST /filter-presets/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | +| `DELETE /filter-presets/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | +| `PUT /filter-presets/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/items/filter-preset-routes.ts` | +| `POST /folders` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `DELETE /folders/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PATCH /folders/:id` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `PATCH /folders/:id/order` | Человек: superuser; системная и plugin-owned структура дополнительно защищена | `apps/core/src/collections/routes.ts` | +| `GET /health` | Публичная проверка процесса | `apps/core/src/app.ts` | +| `GET /item-events/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `GET /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `PATCH /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `POST /items/:collection` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `DELETE /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `PATCH /items/:collection/:id` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id/related` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `GET /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | +| `PATCH /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | +| `POST /items/:collection/:id/relations/:field` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | +| `GET /items/:collection/:id/relations/:field/candidates` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-routes.ts` | +| `GET /items/:collection/:id/relations/:field/links/:linkId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | +| `PATCH /items/:collection/:id/relations/:field/links/:linkId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/:id/relations/:field/links/to/:targetId` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/:id/relations/:field/records` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/relation-link-routes.ts` | +| `POST /items/:collection/commit` | Активный principal; личные read/create/update и поля всех затронутых записей. expectedValues требует чтения обновляемых полей; конфликт откатывает весь commit | `apps/core/src/items/relation-routes.ts` | +| `GET /materialized-views` | Человек-superuser; обнаружение существующих materialized views в public без регистрации/REFRESH; Core/plugin объекты исключены | `apps/core/src/collections/materialized-routes.ts` | +| `POST /materialized-views` | Человек-superuser; атомарное подключение заполненного public MV с поддерживаемыми полями и устойчивым уникальным ключом. Только метаданные отображения/расположения, без SQL/DDL/REFRESH; права другим пользователям не выдаются | `apps/core/src/collections/materialized-routes.ts` | +| `GET /monitoring/browser` | Активная человеческая сессия; только публичный браузерный DSN и разрешённые флаги, без серверного DSN. Сервисные токены не допускаются | `apps/core/src/monitoring/routes.ts` | +| `GET /notifications` | Активная человеческая сессия; собственные уведомления включённых плагинов, доступ к каждой записи проверяется заново | `apps/core/src/notifications/routes.ts` | +| `POST /notifications/:id/read` | Активная человеческая сессия; собственное уведомление и текущий доступ к записи | `apps/core/src/notifications/routes.ts` | +| `POST /notifications/read-all` | Активная человеческая сессия; только собственные уведомления до границы снимка списка | `apps/core/src/notifications/routes.ts` | +| `GET /oauth-apps` | Человек: oauth/read ИЛИ update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `POST /oauth-apps` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `PUT /oauth-apps/:id` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `POST /oauth-apps/:id/secret` | Человек: oauth/update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `GET /oauth-apps/status` | Человек: oauth/read ИЛИ update; superuser bypass | `apps/core/src/oauth/routes.ts` | +| `GET /oauth-interactions/:uid` | Человек; UID, собственная сессия, правила приложения и согласие | `apps/core/src/oauth/interactions.ts` | +| `POST /oauth-interactions/:uid` | Человек; UID, собственная сессия, правила приложения и согласие | `apps/core/src/oauth/interactions.ts` | +| `ALL /oauth/*` | Протокольные endpoints oidc-provider: discovery/JWKS публичны, остальные используют проверки OAuth/OIDC | `apps/core/src/oauth/routes.ts` | +| `POST /oauth/complete/:uid` | Текущий пользователь, подписанные cookies взаимодействия OAuth и совпадающий Origin; идентификатор пользователя должен совпадать с сессией | `apps/core/src/oauth/interactions.ts` | +| `GET /permissions` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/permissions/routes.ts` | +| `POST /permissions` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | +| `DELETE /permissions/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | +| `GET /permissions/:id` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/permissions/routes.ts` | +| `PATCH /permissions/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/permissions/routes.ts` | +| `GET /permissions/me` | Активный principal; только собственные права | `apps/core/src/permissions/routes.ts` | +| `GET /policies` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/policies/routes.ts` | +| `POST /policies` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | +| `GET /policies/:id` | Человек: policies/read ИЛИ update; superuser bypass | `apps/core/src/policies/routes.ts` | +| `PATCH /policies/:id` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id/permissions/:permissionId` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/permissions/:permissionId` | Только человек-superuser: изменение состава политик и permissions | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/users` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; атомарная замена назначений без изменения себя и superuser | `apps/core/src/policies/routes.ts` | +| `DELETE /policies/:id/users/:userId` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser | `apps/core/src/policies/routes.ts` | +| `PUT /policies/:id/users/:userId` | Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser | `apps/core/src/policies/routes.ts` | +| `GET /policies/applications` | Человек: policies/read ИЛИ update; каталог приложений с управлением через политики, без секретов | `apps/core/src/policies/routes.ts` | +| `POST /presence` | Активная человеческая сессия; каждый heartbeat проверяет read коллекции/строки или доступ раздела. /files требует files/read либо update у человека, или superuser. Возвращает только ID, имя, аватар и число окон; без email/сессий/значений записи | `apps/core/src/presence/routes.ts` | +| `DELETE /presence/:clientId` | Активная человеческая сессия; удаляет только собственное окно этой сессии, без требования сохраняющегося read | `apps/core/src/presence/routes.ts` | +| `GET /public/files/:id/content` | Без авторизации: только явно опубликованный ready-файл (visibility=public). Приватный/удалённый файл — 404; no-store/CSP/nosniff. Без списка и метаданных. | `apps/core/src/files/public-routes.ts` | +| `GET /ready` | Публичная проверка готовности, без секретов | `apps/core/src/health/readiness.ts` | +| `DELETE /realtime/locks` | Активная человеческая сессия; освобождает только собственную lease с совпадающим clientId, даже после потери read/update | `apps/core/src/realtime/routes.ts` | +| `POST /realtime/locks` | Активная человеческая сессия; read и update указанного поля и строки. Атомарная 30-секундная lease, конфликт 409 FIELD_LOCKED | `apps/core/src/realtime/routes.ts` | +| `GET /realtime/stream` | Активная человеческая сессия через Bearer token или браузерную HttpOnly cookie; scope проверяется теми же collection/row read правилами, что GET /items. Поток повторно проверяет доступ и закрывается при его потере. Presence только для этой сессии | `apps/core/src/realtime/stream.ts` | +| `GET /schema` | Активный пользователь или сервисный аккаунт: доступные коллекции, wire-типы и разрешения на поля. Без записей, defaults и условий политик; private/no-store; accepts scoped CLI schema:read access and includes permission-gated generated plugin model contracts | `apps/core/src/schema/routes.ts` | +| `GET /search` | Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно | `apps/core/src/items/routes.ts` | +| `GET /service-accounts` | Человек: services/read ИЛИ update; superuser bypass | `apps/core/src/services/routes.ts` | +| `POST /service-accounts` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | +| `GET /service-accounts/:id` | Человек: services/read ИЛИ update; superuser bypass | `apps/core/src/services/routes.ts` | +| `PUT /service-accounts/:id` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | +| `POST /service-accounts/:id/federations` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/federation-routes.ts` | +| `DELETE /service-accounts/:id/federations/:federationId` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/federation-routes.ts` | +| `POST /service-accounts/:id/keys` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | +| `DELETE /service-accounts/:id/keys/:keyId` | Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации | `apps/core/src/services/routes.ts` | +| `GET /settings/access` | Активный человек; разделы, режим изменения и разрешённый набор назначаемых политик из актуальной БД | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/assistant` | Человек: assistant/read ИЛИ update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `PUT /settings/assistant` | Человек: assistant/update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `GET /settings/assistant/telemetry` | Человек: assistant/read ИЛИ update; superuser bypass | `apps/core/src/settings/routes.ts` | +| `GET /settings/integrations` | Только активная человеческая сессия superuser; безопасные параметры и наличие ключей, без значений секретов | `apps/core/src/integrations/routes.ts` | +| `PUT /settings/integrations/:section` | Только superuser; полный запрет изменения env-группы, revision, атомарное перешифрование при смене защиты | `apps/core/src/integrations/routes.ts` | +| `POST /settings/integrations/:section/test` | Только superuser; credential rate limit, проверка соединения без сохранения настроек | `apps/core/src/integrations/routes.ts` | +| `GET /settings/monitoring` | Только human superuser; локальные метрики текущего экземпляра Core, без DSN и пользовательских данных | `apps/core/src/monitoring/routes.ts` | +| `GET /settings/options/collections` | Человек: policies; только проекция схемы без записей; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/options/policies` | Человек: users ИЛИ policies ИЛИ services; только ID/названия; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/options/users` | Человек: oauth; только ID/email; read ИЛИ update соответствующего раздела | `apps/core/src/settings/access-routes.ts` | +| `GET /settings/plugins` | Человек: plugins/read ИЛИ update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `GET /settings/plugins/:namespace` | Человек: plugins/read ИЛИ update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `PUT /settings/plugins/:namespace` | Человек: plugins/update; superuser bypass | `apps/core/src/plugins/settings-routes.ts` | +| `GET /settings/terms` | Человек: terms/read ИЛИ update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `POST /settings/terms` | Человек: terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `PUT /settings/terms/:id` | Человек: terms/update; superuser bypass | `apps/core/src/terms/routes.ts` | +| `POST /sign/sso/:provider` | Точный Origin AUTH_UI_URL; настроенный провайдер; для link необходима активная сессия. | `apps/core/src/auth/browser/sso.ts` | +| `GET /sign/sso/:provider/callback` | Одноразовые state/PKCE и browser proof; для link проверяется сессия владельца. | `apps/core/src/auth/browser/sso.ts` | +| `GET /system-collections` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `DELETE /system-collections/:name/fields/:field` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `POST /system-collections/:name/fields/:field/configuration` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `PUT /system-collections/:name/fields/:field/configuration` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `GET /system-collections/:name/records` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `GET /system-collections/:name/records/:id` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `PATCH /system-collections/:name/records/:id` | Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены | `apps/core/src/system-collections/routes.ts` | +| `GET /table-views/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | +| `POST /table-views/:collection` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | +| `DELETE /table-views/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | +| `PUT /table-views/:collection/:id` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | +| `GET /table-views/:collection/default` | Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser | `apps/core/src/preferences/table-view-routes.ts` | +| `GET /translations` | Активный пользователь или сервисный аккаунт; схема ограничена доступными вызывающему коллекциями и полями, статические каталоги активных плагинов, без записей и defaults | `apps/core/src/translations/routes.ts` | +| `GET /users` | Человек: users/read или update ИЛИ policies/read или update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `POST /users` | Человек: users/update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/access` | Человек: users/read ИЛИ update; superuser bypass | `apps/core/src/auth/user-routes.ts` | +| `PUT /users/:id/delegation` | Только человек-superuser: явный набор готовых политик, которые пользователь вправе назначать | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/extension` | Активный пользователь с users/read и обычными read-правами коллекции | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/:id/extension` | Активный пользователь с users/update и обычными read/create/update-правами коллекции | `apps/core/src/auth/profile-extension-routes.ts` | +| `POST /users/:id/invitation` | Человек: superuser ИЛИ users/update; повторное приглашение только в пределах разрешённого набора, без доступа к аккаунтам с собственным делегированием | `apps/core/src/auth/user-routes.ts` | +| `GET /users/:id/profile` | Активный пользователь с users/read; публичные сведения профиля без credentials | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/:id/profile` | Активный пользователь с users/update; только поля профиля, без изменения статуса и привилегий | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/:id/profile-display` | Активный человек с users/read или superuser; настроенная проекция профиля целевого пользователя | `apps/core/src/auth/profile-display-routes.ts` | +| `POST /users/:id/recovery` | Свежий человек-superuser; только другой активный обычный пользователь. Одноразовая ссылка на 30 минут, аудит; не делегируется | `apps/core/src/auth/user-routes.ts` | +| `GET /users/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | +| `PATCH /users/me` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | +| `POST /users/me/avatar` | Собственная человеческая сессия; приватное растровое изображение до 2 MiB, настроенное файловое хранилище и credential rate limit | `apps/core/src/auth/profile-avatar-routes.ts` | +| `GET /users/me/extension` | Собственная человеческая сессия и обычные read-права коллекции; скрытые поля и строки не раскрываются | `apps/core/src/auth/profile-extension-routes.ts` | +| `PATCH /users/me/extension` | Собственная человеческая сессия; read и create/update, обычные правила строк, полей, связей и файлов | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/me/identities` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/sso/routes.ts` | +| `DELETE /users/me/identities/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/sso/routes.ts` | +| `GET /users/me/oauth-apps` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/oauth/consent-routes.ts` | +| `DELETE /users/me/oauth-apps/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/oauth/consent-routes.ts` | +| `GET /users/me/passkeys` | Собственная активная человеческая сессия; только публичные сведения о ключах | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/passkeys` | Собственный session-bound challenge, свежий вход и WebAuthn verification; до десяти ключей | `apps/core/src/auth/passkeys/routes.ts` | +| `DELETE /users/me/passkeys/:id` | Собственный ключ, свежий человеческий вход; нельзя удалить последний способ входа | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/passkeys/options` | Собственная активная человеческая сессия не старше пяти минут | `apps/core/src/auth/passkeys/routes.ts` | +| `POST /users/me/password` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | +| `POST /users/me/password/setup` | Собственная свежая человеческая сессия; только если пароля ещё нет | `apps/core/src/auth/profile-routes.ts` | +| `GET /users/me/preferences` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | +| `PATCH /users/me/preferences` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | +| `GET /users/me/profile-display` | Активный человек; только собственные значения заранее настроенных selfVisible путей, без права произвольного чтения коллекций | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/me/sessions` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | +| `DELETE /users/me/sessions/:id` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/auth/profile-routes.ts` | +| `GET /users/me/table-preferences/:collection` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | +| `PATCH /users/me/table-preferences/:collection` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/preferences/routes.ts` | +| `PUT /users/me/workspace` | Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость | `apps/core/src/workspaces/routes.ts` | +| `GET /users/profile-display` | Только human superuser; конфигурация блока профиля | `apps/core/src/auth/profile-display-routes.ts` | +| `PUT /users/profile-display` | Только human superuser; явно разрешает ограниченную проекцию выбранных полей управляющим пользователями и при selfVisible владельцу профиля | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/profile-display/sources` | Только human superuser; разрешённые пользовательские поля и пути M2O, без данных записей | `apps/core/src/auth/profile-display-routes.ts` | +| `GET /users/profile-extension` | Активный пользователь с users/read; имя выбранной коллекции и ключа | `apps/core/src/auth/profile-extension-routes.ts` | +| `PUT /users/profile-extension` | Только human superuser; связывает обычную UUID-коллекцию, не выдаёт права и не удаляет данные | `apps/core/src/auth/profile-extension-routes.ts` | +| `GET /users/references` | Человеческая сессия: superuser или чтение раздела users; только ID и подпись пользователя, ограниченные поиск и пагинация; сервисные ключи запрещены | `apps/core/src/auth/user-routes.ts` | +| `GET /workspaces` | Человек; только пространства с доступными коллекциями (superuser видит все) | `apps/core/src/workspaces/routes.ts` | +| `POST /workspaces` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | +| `DELETE /workspaces/:id` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | +| `PUT /workspaces/:id` | Человек: superuser | `apps/core/src/workspaces/routes.ts` | diff --git a/docs/ru/reference/sdk-guide.md b/docs/ru/reference/sdk-guide.md new file mode 100644 index 0000000..40d6d94 --- /dev/null +++ b/docs/ru/reference/sdk-guide.md @@ -0,0 +1,453 @@ + + +# @asmblyr-collaborative/sdk + +Типизированный HTTP-клиент Asmblyr для браузера и Node.js 22+. +Каждая операция вызывает основной API. В пакете нет H3, Knex, PostgreSQL, +компилятора TypeScript или загрузчика плагинов во время исполнения. +Для разработки расширений используется отдельный `@asmblyr-collaborative/kit`. + + + +## Профиль пользователя + +Собственный профиль доступен с пользовательским access token. Статус, права, +пароли и системные даты нельзя менять через профиль. Все новые поля необязательны. + +```ts +await client.users.updateMe({ + firstName: "Ivan", + lastName: "Example", + description: "About me", +}); +await client.users.updatePreferences({ timezone: "Asia/Yekaterinburg" }); +const { data: me } = await client.users.me(); +``` + +Дополнительные поля потребитель настраивает в обычной UUID-коллекции и выбирает +её в настройках пользователей. `asm connect` / `asm generate` включает её поля +в схему проекта. Для клиента с этой схемой: + +```ts +const { data: profile } = await client.users.extension("user_profiles"); +await client.users.saveExtension("user_profiles", { bio: "Hello" }); +``` + +`saveExtension` создаёт запись с ID текущего пользователя или обновляет её. +Для первого сохранения нужны `create` и `read`, затем `update` и `read`. +Действуют обычные правила полей и строк, права на связи и файлы. Передайте только +поля сгенерированной схемы; имя коллекции должно совпадать с выбранным расширением. +Скрытые поля могут отсутствовать в результате, а `data` — быть `null`. +После первого сохранения запись и её связи доступны через обычный `client.items` +и fluent API; произвольное создание профиля через `items.create` запрещено. + + + +## Подключение клиента + +Личные уведомления доступны через `client.notifications.list()`, `.read(id)` и +`.readAll(result.readBefore)`. Они требуют человеческой сессии; сервисные ключи +не дают доступ к личным входящим. Core хранит последние 200 событий пользователя. + +Предварительная версия устанавливается с тегом `beta`: + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +``` + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; + +// В браузере: сессия админки остаётся в HttpOnly cookies. +const client = createClient({ baseUrl: "/api" }); + +const records = await client.items.list("articles", { + fields: ["id", "title"], + limit: 20, + page: 1, + sort: "title", + direction: "asc", +}); +const record = await client.items.get("articles", 4); +const profile = await client.users.me(); +``` + +`baseUrl` — корень API, например `http://localhost:3001` для прямого доступа +к Core или `http://localhost:3000/api` для браузерной админки. Префикс `/api` +автоматически не добавляется. Путь `/api` без origin работает в браузере. + +Для прямых запросов к Core передайте access token: + +```ts +const client = createClient({ + baseUrl: "https://asmblyr.example.com/api", + accessToken: () => currentAccessToken, + timeoutMs: 10_000, +}); +``` + +`accessToken` может быть строкой или асинхронной функцией. Функция вызывается для +каждого запроса. Поддерживаются access tokens пользователя и сервисного аккаунта; +права проверяет Core. `/users/me` предназначен для пользователя. +SDK не хранит токены в браузере, не выполняет вход и не обновляет токены сам. +В админке браузерная сессия хранится в HttpOnly cookie, которую проверяет Core. + + + +## Реализованные методы + +| Метод | HTTP | Ответ | +| ------------------------------------------------ | -------------------------------- | -------------------------- | +| `items.list(collection, options?, request?)` | `GET /items/:collection` | `{ data, labels, page }` | +| `items.get(collection, id, options?, request?)` | `GET /items/:collection/:id` | `{ data, label }` | +| `users.me(request?)` | `GET /users/me` | `{ data: CurrentUser }` | +| `items.create(collection, values, request?)` | `POST /items/:collection` | `{ data: row \| null }` | +| `items.update(collection, id, values, request?)` | `PATCH /items/:collection/:id` | `{ data: row \| null }` | +| `items.delete(collection, id, request?)` | `DELETE /items/:collection/:id` | `void` (204) | +| `items.commit(collection, draft, request?)` | `POST /items/:collection/commit` | `{ data: { id: string } }` | + +Остальные системные ресурсы и массовые операции пока не включены. +Прямого доступа к базе данных и переключения actor/superuser нет. + +`list` поддерживает `fields`, `page`, `limit`, `sort`, `direction`, `q`, `filter`. +SDK сериализует фильтр и query, Core проверяет значения, права и ограничения. +Фильтр — группа `{ logic: "and" | "or", children: [...] }`, значения условий — +строки или массивы строк. Поиск и фильтры по связям используют правила основного API. +Для списка предел страницы — 100 записей. `page.total` остаётся строкой. +Core дополнительно применяет условия permissions; SDK не передаёт доверенный +контекст. В одной странице условно разрешённые поля могут отсутствовать у части +записей. См. [права и ограничения](../features/access.md). + +Без `fields` выдаются доступные поля. `fields: []` отправляется как `fields=` +и возвращает только первичный ключ. Ключ всегда добавляет Core. Явно выбранное +закрытое поле приводит к 403. Названия коллекций и полей — технические. +Подписи `label`/`labels` могут использовать другие разрешённые поля. + +Большие целые ID передавайте строками. SDK отклоняет небезопасные JS-числа. +Строковые ключи кодируются как один сегмент URL; ключи `.` и `..` не поддерживаются, +поскольку HTTP-клиенты нормализуют такие сегменты. Даты HTTP-ответов — ISO-строки, +значения `bigserial` — строки. SDK не преобразует их в `Date` или `number`. + + + +## Запись и сохранение связей + +```ts +const created = await client.items.create("articles", { title: "Статья" }); +await client.items.update("articles", 4, { title: "Новое название" }); +await client.items.delete("articles", 4); + +// Одна транзакция для основной записи и связанных изменений. +await client.items.commit("articles", { + id: "4", + values: { title: "Статья с категорией" }, + references: { category_id: { values: { title: "Новая категория" } } }, +}); +``` + +Core применяет права create/update/delete, ограничения полей, проверки связей, +валидацию и историю изменений. `create`/`update` возвращают только читаемые поля; +`{ data: null }` означает успешную запись без права чтения, а не ошибку. +`delete` разрешается на уровне записи, без списка удаляемых полей. + +Каждый вызов — отдельная транзакция. Последовательность `create` и `update` +не становится одной транзакцией. Для черновика со связями используется `commit`: +ошибка откатывает все его изменения и историю. Существующие записи в черновике +требуют права чтения; обычный `update` допускает право записи без чтения. + +`ItemCommitDraft` описывает `references`, `relations` (attach/detach/create/links) +и `records` — изменения существующих записей. Core ограничивает черновик +100 изменениями и глубиной 5. `commit` возвращает только ID корневой записи, +в том числе при создании без права чтения. UI-поля preview/label/key не передаются. + +Для защиты от перезаписи передайте исходные значения изменяемых полей: + +```ts +const previous = await client.items.get("articles", "4"); +await client.items.commit("articles", { + id: "4", + values: { title: "Моя правка" }, + expectedValues: { title: previous.data.title }, +}); +``` + +При несовпадении Core возвращает `ApiError` со `status: 409` и +`code: "ITEM_CHANGED"`; весь commit откатывается. Сохраните локальный черновик, +прочитайте актуальные данные и предложите пользователю выбрать значение. +Автоматически повторять запрос с новым исходным значением нельзя: это заменит +чужую правку. Изменения разных полей не конфликтуют; уже применённое такое же +значение считается успешным. Для вложенных записей задайте их собственные +`expectedValues`, включая обновляемые FK. Проверка требует read/update каждого +сравниваемого поля. Пропуск параметра и обычный `items.update` сохраняют +прежнее безусловное поведение. JSON и массивы сравниваются целиком. + + + +## Участники страницы + +```ts +const clientId = crypto.randomUUID(); // одно открытое окно +const scope = { kind: "record", collection: "articles", id: "4" } as const; +const { data } = await client.presence.touch({ clientId, scope }); +// data.participants: ID, имя, аватар, число окон, self; data.total +await client.presence.leave(clientId); +``` + +`touch` продлевает отметку на 30 секунд и возвращает до 50 пользователей; +текущий пользователь идёт первым, окна одного человека объединяются. Повторяйте +вызов, пока окно открыто: этот HTTP API требует самостоятельного таймера. +Realtime API ниже обновляет присутствие сам. SDK сам не создаёт +таймеры. `leave` удаляет только окно текущей человеческой сессии и безопасен при +повторении. До 32 активных окон на одну сессию; превышение возвращает 429. + +Область — разрешённая `page`, `collection` либо `record`; каждый `touch` заново +проверяет права просмотра. Пользовательские поля, email, токены и ID сессий не +возвращаются. Сервисные principal не участвуют. При недоступной сети индикатор +может отставать до истечения отметки; присутствие не блокирует запись и не +показывает редактируемое поле. + +## Collaborative Live + +```ts +const live = client.realtime.connect(); +const unsubscribe = live.subscribe( + { kind: "record", collection: "articles", id: "4" }, + (event) => { + if (event.type === "record.updated") { + // Перечитайте запись через items.get; payload не содержит значения полей. + void client.items.get(event.payload.collection, event.payload.recordId); + } + }, +); +const offState = live.onState((state) => console.log(state)); +const lock = { + collection: "articles", + recordId: "4", + field: "title", + clientId: crypto.randomUUID(), +}; +await live.locks.acquire(lock); +// Пока редактор открыт: await live.locks.refresh(lock) не реже 30 секунд. +await live.locks.release(lock); +unsubscribe(); +offState(); +live.close(); +``` + +`subscribe` работает для page, collection и record. Состояния — `connecting`, +`connected`, `reconnecting`, `offline`. SDK повторяет подписку с backoff и +jitter. После переподключения он отправляет `collection.changed`, чтобы клиент +перечитал данные. Повторные события с тем же ID в потоке пропускаются. Через +админку используется HttpOnly cookie; внешнему API-клиенту нужен пользовательский +Bearer token. Сервисный ключ для realtime не подходит. Блокировки — временный +UX-сигнал, а `items.commit` с `expectedValues` остаётся защитой записи. + + + +## Типизация коллекций + +```ts +interface Schema { + articles: { + id: number; + title: string; + created_at: string | null; + }; +} + +const client = createClient({ baseUrl: "/api" }); +const result = await client.items.list("articles", { + fields: ["title"], + sort: "id", +}); +// result.data[0].title: string | undefined +``` + +TypeScript проверяет имя коллекции, выбор и сортировку полей; возвращаемая +проекция учитывает `fields`. Поля конкретной схемы остаются опциональными: +их наличие зависит от прав. Неявно добавленный первичный ключ не выводится +из имени `id`: у коллекции может быть другой ключ. Чтобы получить его в типе +проекции, перечислите его в `fields`. + +Без `Schema` работает динамический режим со словарём JSON-значений. +В своей схеме задавайте `date` как `string` (`YYYY-MM-DD`), обычный `bigint` +как `string` с точным целым значением, а `integer` с вариантами — как `number` +или числовой union. SDK передаёт эти значения без преобразования в `Date`/`Number`. +Связанные пути в фильтрах пока проверяются Core во время запроса. +Схему можно описать вручную или получить из Core через CLI `asm connect`: +подробнее — [генерация типов и fluent-запросы](#генерируемые-типы-fluent-запросы-и-методы-плагинов). +SDK не валидирует структуру ответов во время исполнения; текущие права и +ограничения данных проверяет Core. + +При вручную описанной схеме данные `create`/`update` и корневые `values` в `commit` типизируются как +`Partial`: проверяются имена, типы и nullable из вашей схемы. +Обязательность при создании, значения по умолчанию, неизменяемые ключи и системные +поля проверяет Core по метаданным. Тип строки сам по себе их не описывает. +Сгенерированная схема отдельно описывает чтение, создание и обновление, включая +обязательные поля и доступные действия. Вложенные черновики пока используют +JSON-контракт без вывода типов связанных коллекций. + +`CurrentUser`, контракты записей, страниц, фильтров и ошибок переэкспортируются +из `@asmblyr-collaborative/contracts`. Core использует этот же пакет. Строки БД, хеши паролей +и внутренние модели Core не являются публичными типами SDK. + + + +## Ошибки и отмена + +```ts +import { ApiError } from "@asmblyr-collaborative/sdk"; + +try { + await client.items.get("articles", 4, undefined, { + signal: controller.signal, + }); +} catch (error) { + if (error instanceof ApiError) { + console.error( + error.status, + error.code, + error.requestId, + error.message, + error.details, + ); + } +} +``` + +HTTP-ошибка становится `ApiError`. Ошибки сети и отмены остаются исходными +ошибками `fetch`; локально некорректный путь или ID — `TypeError`. +Таймаут по умолчанию — 10 секунд, включая чтение тела ответа; `timeoutMs: 0` +отключает его. `request.signal` позволяет отменить конкретный запрос. +`request.timeoutMs` переопределяет таймаут одного вызова; редактор админки передаёт +`0` для commit, не ограничивая длительность запроса таймаутом SDK. +SDK не повторяет запросы автоматически и отклоняет HTTP-редиректы. +Ответы запрашиваются с `cache: "no-store"`. + +Отмена или ошибка соединения не подтверждает откат уже отправленной записи: +сервер мог успеть сохранить данные. Перед повтором проверьте результат. + +Можно передать `fetch`, `headers` и `credentials` для собственного HTTP-окружения. +По умолчанию `credentials: "same-origin"`; SDK не настраивает CORS сервера. +На сервере создавайте клиент для конкретного пользователя/запроса, чтобы не +разделять его токен между пользователями. + + + +## Сборка и проверки + +Проверка установки архивов вне workspace: `pnpm packages:check`. +После выпуска `pnpm packages:check --registry` проверяет установку той же версии +из npm без локальных алиасов зависимостей. + +```sh +pnpm --filter @asmblyr-collaborative/sdk build +pnpm --filter @asmblyr-collaborative/sdk typecheck +pnpm --filter @asmblyr-collaborative/sdk test +node scripts/test.mjs core-sdk +``` + +Интеграционный набор запускает Core на временном HTTP-порту с отдельной тестовой +БД и сравнивает SDK с вызовом через kit, включая ограничения прав. + + + +## Переводы + +```ts +const { data } = await client.translations.get("en"); +console.log(data.core["appearance.ocean"]); +console.log(data.schema.articles?.fields.title?.label); +``` + +`GET /translations?locale=ru|en` (в админке `/api/translations`) требует +активной сессии или сервисного ключа. Ответ версии 1 содержит язык, +`fallbackLocale: "ru"`, плоский каталог `core`, каталоги активных `plugins` +по namespace и подписи доступной `schema`. Схема использует права каталога +коллекций, включая поля создания/изменения; это не разрешение читать записи. +Ответ не содержит записей, значений по умолчанию или настроек плагинов. +Пропуск языка выбирает русский; неподдержанный язык возвращает 400. + + + +## Генерируемые типы, fluent-запросы и методы плагинов + +[CLI](cli-guide.md) подключается через согласие в админке или API-токен +из окружения/stdin. Credentials не сохраняются. + +```sh +pnpm exec asm connect --url https://asmblyr.example.test +pnpm exec asm schema pull +pnpm exec asm generate +pnpm exec asm schema check --offline +``` + +GET /schema (также client.schema.pull()) экспортирует доступные wire-типы, +фактические права полей, виды фильтров и сгенерированные model-контракты плагинов. +Defaults, записи, секреты и условия политик исключены. Согласие браузера выдаёт +временный доступ только к схеме; рабочим запросам нужны отдельные credentials/сессия. + +Файл содержит обычные TS-типы и небольшое описание для браузера. Особый компилятор +или плагин сборщика не требуется. Node-only генератор изолирован в +@asmblyr-collaborative/sdk/schema. + +JSON с interface:tags экспортируется как свободный string[] (либо string[] | null), +без закрытого enum. После смены редактора выполните asm schema pull и asm generate. + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; +import { schema } from "./asmblyr.schema.js"; + +const client = createClient({ baseUrl: "/api", schema }); +const articles = await client.Articles.select((a) => [a.id, a.title]) + .where((a) => a.status.eq("published").and(a.price.gte("1000.00"))) + .orderBy((a) => a.created_at.desc()) + .limit(20) + .exec(); +// Аналогичный вход с подсказками имени коллекции: +const result = await client.collection("articles").limit(10).result(); +await client.items.create("articles", { title: "Hello" }); +// Если model handler калькулятора установлен и доступен: +const calculation = await client.plugins.calculator.calculate(input); +``` + +Пример предполагает наличие указанных полей. Псевдонимы выводятся из технических +имён независимо от переведённых подписей. Конструкторы запросов неизменяемы. +Where объединяется через AND; предикаты поддерживают and/or. Exec возвращает строки, +result — прежний ItemListResult. OrderBy поддерживает одно поле, как Core API. + +Search("корм") выбирает релевантность, если ранее не выбрана явная сортировка. +OrderBy включает порядок поля; orderByRelevance() возвращает релевантность и +сохраняет поле для равных совпадений. Низкоуровневый list принимает +{ q: "корм", order: "relevance" }; page.order показывает фактический режим. +Core ранжирует до пагинации по читаемым поисковым полям/приоритету: +точное совпадение, начало целого слова, целое слово, подстрока. Без q сортирует по полю. + +Limit принимает 1–100, также поддержаны page/search. Подсказки операторов зависят +от возможностей поля. JSON/multiselect не дают скалярных предикатов. Старые снимки +без filterKind предоставляют только консервативные equality/list-операторы. +Decimal/bigint остаются строками. + +Read/Create/Update разделяют обязательные create-поля, optional update, +управляемые поля и union вариантов. Ответ содержит выбранные ключи, но значения +остаются optional из-за прав. Core проверяет текущие права/row conditions при вызове. +Ручные схемы строк и items-клиент остаются совместимыми. + +Методы плагинов используют Kit JSON-схемы и исходные HTTP-маршруты без новой +регистрации. AccessGate фильтрует экспорт, выполнение перепроверяет доступ. +Старые действия без output-схемы пропускаются. Ограниченное JSON-подмножество +поддерживает закрытые объекты, массивы, литералы, union и примитивы; +произвольные refs, открытые объекты и defaults запрещены. Личное подключение +проверяется при вызове даже при наличии статического контракта. + +Materialized view имеет sourceKind:materialized-view и только read-действия. +Create/Update генерируются как never, delete недоступен. Fluent, items.list/get +работают. Core запрещает все записи, включая bulk/nested commit суперпользователя. +SQL-создание и refresh выполняются внешними средствами. + +Asm schema check проверяет хеши и изменения удалённого контракта. --offline +сверяет типы с сохранённой схемой; asm generate пересоздаёт их из снимка. +Имена, варианты и аннотации могут раскрывать структуру проекта: коммитьте их +только при допустимости. Npm-публикация — отдельное действие. +[Подготовка пакетов](packages.md). diff --git a/docs/ru/security/access-matrix.md b/docs/ru/security/access-matrix.md new file mode 100644 index 0000000..6b1fa19 --- /dev/null +++ b/docs/ru/security/access-matrix.md @@ -0,0 +1,42 @@ + + +# Что открыто и чем защищено + +«Публичный endpoint» означает отсутствие Core Bearer, но не отсутствие проверки учётных данных в его протоколе. + +| Поверхность | Допуск | Дополнительная граница | +| ------------------------------------------- | --------------------------------- | --------------------------------------------------------------- | +| health/ready, setup status, provider labels | Анонимный | Без паролей/ключей/данных коллекций | +| setup/login/invite/refresh | Без Bearer | Setup secret/пароль/одноразовый токен, rate limit | +| SSO start/callback | Протокол провайдера | Flow, state, PKCE; связывание из собственной сессии | +| service/federation exchange | Ключ/JWT assertion | Подпись, claims, срок, replay protection | +| OAuth discovery/JWKS | Публичный | Остальные OAuth endpoints имеют protocol checks | +| Collections/items/search/history | Активный user/service | Grants действия/полей; проекция истории и связей | +| DDL, folders, формы/метаданные схемы | superuser | Reserved prefixes, ownership, impact checks | +| Профиль, sessions, consent | Человек | Только собственный ресурс | +| Участники страницы/карточки | Человек + доступ просмотра | Свежий read строки/раздела; leave только своей сессии | +| Просмотр настроек | Человек + section read/update | Свежие политики из БД; без выдачи секретов | +| Изменение настроек | Человек + section update | Политики и сервисы имеют отдельные ограничения ниже | +| Состав политик и permissions | Человек-superuser | Менеджер может просматривать готовые права | +| Назначения политик пользователям | Человек + policies/update | Явный личный набор; без своих назначений и superuser | +| Настройка разрешённого набора | Человек-superuser | Отдельная настройка пользователя, не permission | +| Сервисы, их ключи и федерации | Человек + services/update | Все текущие и новые политики входят в личный набор | +| Повторные приглашения | Человек + users/update | Политики в наборе; без собственных/делегирующих аккаунтов | +| File read/resolve | Активный principal | Readable reference, files/read/update у человека либо superuser | +| File management | Человек + files/update; superuser | Размер, тип выдачи/preview, metadata validation | +| Workspaces | Человек | Видимые коллекции; изменения superuser | +| Presets/views | Человек + collection access | Владение; shared изменения superuser | +| Assistant | Человек + data eligibility | Caller permissions, MCP enable, tool validation | +| Plugin HTTP | Активный principal и handler gate | Capabilities Kit + caller/domain authorization | + +Superuser не ограничен набором. Сервисные токены не управляют настройками. Отзыв набора запрещает будущие операции, но автоматически не отзывает уже выданные ключи и назначения. Подробности — [права и политики](../features/access.md#ограниченное-назначение-политик). + +Полный перечень деклараций — [матрица маршрутов](../reference/routes.md). + + + +## Что не обеспечивает изоляцию + +Workspace, скрытие меню/коллекции, default-фильтр опубликованных, metadata readonly в UI, описание инструмента для модели. Доступ обеспечивают проверки сервера. Knex и обычные SQL-запросы не получают права человека автоматически. + +Доверенные server plugins и администратор БД могут выйти за прикладные проверки. В проекте нет песочницы для произвольного кода плагина и нет общей PostgreSQL RLS-модели. diff --git a/docs/ru/security/overview.md b/docs/ru/security/overview.md new file mode 100644 index 0000000..d596c30 --- /dev/null +++ b/docs/ru/security/overview.md @@ -0,0 +1,51 @@ + + +# Границы безопасности + +Asmblyr проверяет доступ на сервере. Скрытие коллекции в меню, представление, +рабочее пространство и параметры редактора поля не заменяют права на данные. +Одна установка обслуживает одну команду; разделения компаний внутри установки нет. + + + +## Авторизация + +Права задаются политиками: действия над коллекциями, разрешённые поля и условия +для строк. Для настроек используются отдельные grants просмотра и изменения. +Superuser управляет структурой данных и составом политик. Назначение готовых +политик можно делегировать только в пределах явно заданного набора. + +Сервисные токены проверяют права данных и не управляют настройками команды. +SSO-идентичность связывается явно по provider, issuer и subject; совпадение email +не связывает аккаунты автоматически. Приглашения и восстановление используют +одноразовые ссылки. Passkey требует корректного origin и свежей сессии для управления. + +Требования допуска перечислены в [матрице доступа](access-matrix.md), +подробные правила — в [правах](../features/access.md) и [авторизации](../features/identity.md). + + + +## Файлы, расширения и ассистент + +Файловая библиотека общая для команды. Чтение конкретной ссылки может быть разрешено +через доступную запись; отдельных ACL для каждого файла нет. Bucket обслуживает +оператор, а содержимое выдаёт Core после проверки прав. + +Расширения исполняются как доверенный код. Kit ограничивает выданные capabilities +и доступ к данным вызывающего, но не изолирует произвольный Node.js-код. +Ассистент использует права текущего пользователя и настройки публикации коллекций +в MCP; модель не получает сырой SQL или привилегированное хранилище плагина. + + + +## Эксплуатация + +Используйте HTTPS и приватные PostgreSQL/S3, храните конфигурацию вне Git, +ограничивайте доступ к signing keys и резервным копиям. Миграции и восстановление +проверяйте на отдельной копии. Производственная установка требует собственной +проверки TLS, IAM, нагрузки и доставки уведомлений. + +Автоматические тесты проверяют отдельные сценарии и отказ в доступе; их успешное +выполнение не является сертификацией безопасности. Инструкции — в +[эксплуатации](../development/operations.md). Порядок сообщения об уязвимости +описан в [SECURITY.md](https://github.com/Asmblyr/Collaborative/blob/main/SECURITY.ru.md). diff --git a/docs/security/access-matrix.md b/docs/security/access-matrix.md index aeb61aa..4986ac5 100644 --- a/docs/security/access-matrix.md +++ b/docs/security/access-matrix.md @@ -1,38 +1,49 @@ -# Что открыто и чем защищено - -«Публичный endpoint» означает отсутствие Core Bearer, но не отсутствие проверки учётных данных в его протоколе. - -| Поверхность | Допуск | Дополнительная граница | -| ------------------------------------------- | --------------------------------- | --------------------------------------------------------------- | -| health/ready, setup status, provider labels | Анонимный | Без паролей/ключей/данных коллекций | -| setup/login/invite/refresh | Без Bearer | Setup secret/пароль/одноразовый токен, rate limit | -| SSO start/callback | Протокол провайдера | Flow, state, PKCE; связывание из собственной сессии | -| service/federation exchange | Ключ/JWT assertion | Подпись, claims, срок, replay protection | -| OAuth discovery/JWKS | Публичный | Остальные OAuth endpoints имеют protocol checks | -| Collections/items/search/history | Активный user/service | Grants действия/полей; проекция истории и связей | -| DDL, folders, формы/метаданные схемы | superuser | Reserved prefixes, ownership, impact checks | -| Профиль, sessions, consent | Человек | Только собственный ресурс | -| Участники страницы/карточки | Человек + доступ просмотра | Свежий read строки/раздела; leave только своей сессии | -| Просмотр настроек | Человек + section read/update | Свежие политики из БД; без выдачи секретов | -| Изменение настроек | Человек + section update | Политики и сервисы имеют отдельные ограничения ниже | -| Состав политик и permissions | Человек-superuser | Менеджер может просматривать готовые права | -| Назначения политик пользователям | Человек + policies/update | Явный личный набор; без своих назначений и superuser | -| Настройка разрешённого набора | Человек-superuser | Отдельная настройка пользователя, не permission | -| Сервисы, их ключи и федерации | Человек + services/update | Все текущие и новые политики входят в личный набор | -| Повторные приглашения | Человек + users/update | Политики в наборе; без собственных/делегирующих аккаунтов | -| File read/resolve | Активный principal | Readable reference, files/read/update у человека либо superuser | -| File management | Человек + files/update; superuser | Размер, тип выдачи/preview, metadata validation | -| Workspaces | Человек | Видимые коллекции; изменения superuser | -| Presets/views | Человек + collection access | Владение; shared изменения superuser | -| Assistant | Человек + data eligibility | Caller permissions, MCP enable, tool validation | -| Plugin HTTP | Активный principal и handler gate | Capabilities Kit + caller/domain authorization | - -Superuser не ограничен набором. Сервисные токены не управляют настройками. Отзыв набора запрещает будущие операции, но автоматически не отзывает уже выданные ключи и назначения. Подробности — [права и политики](../features/access.md#ограниченное-назначение-политик). - -Полный перечень деклараций — [матрица маршрутов](../reference/routes.md). - -## Что не обеспечивает изоляцию - -Workspace, скрытие меню/коллекции, default-фильтр опубликованных, metadata readonly в UI, описание инструмента для модели. Доступ обеспечивают проверки сервера. Knex и обычные SQL-запросы не получают права человека автоматически. - -Доверенные server plugins и администратор БД могут выйти за прикладные проверки. В проекте нет песочницы для произвольного кода плагина и нет общей PostgreSQL RLS-модели. + + +# Public surfaces and access controls + +A public endpoint does not require a Core Bearer token. Its protocol may still +require credentials and other verification. + +| Surface | Admission | Additional boundary | +| ------------------------------------------- | ------------------------------------- | -------------------------------------------------------------- | +| health/ready, setup status, provider labels | Anonymous | No passwords, keys, or collection data | +| setup/login/invite/refresh | No Bearer token | Setup secret/password/single-use token, rate limits | +| SSO start/callback | Provider protocol | Flow, state, PKCE; linking from the user's own session | +| Service/federation exchange | Key/JWT assertion | Signature, claims, expiry, replay protection | +| OAuth discovery/JWKS | Public | Other OAuth endpoints perform protocol checks | +| Collections/items/search/history | Active user/service | Action/field grants; history and relation projection | +| DDL, folders, forms/schema metadata | Superuser | Reserved prefixes, ownership, impact checks | +| Profile, sessions, consent | Human | Own resource only | +| Page/record participants | Human with read access | Fresh row/section read check; leave only the caller's session | +| View settings | Human with section read/update | Fresh database policies; no secrets returned | +| Change settings | Human with section update | Policies and services have the additional limits below | +| Policy contents and permissions | Human superuser | Managers can inspect existing permissions | +| User policy assignments | Human with policies/update | Explicit personal set; no own assignments or superuser changes | +| Configure an assignable set | Human superuser | Separate user setting, not a permission | +| Services, their keys, and federations | Human with services/update | All current and new policies must be in the personal set | +| Repeat invitations | Human with users/update | Policies in the set; no own or delegating accounts | +| File read/resolve | Active principal | Readable reference, human files/read/update, or superuser | +| File management | Human with files/update, or superuser | Size, delivery/preview type, metadata validation | +| Workspaces | Human | Visible collections; changes require superuser | +| Presets/views | Human with collection access | Ownership; shared changes require superuser | +| Assistant | Human with data eligibility | Caller permissions, MCP enablement, tool validation | +| Plugin HTTP | Active principal and handler gate | Kit capabilities plus caller/domain authorization | + +Superusers are not limited by an assignable set. Service tokens cannot manage +settings. Revoking the set blocks future operations but does not automatically +revoke keys or assignments already issued. See [permissions and policies](../features/access.md). + +The complete declarations are in the [route matrix](../reference/routes.md). + + + +## What does not provide isolation + +Workspaces, hidden menus/collections, a default published-record filter, UI +readonly metadata, and a tool's description for the model are not access controls. +Server checks enforce access. Knex and ordinary SQL queries do not automatically +inherit a human user's permissions. + +Trusted server plugins and database administrators can bypass application checks. +There is no sandbox for arbitrary plugin code or general PostgreSQL RLS model. diff --git a/docs/security/overview.md b/docs/security/overview.md index ca715b6..f585c39 100644 --- a/docs/security/overview.md +++ b/docs/security/overview.md @@ -1,43 +1,50 @@ -# Границы безопасности + -Asmblyr проверяет доступ на сервере. Скрытие коллекции в меню, представление, -рабочее пространство и параметры редактора поля не заменяют права на данные. -Одна установка обслуживает одну команду; разделения компаний внутри установки нет. +# Security boundaries -## Авторизация +Collaborative enforces access on the server. Hiding a collection in a menu, +a view, a workspace, or field-editor metadata does not replace data permissions. +One installation serves one team; there is no company isolation within an installation. -Права задаются политиками: действия над коллекциями, разрешённые поля и условия -для строк. Для настроек используются отдельные grants просмотра и изменения. -Superuser управляет структурой данных и составом политик. Назначение готовых -политик можно делегировать только в пределах явно заданного набора. + -Сервисные токены проверяют права данных и не управляют настройками команды. -SSO-идентичность связывается явно по provider, issuer и subject; совпадение email -не связывает аккаунты автоматически. Приглашения и восстановление используют -одноразовые ссылки. Passkey требует корректного origin и свежей сессии для управления. +## Authorization -Требования допуска перечислены в [матрице доступа](./access-matrix.md), -подробные правила — в [правах](../features/access.md) и [авторизации](../features/identity.md). +Policies define collection actions, allowed fields, and row conditions. +Settings have separate read and update grants. A superuser manages the data +schema and policy contents. Assignment of existing policies can be delegated +only within an explicitly configured set. -## Файлы, расширения и ассистент +Service tokens are subject to data permissions and cannot manage team settings. +SSO identities are linked explicitly by provider, issuer, and subject; matching +email addresses never link accounts automatically. Invitations and recovery use +single-use links. Passkeys require the correct origin and a recent session for management. -Файловая библиотека общая для команды. Чтение конкретной ссылки может быть разрешено -через доступную запись; отдельных ACL для каждого файла нет. Bucket обслуживает -оператор, а содержимое выдаёт Core после проверки прав. +Requirements are listed in the [access matrix](./access-matrix.md). Detailed rules +are in [permissions](../features/access.md) and [identity](../features/identity.md). -Расширения исполняются как доверенный код. Kit ограничивает выданные capabilities -и доступ к данным вызывающего, но не изолирует произвольный Node.js-код. -Ассистент использует права текущего пользователя и настройки публикации коллекций -в MCP; модель не получает сырой SQL или привилегированное хранилище плагина. + -## Эксплуатация +## Files, plugins, and the assistant -Используйте HTTPS и приватные PostgreSQL/S3, храните конфигурацию вне Git, -ограничивайте доступ к signing keys и резервным копиям. Миграции и восстановление -проверяйте на отдельной копии. Производственная установка требует собственной -проверки TLS, IAM, нагрузки и доставки уведомлений. +The file library is shared by the team. Reading a particular reference may be +authorized through an accessible record; there are no separate per-file ACLs. +The operator manages the bucket, and Core serves content after checking access. -Автоматические тесты проверяют отдельные сценарии и отказ в доступе; их успешное -выполнение не является сертификацией безопасности. Инструкции — в -[эксплуатации](../development/operations.md). Порядок сообщения об уязвимости -описан в [SECURITY.md](https://github.com/Asmblyr/Collaborative/blob/main/SECURITY.md). +Plugins run as trusted code. Kit restricts granted capabilities and access to +the caller's data, but does not sandbox arbitrary Node.js code. The assistant +uses the current user's permissions and collection MCP settings; the model +does not receive raw SQL access or a plugin's privileged storage. + + + +## Operations + +Use HTTPS and private PostgreSQL/S3, keep configuration out of Git, and limit +access to signing keys and backups. Test migrations and recovery on a separate +copy. Production installations need their own TLS, IAM, load, and notification-delivery checks. + +Automated tests cover specific scenarios and access-denial cases; passing them +is not a security certification. See [operations](../development/operations.md). +Vulnerability reporting is described in +[SECURITY.md](https://github.com/Asmblyr/Collaborative/blob/main/SECURITY.md). diff --git a/examples/gitlab-federation/README.md b/examples/gitlab-federation/README.md index 8f19e4a..98a902d 100644 --- a/examples/gitlab-federation/README.md +++ b/examples/gitlab-federation/README.md @@ -1,24 +1,26 @@ -# Asmblyr federation test +# GitLab federation check -Private smoke test for GitLab CI workload identity. + -- Project ID: 87017556 -- Protected branch: main -- Issuer: https://gitlab.com -- Local service account: GitLab federation test -- No data policies assigned; a successful check currently sees zero collections. +[English](README.md) · [Русский](README.ru.md) -## Run + + +A GitLab CI workload-identity example. Configure your own federation with issuer +https://gitlab.com, a protected branch, and exact project/job-project identifiers. +Values in .gitlab-ci.yml must match your installation. A service without policies +can authenticate but cannot access collections. -1. Set the CI/CD variable ASMBLYR_CORE_URL to the HTTPS address of your Core, reachable by the runner. -2. Open Pipelines, run a pipeline on main, then start the manual federation-smoke job. -3. A local runner may use http://127.0.0.1:3001 if it actually runs in the same network namespace as Core. - A GitLab-hosted runner cannot reach your computer's localhost. +## Run -The audience and federation ID are non-secret identifiers for the local Asmblyr instance. -If you recreate the federation or use another instance, replace both values in .gitlab-ci.yml. -The job obtains a fresh GitLab ID token; it does not need an Asmblyr service key. -Do not enable CI_DEBUG_TRACE. No token is printed or uploaded as an artifact. -An assertion is exchanged once, with no retries. Restart the job to obtain a new assertion. +1. Set ASMBLYR_CORE_URL in CI/CD variables to an HTTPS Core address reachable by the runner. +2. Run a pipeline on protected main and start the manual federation-smoke job. +3. A local runner can use http://127.0.0.1:3001 only in Core's network namespace. + A hosted runner cannot reach your computer's localhost. -The end-to-end job has not been run yet: runner-to-Core connectivity must be configured. +Audience/federation IDs are nonsecret identifiers. Replace them in .gitlab-ci.yml +for another installation. The job obtains a fresh GitLab ID token without a +permanent Collaborative service key. Do not enable CI_DEBUG_TRACE. Tokens are +neither printed nor uploaded. Assertions are exchanged once without retries; +restart the job for a fresh assertion. End-to-end verification requires working +runner-to-Core connectivity. diff --git a/examples/gitlab-federation/README.ru.md b/examples/gitlab-federation/README.ru.md new file mode 100644 index 0000000..81c73be --- /dev/null +++ b/examples/gitlab-federation/README.ru.md @@ -0,0 +1,25 @@ +# Проверка федерации GitLab + + + +[English](README.md) · [Русский](README.ru.md) + + + +Пример проверки workload identity из GitLab CI. Настройте собственную федерацию: +issuer https://gitlab.com, защищённую ветку и точные идентификаторы проекта/job project. +Параметры в .gitlab-ci.yml должны соответствовать вашей установке. +Сервис без политик успешно входит, но не получает доступа к коллекциям. + +## Запуск + +1. Задайте CI/CD-переменную ASMBLYR_CORE_URL с HTTPS-адресом Core, доступным runner. +2. Запустите pipeline защищённой ветки main и ручную задачу federation-smoke. +3. Локальный runner может использовать http://127.0.0.1:3001 только в том же сетевом + пространстве, что Core. Облачный runner не видит localhost вашего компьютера. + +Audience и federation ID — несекретные идентификаторы. При другой установке замените +их в .gitlab-ci.yml. Задача получает свежий GitLab ID token без постоянного сервисного +ключа Collaborative. Не включайте CI_DEBUG_TRACE; токены не выводятся и не сохраняются +в артефакты. Assertion обменивается один раз без повтора; перезапустите задачу для +нового assertion. Сквозная проверка требует настроенной связи runner → Core. diff --git a/examples/oauth-lavinmq/README.md b/examples/oauth-lavinmq/README.md index 6834d3f..3e7910a 100644 --- a/examples/oauth-lavinmq/README.md +++ b/examples/oauth-lavinmq/README.md @@ -1,38 +1,31 @@ -# LavinMQ и OAuth/OIDC +# LavinMQ and OAuth/OIDC -Опциональный пример подключения внешнего приложения к OAuth/OIDC provider Asmblyr. -Он не запускается вместе с Core и не требуется для админки. + -Нужны Docker Compose, настроенный OAuth provider Core, постоянные signing keys -и зарегистрированное приложение. Настройки протокола описаны в -[интеграциях](../../docs/features/integrations.md). +[English](README.md) · [Русский](README.ru.md) -Из корня репозитория: + + +An optional example connecting an external application to Collaborative's OAuth/OIDC provider. It neither starts with Core nor is required by the admin. + +Provide Docker Compose, configured Core OAuth, persistent signing keys, and a registered application. See [integrations](../../docs/features/integrations.md). + +From the repository root: ```sh node examples/oauth-lavinmq/setup.mjs ``` -Команда создаст `.local-data/oauth-lavinmq/lavinmq.ini`, не заменяя существующий файл. -Заполните его параметрами зарегистрированного приложения, затем запустите: +This creates .local-data/oauth-lavinmq/lavinmq.ini without replacing an existing file. Enter the registered application's settings, then start: ```sh docker compose -f examples/oauth-lavinmq/compose.yaml up -d ``` -Gateway доступен на `http://localhost:15673`; настройки и данные брокера приватны. +The gateway is at `http://localhost:15673`. Broker settings/data stay private. -Для персональных прав включите в OAuth-приложении управление через политики. -Добавьте разрешения `lavinmq.tag:monitoring` и `lavinmq.tag:administrator` -с понятными названиями, затем назначьте их разным политикам. В конфигурации брокера -оставьте `mgmt_scopes = openid profile email`, а `audience` и `resource_server_id` -укажите равными Audience приложения (например, `lavinmq`). Права приходят из -`resource_access.lavinmq.roles`; перечислять их в общих `mgmt_scopes` не нужно. -`preferred_username_claims = email,sub` показывает почту вместо UUID. +For personal permissions, enable policy-managed application access. Add lavinmq.tag:monitoring and lavinmq.tag:administrator with readable labels and assign them to different policies. Keep mgmt_scopes = openid profile email. Audience and resource_server_id must match the application's Audience, for example lavinmq. Rights arrive in resource_access.lavinmq.roles, not shared mgmt_scopes. Preferred_username_claims = email,sub displays email instead of UUID. -Разрешение HTTP для localhost относится к этому примеру. Публичная установка -требует HTTPS и корректных issuer, origins и redirect URI. +Localhost HTTP is only for this example. Public installations require HTTPS and correct issuer, origins, and redirect URI. -Live-тест `core-lavinmq` запускается отдельно через `scripts/test.mjs` с переменными, -описанными в `apps/core/test/oauth-lavinmq.integration.test.ts`. Обычный набор тестов -пропускает его без настроенного брокера. +Run the optional core-lavinmq live test through scripts/test.mjs using variables described in apps/core/test/oauth-lavinmq.integration.test.ts. Ordinary suites skip it without a configured broker. diff --git a/examples/oauth-lavinmq/README.ru.md b/examples/oauth-lavinmq/README.ru.md new file mode 100644 index 0000000..18dda90 --- /dev/null +++ b/examples/oauth-lavinmq/README.ru.md @@ -0,0 +1,44 @@ +# LavinMQ и OAuth/OIDC + + + +[English](README.md) · [Русский](README.ru.md) + + + +Опциональный пример подключения внешнего приложения к OAuth/OIDC provider Asmblyr. +Он не запускается вместе с Core и не требуется для админки. + +Нужны Docker Compose, настроенный OAuth provider Core, постоянные signing keys +и зарегистрированное приложение. Настройки протокола описаны в +[интеграциях](../../docs/ru/features/integrations.md). + +Из корня репозитория: + +```sh +node examples/oauth-lavinmq/setup.mjs +``` + +Команда создаст `.local-data/oauth-lavinmq/lavinmq.ini`, не заменяя существующий файл. +Заполните его параметрами зарегистрированного приложения, затем запустите: + +```sh +docker compose -f examples/oauth-lavinmq/compose.yaml up -d +``` + +Gateway доступен на `http://localhost:15673`; настройки и данные брокера приватны. + +Для персональных прав включите в OAuth-приложении управление через политики. +Добавьте разрешения `lavinmq.tag:monitoring` и `lavinmq.tag:administrator` +с понятными названиями, затем назначьте их разным политикам. В конфигурации брокера +оставьте `mgmt_scopes = openid profile email`, а `audience` и `resource_server_id` +укажите равными Audience приложения (например, `lavinmq`). Права приходят из +`resource_access.lavinmq.roles`; перечислять их в общих `mgmt_scopes` не нужно. +`preferred_username_claims = email,sub` показывает почту вместо UUID. + +Разрешение HTTP для localhost относится к этому примеру. Публичная установка +требует HTTPS и корректных issuer, origins и redirect URI. + +Live-тест `core-lavinmq` запускается отдельно через `scripts/test.mjs` с переменными, +описанными в `apps/core/test/oauth-lavinmq.integration.test.ts`. Обычный набор тестов +пропускает его без настроенного брокера. diff --git a/examples/plugins/README.md b/examples/plugins/README.md index ef750f4..27de905 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -1,35 +1,32 @@ -# Примеры расширений +# Plugin examples -Три небольших пакета демонстрируют Kit: + -| Пакет | Пример | -| ---------------------------------- | ------------------------------------------------------- | -| [calculator](calculator/README.md) | Типизированный model handler, форма и вызов ассистентом | -| [color](color/README.md) | Редактор и отображение поля с настройками | -| [overview](overview/README.md) | Страница админки и endpoint текущего участника | +[English](README.md) · [Русский](README.ru.md) -Они входят в workspace для разработки и тестов, но **не включены по умолчанию**. -Рабочие комментарии находятся отдельно в `packages/plugin-comments`. -`package.json` в этом каталоге — конфигурация примеров для тестов, не конфигурация -обычного приложения. `pnpm build` собирает приложение без демонстрационных пакетов. + -## Подключить пример локально +Three small packages demonstrate Kit: -Из корня выполните `pnpm build:examples`. Затем добавьте имя нужного пакета в -`asmblyr.plugins` корневого `package.json`. Для overview также одобрите -`identity.profile` в `asmblyr.pluginPermissions`; calculator и color не требуют -capabilities. Сохраните разрешения уже включённых пакетов. +| Package | Example | +| ---------------------------------- | ----------------------------------------- | +| [calculator](calculator/README.md) | Typed model handler, form, assistant call | +| [color](color/README.md) | Field editor/display with settings | +| [overview](overview/README.md) | Admin page and current-caller endpoint | -Перезапустите Core и UI. Чтобы отключить пример, удалите его из `asmblyr.plugins` -и пересоберите UI. Поля color останутся доступными как обычный текст. -Локальные изменения конфигурации примеров не включайте в pull request. +They belong to the development/test workspace but are disabled by default. The bundled Comments plugin is under packages/plugin-comments. This directory's package.json is test configuration, not ordinary app configuration. Pnpm build builds the app without demonstration packages. -## Проверить контракты +## Enable an example locally + +Run pnpm build:examples from the root, then add the desired package to root asmblyr.plugins. Overview also needs identity.profile approval in asmblyr.pluginPermissions; Calculator/Color need no capabilities. Preserve existing package approvals. + +Restart Core/UI. To disable, remove the example from asmblyr.plugins and rebuild UI. Color fields remain ordinary text. Do not include local example configuration changes in pull requests. + +## Check contracts ```sh pnpm build:examples node scripts/test.mjs core-plugins ``` -Тестовая конфигурация явно включает примеры. Source и production loaders -используют один manifest; сгенерированные schemas и `dist` не хранятся в Git. +Test configuration explicitly enables examples. Source/production loaders share one manifest. Generated schemas and dist stay out of Git. diff --git a/examples/plugins/README.ru.md b/examples/plugins/README.ru.md new file mode 100644 index 0000000..0f4ecc4 --- /dev/null +++ b/examples/plugins/README.ru.md @@ -0,0 +1,41 @@ +# Примеры расширений + + + +[English](README.md) · [Русский](README.ru.md) + + + +Три небольших пакета демонстрируют Kit: + +| Пакет | Пример | +| ------------------------------------- | ------------------------------------------------------- | +| [calculator](calculator/README.ru.md) | Типизированный model handler, форма и вызов ассистентом | +| [color](color/README.ru.md) | Редактор и отображение поля с настройками | +| [overview](overview/README.ru.md) | Страница админки и endpoint текущего участника | + +Они входят в workspace для разработки и тестов, но **не включены по умолчанию**. +Рабочие комментарии находятся отдельно в `packages/plugin-comments`. +`package.json` в этом каталоге — конфигурация примеров для тестов, не конфигурация +обычного приложения. `pnpm build` собирает приложение без демонстрационных пакетов. + +## Подключить пример локально + +Из корня выполните `pnpm build:examples`. Затем добавьте имя нужного пакета в +`asmblyr.plugins` корневого `package.json`. Для overview также одобрите +`identity.profile` в `asmblyr.pluginPermissions`; calculator и color не требуют +capabilities. Сохраните разрешения уже включённых пакетов. + +Перезапустите Core и UI. Чтобы отключить пример, удалите его из `asmblyr.plugins` +и пересоберите UI. Поля color останутся доступными как обычный текст. +Локальные изменения конфигурации примеров не включайте в pull request. + +## Проверить контракты + +```sh +pnpm build:examples +node scripts/test.mjs core-plugins +``` + +Тестовая конфигурация явно включает примеры. Source и production loaders +используют один manifest; сгенерированные schemas и `dist` не хранятся в Git. diff --git a/examples/plugins/calculator/README.md b/examples/plugins/calculator/README.md index f22508d..90b1d59 100644 --- a/examples/plugins/calculator/README.md +++ b/examples/plugins/calculator/README.md @@ -1,32 +1,27 @@ # Calculator -Пример одного действия для формы, внутреннего MCP и ассистента. -Пример не включён по умолчанию; [подключите его](../README.md) перед запуском. -Страница: `http://localhost:3000/extensions/calculator/home`. + -- `shared/calculation.ts` — TypeScript-типы входа/выхода, подписи и ограничения в JSDoc. -- `server/api/calculator/calculate.post.ts` — h3-обработчик, расчёт в целых копейках. - Путь и метод определяются файлом. `defineModelContext` и - `defineModelAnnotation` публикуют действие для MCP. `readOnly: true` запрещает запись. -- `plugin.ts` — пустой `definePlugin({})`, без регистрации обработчиков. -- `ui/pages/calculator-page.tsx` — форма shadcn и `useAction`. +[English](README.md) · [Русский](README.ru.md) -HTTP: `POST /calculator/calculate`, из UI — `/api/calculator/calculate`. -MCP и HTTP вызывают один handler; обёртка `defineModelContext` явно включает инструмент. -Сборщик создаёт `.asmblyr/models/calculator/calculate.post.json`: UI и Core используют -его для проверки входа и результата. Генерируемые файлы в Git не включаются. + -Сборка/запуск из корня: `pnpm dev`. Core требует локальную PostgreSQL для сессий; -у самого калькулятора таблиц нет. После подключения нового пакета перезапустите UI. +One action shared by a form, internal MCP, and the assistant. [Enable the example](../README.md) first; it is disabled by default. Page: http://localhost:3000/extensions/calculator/home. + +- Shared/calculation.ts defines input/output types, labels, and JSDoc constraints. +- Server/api/calculator/calculate.post.ts is an H3 handler using integer minor-unit arithmetic. File routing determines URL/method. DefineModelContext and defineModelAnnotation explicitly expose it to MCP. ReadOnly:true blocks writes. +- `plugin.ts` is definePlugin({}), without handler registration. +- Ui/pages/calculator-page.tsx uses shadcn and useAction. + +HTTP: POST /calculator/calculate, or /api/calculator/calculate from UI. HTTP/MCP share the handler. The builder creates .asmblyr/models/calculator/calculate.post.json for Core/UI input/output validation. Generated files stay out of Git. + +Run pnpm dev at the root. Core needs local PostgreSQL for sessions; Calculator has no tables. Restart UI after enabling the package. ```text -Рассчитай стоимость для 150 пользователей на 12 месяцев по 990 рублей -за пользователя в месяц со скидкой 10%. Открой калькулятор. +Calculate the price for 150 users over 12 months at 990 rubles per user +per month with a 10% discount. Open the calculator. ``` -Результат: 1 603 800 ₽. Ассистент подготовит приватную форму на 20 минут. -Для перехода нажмите «Открыть страницу» в его ответе. -Она исчезает после перезапуска Core; новые ручные значения можно пересчитать. -Запрос должен содержать все параметры — цены и скидки ассистент не выдумывает. +Result: 1,603,800 ₽. The assistant prepares a private form for twenty minutes. Click Open page to navigate; manual values can then be recalculated. PostgreSQL snapshots survive Core restarts until expiry. Supply every parameter: the assistant must not invent prices or discounts. -Контракт, доступ и ограничения: [действия плагинов](../../../docs/development/plugin-actions.md). +See [action contracts and limits](../../../docs/development/plugin-actions.md). diff --git a/examples/plugins/calculator/README.ru.md b/examples/plugins/calculator/README.ru.md new file mode 100644 index 0000000..1b21b08 --- /dev/null +++ b/examples/plugins/calculator/README.ru.md @@ -0,0 +1,39 @@ +# Calculator + + + +[English](README.md) · [Русский](README.ru.md) + + + +Пример одного действия для формы, внутреннего MCP и ассистента. +Пример не включён по умолчанию; [подключите его](../README.ru.md) перед запуском. +Страница: `http://localhost:3000/extensions/calculator/home`. + +- `shared/calculation.ts` — TypeScript-типы входа/выхода, подписи и ограничения в JSDoc. +- `server/api/calculator/calculate.post.ts` — h3-обработчик, расчёт в целых копейках. + Путь и метод определяются файлом. `defineModelContext` и + `defineModelAnnotation` публикуют действие для MCP. `readOnly: true` запрещает запись. +- `plugin.ts` — пустой `definePlugin({})`, без регистрации обработчиков. +- `ui/pages/calculator-page.tsx` — форма shadcn и `useAction`. + +HTTP: `POST /calculator/calculate`, из UI — `/api/calculator/calculate`. +MCP и HTTP вызывают один handler; обёртка `defineModelContext` явно включает инструмент. +Сборщик создаёт `.asmblyr/models/calculator/calculate.post.json`: UI и Core используют +его для проверки входа и результата. Генерируемые файлы в Git не включаются. + +Сборка/запуск из корня: `pnpm dev`. Core требует локальную PostgreSQL для сессий; +у самого калькулятора таблиц нет. После подключения нового пакета перезапустите UI. + +```text +Рассчитай стоимость для 150 пользователей на 12 месяцев по 990 рублей +за пользователя в месяц со скидкой 10%. Открой калькулятор. +``` + +Результат: 1 603 800 ₽. Ассистент подготовит приватную форму на 20 минут. +Для перехода нажмите «Открыть страницу» в его ответе. +Подготовленная форма хранится в PostgreSQL, доступна репликам Core и сохраняется +после перезапуска до истечения срока; новые ручные значения можно пересчитать. +Запрос должен содержать все параметры — цены и скидки ассистент не выдумывает. + +Контракт, доступ и ограничения: [действия плагинов](../../../docs/ru/development/plugin-actions.md). diff --git a/examples/plugins/color/README.md b/examples/plugins/color/README.md index 6cdd4be..2b8ff71 100644 --- a/examples/plugins/color/README.md +++ b/examples/plugins/color/README.md @@ -1,25 +1,26 @@ -# Цвет + -Пример расширения редактора обычного текстового поля. +# Color -1. Откройте настройки текстового поля → **Отображение**. -2. В списке **Редактор** выберите **Цвет · расширение**. -3. Настройте палитру и возможность вводить произвольный цвет. -4. Сохраните поле. В карточке появится палитра, в таблице — цвет и HEX. + -Каждое поле хранит свою палитру. Поддержаны 1–24 цвета в формате `#RRGGBB`, -очистка значения, обязательность и блокировка на время сохранения. Старое -значение вне палитры сохраняется, пока пользователь сам его не заменит. -Некорректная строка отображается как текст, без выполнения HTML или CSS. +[English](README.md) · [Русский](README.ru.md) -`plugin.ts` — пустой серверный контракт. `ui/index.ts` регистрирует вклад -`color:picker`; editor, display, settings и чтение options находятся рядом -в отдельных небольших файлах. Импортов из Core или UI нет. + -Сборка: `pnpm --filter @asmblyr-collaborative/plugin-color build`. Пакет не включён по умолчанию; подключение описано в [руководстве примеров](../README.md). После изменения состава пакетов перезапустите -Core и UI. При отключении плагина поле остаётся доступно как обычный текст. +An example editor extension for ordinary text fields. -Настройка палитры не ограничивает HTTP API: формат и палитра являются UI. -Сервер применяет обычные права и ограничения текстового поля. +1. Open a text field's Presentation settings. +2. Select Color · extension in Editor. +3. Configure its palette and whether custom colors are allowed. +4. Save. Record cards show the palette; tables show color and HEX. -Полный контракт: [Kit / Редакторы полей](../../../packages/kit/FIELDS.md). +Each field stores its own palette of 1–24 #RRGGBB colors. Clearing, required validation, and disabling during save are supported. Existing values outside the palette remain until explicitly replaced. Invalid strings display as text without executing HTML/CSS. + +`plugin.ts` is the empty server contract. Ui/index.ts registers color:picker; editor, display, settings, and option parsing live in separate small files. There are no Core/UI imports. + +Build: pnpm --filter @asmblyr-collaborative/plugin-color build. It is disabled by default; see [example setup](../README.md). Restart Core/UI after changing packages. Disabling falls back to ordinary text. + +Palette settings constrain UI only, not HTTP API. The server applies normal text-field permissions/constraints. + +Full contract: [Kit field editors](../../../packages/kit/FIELDS.md). diff --git a/examples/plugins/color/README.ru.md b/examples/plugins/color/README.ru.md new file mode 100644 index 0000000..e114102 --- /dev/null +++ b/examples/plugins/color/README.ru.md @@ -0,0 +1,33 @@ + + +# Цвет + + + +[English](README.md) · [Русский](README.ru.md) + + + +Пример расширения редактора обычного текстового поля. + +1. Откройте настройки текстового поля → **Отображение**. +2. В списке **Редактор** выберите **Цвет · расширение**. +3. Настройте палитру и возможность вводить произвольный цвет. +4. Сохраните поле. В карточке появится палитра, в таблице — цвет и HEX. + +Каждое поле хранит свою палитру. Поддержаны 1–24 цвета в формате `#RRGGBB`, +очистка значения, обязательность и блокировка на время сохранения. Старое +значение вне палитры сохраняется, пока пользователь сам его не заменит. +Некорректная строка отображается как текст, без выполнения HTML или CSS. + +`plugin.ts` — пустой серверный контракт. `ui/index.ts` регистрирует вклад +`color:picker`; editor, display, settings и чтение options находятся рядом +в отдельных небольших файлах. Импортов из Core или UI нет. + +Сборка: `pnpm --filter @asmblyr-collaborative/plugin-color build`. Пакет не включён по умолчанию; подключение описано в [руководстве примеров](../README.ru.md). После изменения состава пакетов перезапустите +Core и UI. При отключении плагина поле остаётся доступно как обычный текст. + +Настройка палитры не ограничивает HTTP API: формат и палитра являются UI. +Сервер применяет обычные права и ограничения текстового поля. + +Полный контракт: [Kit / Редакторы полей](../../../packages/kit/FIELDS.ru.md). diff --git a/examples/plugins/overview/README.md b/examples/plugins/overview/README.md index 0f7dded..063c6e1 100644 --- a/examples/plugins/overview/README.md +++ b/examples/plugins/overview/README.md @@ -1,59 +1,34 @@ # Overview -Второй пример плагина: отдельная страница внутри админки и собственный HTTP endpoint. -Открывается через **Приложения → Обзор** по адресу -`http://localhost:3000/extensions/overview/home`. - -## Где писать код - -- `ui/pages/overview-page.tsx` — содержимое страницы; начните с этого файла. -- `ui/index.ts` — регистрация: `id`, название в меню и компонент страницы. -- `ui/hooks/use-overview.ts` — загрузка, отмена запроса, ошибка и повтор. -- `server/api/overview/session.get.ts` — пример серверного обработчика с текущим actor. -- `shared/overview.ts` — общий тип ответа. -- `plugin.ts` — обязательный серверный entry. - -UI получает `PluginPageProps` из `@asmblyr-collaborative/kit/ui`. Его `request("/session")` -обращается к `/api/overview/session` через сессию пользователя. Прямой адрес Core: -`GET http://localhost:3001/overview/session` (нужен bearer token). -Токены и серверные модули в компонент не передаются. - -Общий shadcn `Button` импортируется из `@asmblyr-collaborative/kit/ui/button`. -Доступные компоненты и правила их использования описаны в -[Kit UI](../../../packages/kit/UI.md). - -Пример показывает только собственный ID, имя, вид actor и время ответа. -Он не читает коллекции, не создаёт таблицы и не сохраняет настройки. -Доступ к записям добавляйте через `useAsmblyr(event).items` с правами вызывающего. - -## Разработка - -Пакет не включён при обычном запуске. Для подключения следуйте [руководству примеров](../README.md). -Из корня репозитория: - -```sh -pnpm dev -``` - -Обычный Core требует запущенную PostgreSQL (`pnpm db:up`). -Правки TSX подхватывает Next; API подхватывает watcher Core. -При добавлении нового пакета перезапустите оба dev-сервера. -Для production нужны `pnpm build` и перезапуск сервисов. - -Название страницы меняется в `ui/index.ts`. В том же `pages` можно зарегистрировать -ещё один компонент с другим ID — меню и маршрут появятся автоматически. -Имена состоят из строчных латинских букв, цифр и дефисов, начинаются с буквы. -Адрес всегда `/extensions//`; встроенные страницы не перехватываются. - -## Границы первого этапа - -Страница доступна вошедшим пользователям, допущенным текущим layout в админку. -Отдельных прав на страницы расширений пока нет. Для пользователя без минимального -доступа сохраняется существующий экран «Нет доступа». Endpoint проверяет сессию -независимо от меню и возвращает только контекст самого вызывающего. - -Пока нет вложенных маршрутов, параметров маршрута и подключения UI без пересборки. -Страницы могут сообщать host о несохранённых изменениях через `onStateChange`: -это защищает от замены формы ассистентом и предупреждает перед перезагрузкой. -Защиты всех обычных переходов через меню пока нет. -Текущий пример только читает данные и не содержит несохранённых форм. + + +[English](README.md) · [Русский](README.ru.md) + + + +An admin page and its own HTTP endpoint. Open Applications → Overview at http://localhost:3000/extensions/overview/home. + +## Code locations + +- Ui/pages/overview-page.tsx: page content; start here. +- Ui/index.ts: page ID, menu title, component registration. +- Ui/hooks/use-overview.ts: load, cancellation, errors, retry. +- Server/api/overview/session.get.ts: current-actor endpoint. +- Shared/overview.ts: response type. +- `plugin.ts`: required server entry. + +PluginPageProps from `kit/ui` supplies request("/session"), calling /api/overview/session with the user's session. Direct Core: GET http://localhost:3001/overview/session with a Bearer token. Tokens/server modules never reach the component. + +Shared Button comes from `kit/ui`/button. See [Kit UI](../../../packages/kit/UI.md). The example displays only the caller's ID, name, actor kind, and response time. It reads no collections, creates no tables, and saves no settings. Add record operations through useAsmblyr(event).items under caller permissions. + +## Development + +The package is disabled by default; follow [example setup](../README.md), then run pnpm dev. Start PostgreSQL with pnpm db:up. Next handles TSX changes; Core's watcher handles API changes. New packages require restarting both dev servers. Production needs pnpm build and service restart. + +Change the title in ui/index.ts. Register another pages entry with a different ID to add its menu/route automatically. IDs use lowercase Latin letters, digits, and hyphens, beginning with a letter. Routes always use /extensions/<namespace>/<id> and cannot override built-ins. + +## Current limits + +Pages require authenticated users admitted by the existing admin layout; users without minimum access retain the no-access screen. Separate plugin-page grants are unsupported. The endpoint independently validates the session and returns only its caller's context. + +Nested/dynamic routes and UI loading without rebuilding are unsupported. OnStateChange reports unsaved state to protect against assistant form replacement and warn on reload. A universal guard for normal menu navigation is not yet implemented. This read-only example has no unsaved forms. diff --git a/examples/plugins/overview/README.ru.md b/examples/plugins/overview/README.ru.md new file mode 100644 index 0000000..7944ed5 --- /dev/null +++ b/examples/plugins/overview/README.ru.md @@ -0,0 +1,65 @@ +# Overview + + + +[English](README.md) · [Русский](README.ru.md) + + + +Второй пример плагина: отдельная страница внутри админки и собственный HTTP endpoint. +Открывается через **Приложения → Обзор** по адресу +`http://localhost:3000/extensions/overview/home`. + +## Где писать код + +- `ui/pages/overview-page.tsx` — содержимое страницы; начните с этого файла. +- `ui/index.ts` — регистрация: `id`, название в меню и компонент страницы. +- `ui/hooks/use-overview.ts` — загрузка, отмена запроса, ошибка и повтор. +- `server/api/overview/session.get.ts` — пример серверного обработчика с текущим actor. +- `shared/overview.ts` — общий тип ответа. +- `plugin.ts` — обязательный серверный entry. + +UI получает `PluginPageProps` из `@asmblyr-collaborative/kit/ui`. Его `request("/session")` +обращается к `/api/overview/session` через сессию пользователя. Прямой адрес Core: +`GET http://localhost:3001/overview/session` (нужен bearer token). +Токены и серверные модули в компонент не передаются. + +Общий shadcn `Button` импортируется из `@asmblyr-collaborative/kit/ui/button`. +Доступные компоненты и правила их использования описаны в +[Kit UI](../../../packages/kit/UI.ru.md). + +Пример показывает только собственный ID, имя, вид actor и время ответа. +Он не читает коллекции, не создаёт таблицы и не сохраняет настройки. +Доступ к записям добавляйте через `useAsmblyr(event).items` с правами вызывающего. + +## Разработка + +Пакет не включён при обычном запуске. Для подключения следуйте [руководству примеров](../README.ru.md). +Из корня репозитория: + +```sh +pnpm dev +``` + +Обычный Core требует запущенную PostgreSQL (`pnpm db:up`). +Правки TSX подхватывает Next; API подхватывает watcher Core. +При добавлении нового пакета перезапустите оба dev-сервера. +Для production нужны `pnpm build` и перезапуск сервисов. + +Название страницы меняется в `ui/index.ts`. В том же `pages` можно зарегистрировать +ещё один компонент с другим ID — меню и маршрут появятся автоматически. +Имена состоят из строчных латинских букв, цифр и дефисов, начинаются с буквы. +Адрес всегда `/extensions//`; встроенные страницы не перехватываются. + +## Границы первого этапа + +Страница доступна вошедшим пользователям, допущенным текущим layout в админку. +Отдельных прав на страницы расширений пока нет. Для пользователя без минимального +доступа сохраняется существующий экран «Нет доступа». Endpoint проверяет сессию +независимо от меню и возвращает только контекст самого вызывающего. + +Пока нет вложенных маршрутов, параметров маршрута и подключения UI без пересборки. +Страницы могут сообщать host о несохранённых изменениях через `onStateChange`: +это защищает от замены формы ассистентом и предупреждает перед перезагрузкой. +Защиты всех обычных переходов через меню пока нет. +Текущий пример только читает данные и не содержит несохранённых форм. diff --git a/package.json b/package.json index f9fb381..295d8be 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "docs:generate": "node scripts/docs/generate.mjs", - "docs:check": "node scripts/docs/generate.mjs --check", + "docs:check": "node --test scripts/docs/localization.test.mjs && node scripts/docs/generate.mjs --check", "docs:dev": "pnpm docs:generate && vitepress dev docs --host 127.0.0.1", "docs:build": "pnpm docs:generate && pnpm docs:check && vitepress build docs", "docs:preview": "vitepress preview docs --host 127.0.0.1", diff --git a/packages/cli/README.md b/packages/cli/README.md index c114fb8..470cdf2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,5 +1,11 @@ # @asmblyr-collaborative/cli + + +[English](README.md) · [Русский](README.ru.md) + + + Connect a TypeScript project to an Asmblyr installation and generate collection and plugin types from the access available to the signed-in user. Node.js 22+. Install the preview CLI alongside the SDK: diff --git a/packages/cli/README.ru.md b/packages/cli/README.ru.md new file mode 100644 index 0000000..e420587 --- /dev/null +++ b/packages/cli/README.ru.md @@ -0,0 +1,83 @@ +# @asmblyr-collaborative/cli + + + +[English](README.md) · [Русский](README.ru.md) + + + +Подключает TypeScript-проект к Collaborative и генерирует типы коллекций/плагинов +по доступу вошедшего пользователя. Нужен Node.js 22+. Установите CLI рядом с SDK: + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +npx asm connect --url https://asmblyr.example.test +``` + +В репозитории соберите пакеты и используйте workspace-команду: + +```sh +pnpm build:packages +pnpm exec asm connect --url https://asmblyr.example.test +pnpm exec asm schema pull +pnpm exec asm generate +pnpm exec asm schema check +pnpm exec asm schema check --offline +``` + +Без credentials сетевые команды открывают вход и согласие в админке. Можно указать +корень Core API, UI или UI /api; CLI при необходимости обнаружит API. +С --no-browser откройте показанный URL самостоятельно. Согласие выдаёт schema:read +на 10 минут, с привязкой к человеческой сессии и PKCE S256. Читать/изменять записи +оно не позволяет. Токен остаётся только в памяти команды; следующий онлайн-вызов +снова требует согласия. Отзыв сессии отзывает доступ. AUTH_UI_URL должен указывать +на доступный origin админки. + +Для CI используйте ASMBLYR_ACCESS_TOKEN или --token-stdin и явный Core API/UI /api. +Credentials не сохраняются в генерируемых файлах. Требуется HTTPS, кроме localhost; +HTTP-редиректы отклоняются. + +Connect создаёт asmblyr.config.json, asmblyr.schema.json и asmblyr.schema.ts, +не перезаписывая существующие файлы. Schema pull обновляет подключение. +--config, --schema-file и --types-file выбирают относительные пути проекта. +Asm generate (также asm schema generate) пересоздаёт TypeScript по проверенному +хешем снимку без сети и токенов. + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; +import { schema } from "./asmblyr.schema.js"; + +const client = createClient({ + baseUrl: "https://asmblyr.example.test", + accessToken: process.env.ASMBLYR_ACCESS_TOKEN, + schema, +}); +const articles = await client.Articles.select((a) => [a.id, a.title]) + .where((a) => a.status.eq("published").and(a.price.gte("1000.00"))) + .orderBy((a) => a.created_at.desc()) + .limit(20) + .exec(); +``` + +Пример предполагает наличие указанных коллекций/полей. Рабочий клиент использует +отдельные API-креды или браузерную сессию, а не schema-токен CLI. +Read/Create/Update различают обязательные, readonly-поля и закрытые варианты. +Псевдонимы выводятся из технических имён: articles → Articles. +Collection("articles") тоже поддерживает подсказки. Проекция содержит выбранные +поля; значения остаются optional из-за прав. Decimal/bigint и даты передаются +строками API. Особый TS-трансформер или плагин сборщика не нужен. + +Kit model handlers дают методы вроде client.plugins.calculator.calculate(input), +вызывающие исходный HTTP handler. Core проверяет схемы и AccessGate; при вызове +может потребоваться личное OAuth-подключение. Старые действия без выходной схемы +пропускаются. Браузерные типы/клиент не импортируют Node-only генератор. + +Schema check завершится с кодом 1 при изменении удалённой схемы или типов. +--offline сверяет сохранённый снимок и исходник. Снимок содержит доступные типы, +права и JSON-контракты плагинов, без defaults, условий политик, записей и секретов. +Имена, варианты и аннотации тоже могут быть приватными: публикуйте их только при +допустимости для проекта. Генерируйте с тем же доступом, что у рабочего клиента. +Актуальные права и условия строк всегда проверяет Core. + +[SDK](../sdk/README.ru.md) · [Подготовка пакетов](../../docs/ru/reference/packages.md). diff --git a/packages/kit/CAPABILITIES.md b/packages/kit/CAPABILITIES.md index 0e9ddf4..12744de 100644 --- a/packages/kit/CAPABILITIES.md +++ b/packages/kit/CAPABILITIES.md @@ -1,6 +1,14 @@ -# Возможности плагина + -Пакет перечисляет используемые возможности в `package.json`: +# Plugin capabilities + + + +[English](CAPABILITIES.md) · [Русский](CAPABILITIES.ru.md) + + + +Declare capabilities in the package's package.json: ```json { @@ -22,7 +30,7 @@ } ``` -Владелец проекта разрешает их по **имени npm-пакета** в корневом `package.json`: +The project owner approves them by npm package name in the root package.json: ```json { @@ -43,50 +51,31 @@ } ``` -Core сверяет все включённые пакеты до импорта их серверных модулей. -Неизвестные, повторяющиеся или неразрешённые возможности останавливают запуск. -Отсутствующий список означает отсутствие дополнительных возможностей. -Разрешение, которого нет в декларации самого пакета, не выдаётся. -При обновлении пакета новые возможности требуют изменения конфигурации проекта; -изменения деклараций и разрешений применяются после перезапуска Core. - -| Возможность | Что разрешено | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identity.profile` | `actor.displayName` текущего пользователя или сервиса. Email, токены и чужие профили не выдаются. | -| `items.read` | `items.list/get` в пределах прав вызывающего пользователя. | -| `items.write` | `items.create/update/delete` в пределах прав пользователя. Без `items.read` ответ записи содержит `data: null`. `commit` требует обе возможности. | -| `collections.manage` | Установка деклараций и выполнение миграций собственных коллекций. Нет произвольного SQL или изменения чужих таблиц. | -| `storage.own` | Доступ к собственным коллекциям через `storage`. Предметные условия проверяет плагин. | -| `hooks.items` | Обработчики `items.create/update/delete`. | -| `hooks.collections` | Обработчик `collections.delete`. | -| `settings` | Декларация настроек и получение значений своего пакета. | -| `notifications` | Подписка текущего человека на запись; публикация, обновление и удаление inbox-событий своего namespace. Получателей и доступ проверяет Core. Model handlers этот контекст не получают. | - -Для обсуждений добавьте `notifications` одновременно в manifest комментариев и -разрешения проекта. См. [обсуждения и уведомления](https://github.com/Asmblyr/Collaborative/blob/main/docs/features/notifications.md). -`context.notifications.publish({ collection, item, eventId, panelId, targetId, preview })` -вызывается внутри `context.withRecord`, рядом с записью события в storage, и должна -быть awaited. `eventId` устраняет повторную доставку в пределах сохранённого inbox; -`preview` — обычный текст до 300 символов. Произвольных получателей или URL нет. - -Базовые `actor.id` и `actor.kind` доступны аутентифицированному endpoint без -дополнительных возможностей: они нужны для авторства операций. -Проверка возможностей действует и для superuser. Возможность `items.write` -не добавляет пользователю права записи. HTTP и MCP используют те же ограничения; -MCP дополнительно учитывает публикацию коллекций и `readOnly` действия. -Привилегированный `storage` model handlers не получают. - -Администратор видит включённые пакеты и разрешения в **Система → Плагины**. -Эта страница не изменяет разрешения: они остаются частью конфигурации проекта. - -## Граница контроля - -Это контракт доверенных пакетов, исполняемых в процессе Core. Он контролирует -выданные Core возможности: Kit, установку схемы, hooks и настройки. Произвольный -Node-код пакета технически может импортировать сетевые и файловые библиотеки; -декларация не является песочницей. UI пакета также исполняется в контексте админки. -Устанавливайте только доверенные пакеты. Изолированное выполнение — отдельный этап. - -Секреты не хранятся в обычных настройках плагина. Для них нужен отдельный контракт -хранения и выдачи. Объекты запроса, `items`, `storage` и контекст hook нельзя -сохранять в глобальных переменных или использовать в фоновых задачах. +Core checks all enabled packages before importing server modules. Unknown, duplicate, or unapproved capabilities stop startup. A missing list grants no additional capabilities. Project approval cannot grant something absent from the package declaration. New capabilities after an upgrade require project configuration changes; restart Core to apply declarations/approvals. + +| Capability | Access | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| identity.profile | Current human/service actor.displayName; no email, tokens, or other profiles | +| items.read | Items.list/get within caller permissions | +| items.write | Create/update/delete within caller permissions; without items.read, writes return data:null; commit requires both | +| collections.manage | Install/migrate own declared collections; no arbitrary SQL or foreign tables | +| storage.own | Own collections through storage; plugin enforces domain conditions | +| hooks.items | Items.create/update/delete hooks | +| hooks.collections | Collections.delete hooks | +| settings | Declare/read own package settings | +| notifications | Current human's record subscriptions and own-namespace inbox publish/update/remove; Core selects recipients/checks access; unavailable in model handlers | +| connections.google | Restricted personal Google broker for eligible model handlers; no credentials or automatic write confirmation | + +For discussions, declare and approve notifications together. See [notifications](https://github.com/Asmblyr/Collaborative/blob/main/docs/features/notifications.md). Await context.notifications.publish({ collection, item, eventId, panelId, targetId, preview }) inside context.withRecord beside the storage write. EventId deduplicates within retained inbox data. Preview is plain text up to 300 characters; arbitrary recipients/URLs are not accepted. + +Basic actor.id/kind are available to authenticated endpoints without extra capabilities for authorship. Capability checks also apply to superusers. Items.write does not grant the caller write permissions. HTTP/MCP share restrictions; MCP additionally checks publication/readOnly. Model handlers never receive privileged storage. + +Administrators view enabled packages and capabilities under Settings → Plugins. Approval remains project configuration, not an editable UI switch. + + + +## Control boundary + +Trusted packages run inside Core. Capabilities control services supplied by Core: Kit, schema installation, hooks, and settings. Arbitrary Node code can still import networking/filesystem libraries. This is not a sandbox. Plugin UI also runs in the admin context. Install trusted packages; isolated execution is not provided. + +Ordinary plugin settings are not secret storage. Secrets need a separate storage/access contract. Never retain request, items, storage, or hook contexts globally or use them in background tasks. diff --git a/packages/kit/CAPABILITIES.ru.md b/packages/kit/CAPABILITIES.ru.md new file mode 100644 index 0000000..5e32fcc --- /dev/null +++ b/packages/kit/CAPABILITIES.ru.md @@ -0,0 +1,102 @@ + + +# Возможности плагина + + + +[English](CAPABILITIES.md) · [Русский](CAPABILITIES.ru.md) + + + +Пакет перечисляет используемые возможности в `package.json`: + +```json +{ + "asmblyr": { + "manifest": { + "version": 1, + "namespace": "comments", + "capabilities": [ + "identity.profile", + "items.read", + "collections.manage", + "storage.own", + "hooks.items", + "hooks.collections", + "settings" + ] + } + } +} +``` + +Владелец проекта разрешает их по **имени npm-пакета** в корневом `package.json`: + +```json +{ + "asmblyr": { + "plugins": ["@asmblyr-collaborative/plugin-comments"], + "pluginPermissions": { + "@asmblyr-collaborative/plugin-comments": [ + "identity.profile", + "items.read", + "collections.manage", + "storage.own", + "hooks.items", + "hooks.collections", + "settings" + ] + } + } +} +``` + +Core сверяет все включённые пакеты до импорта их серверных модулей. +Неизвестные, повторяющиеся или неразрешённые возможности останавливают запуск. +Отсутствующий список означает отсутствие дополнительных возможностей. +Разрешение, которого нет в декларации самого пакета, не выдаётся. +При обновлении пакета новые возможности требуют изменения конфигурации проекта; +изменения деклараций и разрешений применяются после перезапуска Core. + +| Возможность | Что разрешено | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `identity.profile` | `actor.displayName` текущего пользователя или сервиса. Email, токены и чужие профили не выдаются. | +| `items.read` | `items.list/get` в пределах прав вызывающего пользователя. | +| `items.write` | `items.create/update/delete` в пределах прав пользователя. Без `items.read` ответ записи содержит `data: null`. `commit` требует обе возможности. | +| `collections.manage` | Установка деклараций и выполнение миграций собственных коллекций. Нет произвольного SQL или изменения чужих таблиц. | +| `storage.own` | Доступ к собственным коллекциям через `storage`. Предметные условия проверяет плагин. | +| `hooks.items` | Обработчики `items.create/update/delete`. | +| `hooks.collections` | Обработчик `collections.delete`. | +| `settings` | Декларация настроек и получение значений своего пакета. | +| `notifications` | Подписка текущего человека на запись; публикация, обновление и удаление inbox-событий своего namespace. Получателей и доступ проверяет Core. Model handlers этот контекст не получают. | + +Для обсуждений добавьте `notifications` одновременно в manifest комментариев и +разрешения проекта. См. [обсуждения и уведомления](https://github.com/Asmblyr/Collaborative/blob/main/docs/ru/features/notifications.md). +`context.notifications.publish({ collection, item, eventId, panelId, targetId, preview })` +вызывается внутри `context.withRecord`, рядом с записью события в storage, и должна +быть awaited. `eventId` устраняет повторную доставку в пределах сохранённого inbox; +`preview` — обычный текст до 300 символов. Произвольных получателей или URL нет. + +Базовые `actor.id` и `actor.kind` доступны аутентифицированному endpoint без +дополнительных возможностей: они нужны для авторства операций. +Проверка возможностей действует и для superuser. Возможность `items.write` +не добавляет пользователю права записи. HTTP и MCP используют те же ограничения; +MCP дополнительно учитывает публикацию коллекций и `readOnly` действия. +Привилегированный `storage` model handlers не получают. + +Администратор видит включённые пакеты и разрешения в **Система → Плагины**. +Эта страница не изменяет разрешения: они остаются частью конфигурации проекта. + + + +## Граница контроля + +Это контракт доверенных пакетов, исполняемых в процессе Core. Он контролирует +выданные Core возможности: Kit, установку схемы, hooks и настройки. Произвольный +Node-код пакета технически может импортировать сетевые и файловые библиотеки; +декларация не является песочницей. UI пакета также исполняется в контексте админки. +Устанавливайте только доверенные пакеты. Изолированное выполнение — отдельный этап. + +Секреты не хранятся в обычных настройках плагина. Для них нужен отдельный контракт +хранения и выдачи. Объекты запроса, `items`, `storage` и контекст hook нельзя +сохранять в глобальных переменных или использовать в фоновых задачах. diff --git a/packages/kit/FIELDS.md b/packages/kit/FIELDS.md index a686e1c..02f382f 100644 --- a/packages/kit/FIELDS.md +++ b/packages/kit/FIELDS.md @@ -1,15 +1,21 @@ -# Редакторы полей из плагинов + -Первый этап поддерживает обычные поля хранения `text`. Плагин предоставляет -ввод, необязательное отображение значения и настройки каждого поля. Его -регистрация использует существующий браузерный export `./ui`: +# Plugin field editors + + + +[English](FIELDS.md) · [Русский](FIELDS.ru.md) + + + +The initial contract supports ordinary text storage fields. A plugin supplies an editor, optional value display, and per-field settings through its existing ./ui browser export. ```ts export default defineUiPlugin({ fieldInterfaces: [ { id: "picker", - title: "Цвет", + title: "Color", types: ["text"], editor: ColorEditor, display: ColorDisplay, @@ -19,44 +25,31 @@ export default defineUiPlugin({ }); ``` -Рабочий пример: [examples/plugins/color](../../examples/plugins/color/README.md). Серверный -`plugin.ts` остаётся `definePlugin({})`; таблицы и HTTP endpoints этому плагину -не нужны. Включение пакета, сборка, source/built discovery и общие компоненты -работают так же, как у страниц и вкладок. +See the working [color example](../../examples/plugins/color/README.md). Server plugin.ts remains definePlugin({}); no tables or endpoints are needed. Enablement, build, source/built discovery, and shared components follow page/panel conventions. + + + +## Component contract -## Контракт компонентов +Types from @asmblyr-collaborative/kit/ui: -Типы доступны из `@asmblyr-collaborative/kit/ui`: +- FieldEditorProps: id, label, value, options, disabled, required, placeholder, describedBy, onChange(value). +- FieldDisplayProps: value:string|null, options. +- FieldInterfaceSettingsProps: options, disabled, onChange(options). -- `FieldEditorProps`: `id`, `label`, `value`, `options`, `disabled`, `required`, - `placeholder`, `describedBy`, `onChange(value)`. -- `FieldDisplayProps`: `value: string | null`, `options`. -- `FieldInterfaceSettingsProps`: `options`, `disabled`, `onChange(options)`. +Options is a JSON object. Handle empty, older, and unknown options with suitable defaults. Settings changes use the callback and belong to that field. -`options` — JSON-объект. Плагин должен читать пустые, старые и неизвестные -настройки с подходящими значениями по умолчанию. Компонент настроек сохраняет -изменения через callback; настройки принадлежат конкретному полю. +Editors receive string drafts; empty string means no input. Core's form owns null conversion, omission for create defaults, required validation, and API submission. Do not save records from editors or replace callbacks with separate storage. Call onChange only after user action, respect disabled, and never mutate values on mount. Host/API enforce field reads/writes. -Редактор получает строковый черновик, пустая строка означает отсутствие ввода. -Преобразование в `null`, пропуск default при создании, проверка обязательности -и отправка в API принадлежат форме Core. Нельзя сохранять запись из редактора -или подменять callback собственным хранилищем. Плагин вызывает `onChange` только -в ответ на действие пользователя, учитывает `disabled` и не меняет значение -при монтировании. Права на чтение и изменение полей проверяются хостом и API. +Use native required, pattern, and setCustomValidity on Input/Textarea. The form checks changed fields before save and shows errors at the field. Server validation remains essential; UI constraints are not API authorization. -Для проверок ввода используйте обычные `required`, `pattern`, `setCustomValidity` -на Input/Textarea. Форма проверяет их перед сохранением изменённых полей и -показывает ошибку у нужного поля. Серверные ограничения остаются обязательными: -UI-проверки не являются ограничением API или способом авторизации. +Import shadcn components from @asmblyr-collaborative/kit/ui/<component>. PortalContainerContext keeps nested Select portals inside the native dialog. Components must support React SSR: access DOM in handlers/effects, not imports or render. -Компоненты shadcn импортируются из `@asmblyr-collaborative/kit/ui/`. Хост передаёт -контейнер нативного диалога через `PortalContainerContext`: вложенные Select -остаются внутри окна. Компоненты должны поддерживать серверный рендер React; -доступ к DOM допустим в обработчиках и effects, а не при импорте/рендере. + -## Хранение и отключение +## Persistence and disabling -В существующем JSONB presentation сохраняется: +Existing presentation JSONB stores: ```json { @@ -68,27 +61,17 @@ UI-проверки не являются ограничением API или с } ``` -ID формируется как `:`. Core проверяет формат, совместимый -тип и ограниченный JSON: до 8 КиБ, 8 уровней, 1000 узлов. URL/импорты из -метаданных не исполняются. Настройки видны читателям поля и не подходят для -секретов. Новая миграция базы не требуется. - -Настройки сохраняются и для отсутствующего пакета. UI использует только -собранные расширения, включённые в Core. Если расширение отсутствует, ввод -возвращается к стандартному полю с уведомлением, отображение — к обычному -тексту. Ошибка клиентского рендера компонента также включает fallback. Черновик -остаётся в хосте. Повторное включение пакета возвращает оформление. - -Смена редактора в настройках — явное действие: она удаляет ссылку на предыдущий -редактор и его options, не меняя записанные значения. Настройка плагина имеет -приоритет над встроенным display; при её выборе UI сбрасывает старый display. - -## Границы этапа - -- Один редактор может включать свой display; независимый выбор display плагина - пока не реализован. -- Расширение не определяет SQL-тип, серверную валидацию или отдельные права. -- Цвет поддерживает HEX `#RRGGBB`. `allowCustom` управляет выбором в UI; - через API поле остаётся строкой с обычными ограничениями Core. -- События, внешние справочники, редакторы связей и асинхронные операции - редактора относятся к следующим этапам. +IDs use namespace:local-id. Core validates format, compatible type, and bounded JSON: 8 KiB, depth 8, 1000 nodes. Metadata URLs/imports are never executed. Field readers see options, so they must contain no secrets. No new database migration is required. + +Missing packages retain settings. UI loads only built extensions enabled in Core. If absent, editors fall back to the standard input with a notice, and displays to plain text. Client render errors also trigger fallback. The host retains the draft. Re-enabling restores presentation. + +Changing editor explicitly removes the prior extension/options without changing stored values. Plugin presentation takes precedence over built-in display; selecting it clears the old display. + + + +## Limits + +- An editor may include its own display; independent plugin-display selection is unsupported. +- Extensions define no SQL type, server validator, or separate permissions. +- Color supports HEX #RRGGBB. AllowCustom controls UI choices; API values remain strings under ordinary Core rules. +- Events, external catalogs, relationship editors, and asynchronous editor operations are not part of this contract. diff --git a/packages/kit/FIELDS.ru.md b/packages/kit/FIELDS.ru.md new file mode 100644 index 0000000..080d31d --- /dev/null +++ b/packages/kit/FIELDS.ru.md @@ -0,0 +1,108 @@ + + +# Редакторы полей из плагинов + + + +[English](FIELDS.md) · [Русский](FIELDS.ru.md) + + + +Первый этап поддерживает обычные поля хранения `text`. Плагин предоставляет +ввод, необязательное отображение значения и настройки каждого поля. Его +регистрация использует существующий браузерный export `./ui`: + +```ts +export default defineUiPlugin({ + fieldInterfaces: [ + { + id: "picker", + title: "Цвет", + types: ["text"], + editor: ColorEditor, + display: ColorDisplay, + settings: ColorSettings, + }, + ], +}); +``` + +Рабочий пример: [examples/plugins/color](../../examples/plugins/color/README.ru.md). Серверный +`plugin.ts` остаётся `definePlugin({})`; таблицы и HTTP endpoints этому плагину +не нужны. Включение пакета, сборка, source/built discovery и общие компоненты +работают так же, как у страниц и вкладок. + + + +## Контракт компонентов + +Типы доступны из `@asmblyr-collaborative/kit/ui`: + +- `FieldEditorProps`: `id`, `label`, `value`, `options`, `disabled`, `required`, + `placeholder`, `describedBy`, `onChange(value)`. +- `FieldDisplayProps`: `value: string | null`, `options`. +- `FieldInterfaceSettingsProps`: `options`, `disabled`, `onChange(options)`. + +`options` — JSON-объект. Плагин должен читать пустые, старые и неизвестные +настройки с подходящими значениями по умолчанию. Компонент настроек сохраняет +изменения через callback; настройки принадлежат конкретному полю. + +Редактор получает строковый черновик, пустая строка означает отсутствие ввода. +Преобразование в `null`, пропуск default при создании, проверка обязательности +и отправка в API принадлежат форме Core. Нельзя сохранять запись из редактора +или подменять callback собственным хранилищем. Плагин вызывает `onChange` только +в ответ на действие пользователя, учитывает `disabled` и не меняет значение +при монтировании. Права на чтение и изменение полей проверяются хостом и API. + +Для проверок ввода используйте обычные `required`, `pattern`, `setCustomValidity` +на Input/Textarea. Форма проверяет их перед сохранением изменённых полей и +показывает ошибку у нужного поля. Серверные ограничения остаются обязательными: +UI-проверки не являются ограничением API или способом авторизации. + +Компоненты shadcn импортируются из `@asmblyr-collaborative/kit/ui/`. Хост передаёт +контейнер нативного диалога через `PortalContainerContext`: вложенные Select +остаются внутри окна. Компоненты должны поддерживать серверный рендер React; +доступ к DOM допустим в обработчиках и effects, а не при импорте/рендере. + + + +## Хранение и отключение + +В существующем JSONB presentation сохраняется: + +```json +{ + "interface": "auto", + "extension": { + "id": "color:picker", + "options": { "palette": ["#0d9488", "#3b82f6"], "allowCustom": false } + } +} +``` + +ID формируется как `:`. Core проверяет формат, совместимый +тип и ограниченный JSON: до 8 КиБ, 8 уровней, 1000 узлов. URL/импорты из +метаданных не исполняются. Настройки видны читателям поля и не подходят для +секретов. Новая миграция базы не требуется. + +Настройки сохраняются и для отсутствующего пакета. UI использует только +собранные расширения, включённые в Core. Если расширение отсутствует, ввод +возвращается к стандартному полю с уведомлением, отображение — к обычному +тексту. Ошибка клиентского рендера компонента также включает fallback. Черновик +остаётся в хосте. Повторное включение пакета возвращает оформление. + +Смена редактора в настройках — явное действие: она удаляет ссылку на предыдущий +редактор и его options, не меняя записанные значения. Настройка плагина имеет +приоритет над встроенным display; при её выборе UI сбрасывает старый display. + + + +## Границы этапа + +- Один редактор может включать свой display; независимый выбор display плагина + пока не реализован. +- Расширение не определяет SQL-тип, серверную валидацию или отдельные права. +- Цвет поддерживает HEX `#RRGGBB`. `allowCustom` управляет выбором в UI; + через API поле остаётся строкой с обычными ограничениями Core. +- События, внешние справочники, редакторы связей и асинхронные операции + редактора относятся к следующим этапам. diff --git a/packages/kit/HOOKS.md b/packages/kit/HOOKS.md index 47e129c..02bee72 100644 --- a/packages/kit/HOOKS.md +++ b/packages/kit/HOOKS.md @@ -1,11 +1,20 @@ -# Hooks и настройки + -`plugin.ts` остаётся `definePlugin({})`. Core обнаруживает дополнительные файлы -автоматически. Возможности объявляются и разрешаются по [контракту capabilities](CAPABILITIES.md). +# Hooks and settings -## Транзакционные hooks + -Каждый файл непосредственно в `server/hooks/` экспортирует один обработчик: +[English](HOOKS.md) · [Русский](HOOKS.ru.md) + + + +`plugin.ts` remains definePlugin({}). Core discovers additional files automatically. Declare and approve [capabilities](CAPABILITIES.md). + + + +## Transactional hooks + +Each direct server/hooks file exports one handler: ```ts import { defineHook } from "@asmblyr-collaborative/kit"; @@ -18,91 +27,63 @@ export default defineHook("items.delete", async (event, context) => { }); ``` -Пример очистки с пагинацией: `packages/plugin-comments/server/services/cleanup.ts`. -Не создавайте реестр hooks в `plugin.ts`. - -| Событие | Данные | -| -------------------- | -------------------------------------- | -| `items.create` | `collection`, `collectionId`, `itemId` | -| `items.update` | `collection`, `collectionId`, `itemId` | -| `items.delete` | `collection`, `collectionId`, `itemId` | -| `collections.delete` | `collection`, `collectionId` | - -`collectionId` — стабильный ID экземпляра коллекции; имя может впоследствии -принадлежать другой коллекции. `itemId` — строковое представление сохранённого ключа. -Скрытые значения полей в событие не попадают. Контекст предоставляет actor, -requestId, logger, reader `items`, разрешённый `storage` и настройки пакета. -Reader использует права исходного пользователя; удалённую запись уже не прочитать. - -Событие вызывается после мутации и записи истории, **до commit**. Hook выполняется -в той же транзакции, включая свою работу с `storage`. Ошибка прерывает операцию и -откатывает данные, историю и изменения предыдущих hooks. Обработчики выполняются -последовательно: порядок пакетов в конфигурации, затем имена файлов по порядку -кодовых единиц строки. Source и production используют один порядок. -Нельзя рассчитывать, что внешний HTTP-вызов или отправка уведомления откатятся -вместе с БД. Такие эффекты и гарантированная доставка после commit пока не поддержаны. -Каждый вызов сервиса обязательно нужно `await`-ить. - -HTTP items, Kit, MCP, массовые изменения, операции связей и сохранение черновика -используют общую границу мутаций. Нет событий для update без изменений, прямого -SQL, каскадных действий PostgreSQL и записей в `asmblyr_`/`plugin_`. Исключение -внутренних коллекций предотвращает рекурсию при очистке собственного хранилища. -Отключённый плагин не получает события; его данные сохраняются. - -Production export: `"./hooks": "./dist/hooks.json"`. -Сборщик создаёт индекс; вручную поддерживать его не нужно. -В `server/hooks` допустимы только непосредственные `.ts`-файлы без symlinks; -вспомогательный код размещается в `server/services` или другой соседней папке. - -## Работа с данными, привязанными к записи - -В обычном endpoint `context.withRecord(collection, id, async context => ...)` -проверяет право чтения, удерживает разделяемую блокировку записи и выполняет callback -в транзакции. Используйте **контекст callback** для своего `storage`: он участвует -в той же транзакции. Это позволяет сохранять комментарий и исключить одновременное -удаление его целевой записи. Метод требует `items.read`; в model handlers его нет. -В callback не выполняйте длительную внешнюю работу и не сохраняйте контекст наружу. - -## Настройки - -Файл `server/settings.ts`: +See packages/plugin-comments/server/services/cleanup.ts for paginated cleanup. Do not create a hook registry in plugin.ts. + +| Event | Data | +| ------------------ | -------------------------------- | +| items.create | collection, collectionId, itemId | +| items.update | collection, collectionId, itemId | +| items.delete | collection, collectionId, itemId | +| collections.delete | collection, collectionId | + +`collectionId` identifies the collection instance; the name may later be reused. `itemId` is the saved key as a string. Hidden field values are excluded. Context includes actor, requestId, logger, items reader, approved storage, and package settings. Reads use the original caller; deleted rows cannot be reread. + +Hooks run after mutation/history but before commit, in the same transaction, including storage operations. Failure rolls back data, history, and earlier hooks. Execution is sequential: configured package order, then filename order by string code units, identical in source/production. + +External HTTP calls or notifications cannot be rolled back with PostgreSQL. External effects and guaranteed post-commit delivery are unsupported. Await every service call. + +HTTP items, Kit, MCP, bulk, relation changes, and draft saves share this boundary. No events fire for unchanged updates, direct SQL, PostgreSQL cascades, or asmblyr*/plugin* writes. Internal-collection exclusion prevents recursive cleanup. Disabled plugins receive no events and retain data. + +Production export: "./hooks": "./dist/hooks.json". The builder maintains the index. Only direct .ts files without symlinks belong under server/hooks; put helpers in server/services or a sibling directory. + + + +## Record-bound data + +In ordinary endpoints, context.withRecord(collection, id, async context => ...) checks reads, holds a shared record lock, and runs the callback transactionally. Use the callback's context for storage in that same transaction. This allows comment saves while preventing concurrent target deletion. + +It requires items.read and is unavailable in model handlers. Do not perform slow external work inside the callback or retain its context outside. + + + +## Settings + +`server/settings.ts`: ```ts import { defineSettings } from "@asmblyr-collaborative/kit"; export default defineSettings({ - title: "Комментарии", + title: "Comments", fields: { allowNewComments: { type: "boolean", - label: "Разрешить новые комментарии", + label: "Allow new comments", default: true, }, }, }); ``` -Использование в endpoint, model handler или hook: +Use in endpoints, model handlers, or hooks: ```ts const options = useSettings(context, settings); -// options.allowNewComments имеет тип boolean. +// options.allowNewComments is boolean. ``` -Поддержаны boolean, строка с maxLength, число с min/max/integer, select со списком -вариантов. Core проверяет декларацию, default и каждое сохраняемое значение. -Неизвестные поля, пропущенные значения и неявное приведение типов не допускаются. -До 40 полей и 32 000 символов JSON значений. Это обычные настройки, не секреты. - -Production export: `"./settings": "./dist/server/settings.js"`. -Суперпользователь управляет настройками через `/settings/plugins/:namespace` -и форму **Система → Плагины**. PUT принимает `{ values, revision }`; устаревшая -revision возвращает 409, сохраняя черновик в UI. `null` обозначает ещё не сохранённые -значения по умолчанию. Кнопка «По умолчанию» меняет черновик до явного сохранения. - -Значения хранятся в `asmblyr_settings` под ключом `plugin:`. -Изменения действуют для новых запросов без перезапуска; в журнале безопасности -сохраняются автор, namespace и имена изменённых настроек, без копирования значений. -Новые поля получают default, удалённые игнорируются. Несовместимое изменение типа -или диапазона существующей настройки требует предварительного обновления её значения: -Core отклоняет неверное сохранённое значение и не исправляет его молча. +Supported types: boolean, string with maxLength, number with min/max/integer, and select options. Core validates declaration, defaults, and saved values. Unknown fields, missing values, and implicit coercion are rejected. Limits: 40 fields and 32000 JSON characters. These are ordinary settings, not secrets. + +Production export: `"./settings": "./dist/server/settings.js"`. Manage them through `/settings/plugins/:namespace` and Settings → Extensions. Reads require `plugins/read` (or `plugins/update`); writes require `plugins/update`. Superusers also have access. PUT accepts `{ values, revision }`; stale revision returns 409 and preserves the UI draft. A null revision represents unsaved defaults. Restore defaults changes the draft until explicitly saved. + +Values live in asmblyr_settings under plugin:<namespace>. New requests see changes without restart. Security audit stores actor, namespace, and changed setting names, not values. New fields receive defaults; removed fields are ignored. Incompatible type/range changes require updating stored values first: Core rejects invalid saved values rather than silently repairing them. diff --git a/packages/kit/HOOKS.ru.md b/packages/kit/HOOKS.ru.md new file mode 100644 index 0000000..9b63ef3 --- /dev/null +++ b/packages/kit/HOOKS.ru.md @@ -0,0 +1,123 @@ + + +# Hooks и настройки + + + +[English](HOOKS.md) · [Русский](HOOKS.ru.md) + + + +`plugin.ts` остаётся `definePlugin({})`. Core обнаруживает дополнительные файлы +автоматически. Возможности объявляются и разрешаются по [контракту capabilities](CAPABILITIES.ru.md). + + + +## Транзакционные hooks + +Каждый файл непосредственно в `server/hooks/` экспортирует один обработчик: + +```ts +import { defineHook } from "@asmblyr-collaborative/kit"; + +export default defineHook("items.delete", async (event, context) => { + context.logger.info("Record removed", { + collection: event.collection, + itemId: event.itemId, + }); +}); +``` + +Пример очистки с пагинацией: `packages/plugin-comments/server/services/cleanup.ts`. +Не создавайте реестр hooks в `plugin.ts`. + +| Событие | Данные | +| -------------------- | -------------------------------------- | +| `items.create` | `collection`, `collectionId`, `itemId` | +| `items.update` | `collection`, `collectionId`, `itemId` | +| `items.delete` | `collection`, `collectionId`, `itemId` | +| `collections.delete` | `collection`, `collectionId` | + +`collectionId` — стабильный ID экземпляра коллекции; имя может впоследствии +принадлежать другой коллекции. `itemId` — строковое представление сохранённого ключа. +Скрытые значения полей в событие не попадают. Контекст предоставляет actor, +requestId, logger, reader `items`, разрешённый `storage` и настройки пакета. +Reader использует права исходного пользователя; удалённую запись уже не прочитать. + +Событие вызывается после мутации и записи истории, **до commit**. Hook выполняется +в той же транзакции, включая свою работу с `storage`. Ошибка прерывает операцию и +откатывает данные, историю и изменения предыдущих hooks. Обработчики выполняются +последовательно: порядок пакетов в конфигурации, затем имена файлов по порядку +кодовых единиц строки. Source и production используют один порядок. +Нельзя рассчитывать, что внешний HTTP-вызов или отправка уведомления откатятся +вместе с БД. Такие эффекты и гарантированная доставка после commit пока не поддержаны. +Каждый вызов сервиса обязательно нужно `await`-ить. + +HTTP items, Kit, MCP, массовые изменения, операции связей и сохранение черновика +используют общую границу мутаций. Нет событий для update без изменений, прямого +SQL, каскадных действий PostgreSQL и записей в `asmblyr_`/`plugin_`. Исключение +внутренних коллекций предотвращает рекурсию при очистке собственного хранилища. +Отключённый плагин не получает события; его данные сохраняются. + +Production export: `"./hooks": "./dist/hooks.json"`. +Сборщик создаёт индекс; вручную поддерживать его не нужно. +В `server/hooks` допустимы только непосредственные `.ts`-файлы без symlinks; +вспомогательный код размещается в `server/services` или другой соседней папке. + + + +## Работа с данными, привязанными к записи + +В обычном endpoint `context.withRecord(collection, id, async context => ...)` +проверяет право чтения, удерживает разделяемую блокировку записи и выполняет callback +в транзакции. Используйте **контекст callback** для своего `storage`: он участвует +в той же транзакции. Это позволяет сохранять комментарий и исключить одновременное +удаление его целевой записи. Метод требует `items.read`; в model handlers его нет. +В callback не выполняйте длительную внешнюю работу и не сохраняйте контекст наружу. + + + +## Настройки + +Файл `server/settings.ts`: + +```ts +import { defineSettings } from "@asmblyr-collaborative/kit"; + +export default defineSettings({ + title: "Комментарии", + fields: { + allowNewComments: { + type: "boolean", + label: "Разрешить новые комментарии", + default: true, + }, + }, +}); +``` + +Использование в endpoint, model handler или hook: + +```ts +const options = useSettings(context, settings); +// options.allowNewComments имеет тип boolean. +``` + +Поддержаны boolean, строка с maxLength, число с min/max/integer, select со списком +вариантов. Core проверяет декларацию, default и каждое сохраняемое значение. +Неизвестные поля, пропущенные значения и неявное приведение типов не допускаются. +До 40 полей и 32 000 символов JSON значений. Это обычные настройки, не секреты. + +Production export: `"./settings": "./dist/server/settings.js"`. +Настройки доступны через `/settings/plugins/:namespace` и форму **Настройки → Расширения**: +чтение требует `plugins/read` (либо `plugins/update`), запись — `plugins/update`; +superuser также имеет доступ. PUT принимает `{ values, revision }`; устаревшая +revision возвращает 409, сохраняя черновик в UI. `null` обозначает ещё не сохранённые +значения по умолчанию. Кнопка «По умолчанию» меняет черновик до явного сохранения. + +Значения хранятся в `asmblyr_settings` под ключом `plugin:`. +Изменения действуют для новых запросов без перезапуска; в журнале безопасности +сохраняются автор, namespace и имена изменённых настроек, без копирования значений. +Новые поля получают default, удалённые игнорируются. Несовместимое изменение типа +или диапазона существующей настройки требует предварительного обновления её значения: +Core отклоняет неверное сохранённое значение и не исправляет его молча. diff --git a/packages/kit/LIFECYCLE.md b/packages/kit/LIFECYCLE.md index 98fda9c..d4936b7 100644 --- a/packages/kit/LIFECYCLE.md +++ b/packages/kit/LIFECYCLE.md @@ -1,9 +1,18 @@ -# Миграции, хранилище и UI плагина + -## Миграции +# Plugin migrations, storage, and UI -Файлы `server/migrations/YYYYMMDDHHmmss_name.ts` экспортируют `defineMigration()`. -Нужен export `"./migrations": "./dist/migrations.json"` в package.json. + + +[English](LIFECYCLE.md) · [Русский](LIFECYCLE.ru.md) + + + + + +## Migrations + +`server/migrations/YYYYMMDDHHmmss_name.ts` files export defineMigration. `package.json` needs "./migrations": "./dist/migrations.json". ```ts import { defineMigration } from "@asmblyr-collaborative/kit"; @@ -26,47 +35,27 @@ export default defineMigration({ }); ``` -Одновременно обновите актуальное описание в `server/collections`. Миграции содержат -неизменяемые JSON-планы: контрольная сумма не зависит от компиляции TS в JS. -Не импортируйте изменяемое актуальное описание в старую миграцию: её план должен -оставаться прежним. Есть `addField` и обычный составной B-tree `addIndex`. -SQL, `drop`, смены типов, переименований, backfill и автоматического down пока нет. -NOT NULL без default на заполненной таблице завершится ошибкой PostgreSQL и откатом. +Update the current server/collections declaration too. Migrations are immutable JSON plans; checksums do not depend on TS-to-JS compilation. Never import mutable current declarations into old migrations. Supported operations are addField and ordinary composite B-tree addIndex. SQL, drop, type changes, renames, backfills, and automatic down are unsupported. Adding NOT NULL without a default to populated data fails in PostgreSQL and rolls back. + +Core applies new plans by filename under a shared lock. Operations accept only local names of owned collections. Asmblyr_plugin_migrations records name, SHA-256 plan hash, timestamp, and baseline flag. Editing/removing applied plans or inserting a plan before an applied one is forbidden. Operation failures or final-declaration mismatch roll back the full installation: DDL, metadata, and journal. -Core под общей блокировкой запускает новые планы по имени файла. Операции принимают -только локальные имена собственных коллекций. Реестр `asmblyr_plugin_migrations` -хранит имя, SHA-256 плана, время и признак baseline. Изменение/удаление выполненного -плана и добавление плана раньше уже применённого запрещены. Ошибка операции или -несовпадение итоговой декларации откатывает всю установку: DDL, метаданные и журнал. +New installations create current declarations, validate addField, create indexes, and baseline plans. New tables in existing plugins also use current schemas. Removing declarations is unsupported. Disabling never deletes data; registry down is blocked while history exists. Direct SQL changes do not synchronize automatically. -При новой установке таблица создаётся по актуальному описанию, addField проверяется, -индексы создаются, планы отмечаются как baseline. Новая таблица в существующем -плагине также получает текущую схему. Удаление деклараций пока не поддержано. -Данные не удаляются при отключении. Down реестра запрещён при наличии истории. -Прямые изменения схемы через SQL автоматически не согласуются с декларациями. + -## Собственное хранилище +## Own storage -`storage` доступен внутри аутентифицированного обработчика пакета с namespace -и разрешённой возможностью `storage.own`. -Методы `list/get/create/update/delete` имеют формат `items`, но принимают локальное -имя, например `storage.list("entries")`. Область доступа ограничена коллекциями -этого пакета. Нет SQL, Knex, `commit`, смены actor или получения чужого namespace. -Применяются валидация Core, история с реальным автором и транзакции записи. +Authenticated namespaced handlers with storage.own receive storage. List/get/create/update/delete follow items shapes but accept local names, such as storage.list("entries"). Access is restricted to that package. No SQL, Knex, commit, actor switching, or foreign namespace access. Core validation, actual-actor history, and write transactions apply. -Это **право доверенного серверного плагина на своё хранилище**. Обычный `items` -по-прежнему использует права пользователя. Плагин обязан проверить предметные -условия до `storage`: comments сначала читает обсуждаемую запись через `items.get`, -потом обращается к своим комментариям. Grants на таблицу комментариев не нужны. +This is privileged access by a trusted plugin to its own storage. Ordinary items keeps user permissions. Plugins must enforce domain rules first: Comments reads the target through items.get before accessing comments storage, so users need no grants on comment tables. -Прямой `/items/plugin_…` остаётся административным путём с обычными grants; выдача -таких grants разрешает работу с таблицей в обход специализированного endpoint. -Для comments выдавать их участникам не нужно. Код плагина доверенный и выполняется -в процессе Core, без песочницы. Пользовательские таблицы через storage недоступны. +Direct /items/plugin\_… remains an administrative path under ordinary grants. Granting it bypasses specialized endpoint rules and is unnecessary for discussion participants. Plugin code is trusted in Core, without a sandbox. Storage cannot access user collections. -## Типизированное хранилище + -Для типизированного доступа к собственной коллекции используйте её декларацию: +## Typed storage + +Use the declaration for typed access: ```ts import { useStorage } from "@asmblyr-collaborative/kit"; @@ -74,109 +63,70 @@ import entries from "./server/collections/entries.js"; const comments = useStorage(context, entries); const row = await comments.get(id); -// row.body, row.author_id и остальные поля выведены из entries. +// row.body, row.author_id, and other fields are inferred from entries. ``` -`get/create/update` возвращают саму строку, `list` — `{ data, labels, page }`. -Имена и типы полей, обязательность при создании, nullable и ручной первичный ключ -выводятся из `defineCollection`. Дефолты позволяют пропускать поле при создании. -Генерируемые ключи и системные даты не принимаются в типизированной записи. -Даты нормализуются в ISO-строки, decimal/bigint сохраняются строками. Несовпадение -типов прочитанной строки с декларацией даёт ошибку вместо неявного преобразования. -Проверки записи и область доступа остаются у Core. Этот API читает полные строки; -для проекций используйте исходный `context.storage`. Не сохраняйте его между запросами. +Get/create/update return the row itself; list returns { data, labels, page }. DefineCollection infers names/types, required create fields, nullability, and manual primary keys. Defaults allow omission on create. Generated keys/timestamps cannot be supplied to typed writes. + +Dates normalize to ISO strings; decimal/bigint remain strings. Read values inconsistent with declarations throw instead of coercing. Core still owns validation/scope. This API reads full rows; use context.storage for projections. Never retain it across requests. + + -## Вкладки редактора записи +## Record-editor tabs -`ui/index.ts` экспортирует `defineUiPlugin({ recordPanels: [...] })` из -**`@asmblyr-collaborative/kit/ui`**. Package.json экспортирует его сборку как `./ui`. -Корневой entry kit и `plugin.ts` в браузер не импортируются. -У вкладки есть `id`, `title`, React `component` и необязательный `supports(record)`. +Ui/index.ts exports defineUiPlugin({ recordPanels: [...] }) from @asmblyr-collaborative/kit/ui, with its build exported as ./ui. Do not import root Kit/plugin.ts into browsers. Panels define id, title, React component, and optional supports(record). -Host передаёт `RecordPanelProps`: +RecordPanelProps includes: -- `record`: техническое имя коллекции, сохранённый ID и отображаемое имя; -- `request(path, init)`: запрос через сессию UI в `/api//…`; -- `onStateChange({ dirty, busy })`: защита текста и выполняющейся записи от закрытия. +- Record: technical collection name, saved ID, display name. +- Request<T>(path, init): authenticated namespace request under /api/<namespace>/…. +- OnStateChange({ dirty, busy }): protects unsaved text and active writes from closing. -Компоненты shadcn импортируются напрямую из `@asmblyr-collaborative/kit/ui/`. -Админка и плагины используют одну реализацию из Kit, включая полные props и ref. -Дополнительный provider или prop `components` не требуется. -Подробнее: [общие компоненты UI](./UI.md). +Import shadcn directly from kit/ui/<component>. Admin/plugins share full props/refs, without extra providers or components props. See [shared UI](./UI.md). -Вкладка монтируется при первом открытии и сохраняется при смене вкладок. -Сбой рендера изолируется от формы записи. Новой несохранённой записи вкладки не -предоставляются. Активные пакеты сверяются с аутентифицированным `/extensions`. +Panels mount on first open and persist across tab changes. Render failures are isolated from the record form. New unsaved records have no panels. Active packages are checked through authenticated /extensions. -Next при запуске/сборке создаёт статические импорты из exports включённых пакетов -и источники Tailwind для их UI. Локальная разработка читает `ui/index.ts`, production -и npm — собранный `./ui`. Изменение списка пакетов требует перезапуска dev-сервера; -production требует пересборки UI. Изменения TSX используют обычный Fast Refresh. -React — peer dependency. Для UI используйте `.ts`/`.tsx` в относительных импортах -и `rewriteRelativeImportExtensions: true` в tsconfig: TypeScript заменяет их на -`.js` при сборке. Добавьте `ui/**/*` и `shared/**/*` в include. +Next generates static imports and Tailwind sources from enabled exports at startup/build. Development reads ui/index.ts; production/npm use built ./ui. Package-list changes require dev restart or production UI rebuild; TSX uses Fast Refresh. React is a peer dependency. Use .ts/.tsx relative imports with rewriteRelativeImportExtensions:true, and include ui/**/\* and shared/**/\*. -Первый полный пример — `packages/plugin-comments`. Серверные пакеты нельзя импортировать в UI. +Packages/plugin-comments is the complete example. Server packages must never enter UI imports. -## Отдельные страницы + -Второй пример — `examples/plugins/overview`. Браузерный entry может экспортировать -страницы, вкладки записи или оба вида расширений: +## Standalone pages + +Examples/plugins/overview demonstrates pages, which can coexist with record panels. ```ts import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; import { OverviewPage } from "./pages/overview-page.tsx"; export default defineUiPlugin({ - pages: [{ id: "home", title: "Обзор", component: OverviewPage }], + pages: [{ id: "home", title: "Overview", component: OverviewPage }], }); ``` -Маршрут: `/extensions//`. Host добавляет страницу в группу -«Приложения» бокового меню, отображает её название в шапке и использует текущий -layout. ID проверяются на допустимость в URL и уникальность внутри каждого вида -расширений. Страница и вкладка одного пакета могут иметь одинаковый ID. - -`PluginPageProps` предоставляет `request: PluginRequest` — тот же ограниченный -namespace HTTP-клиент, что у вкладок. `Button` и `Textarea` доступны из kit. -Общая оболочка изолирует ошибки отрисовки от остального интерфейса. -Список активных пакетов загружается для layout через аутентифицированный -`/extensions` и используется страницами, меню и вкладками записи. - -Пользователь должен войти и иметь доступ к админке по её существующим правилам. -Отдельные разрешения на UI-страницы пока не реализованы; API каждого плагина обязан -сам проверять предметные права. Отсутствующий или отключённый плагин не отображается. -Включение пакета требует сборки/перезапуска UI и Core. - -Пока поддержаны фиксированные страницы без вложенных и динамических маршрутов. -Страница получает также `preparedAction` и `onStateChange`. -`useAction(contract, props, { path, initialInput })` использует обычный `request` -и связывает схему формы с серверным действием, подготовленным результатом -и признаками dirty/busy для host. Обзор только читает собственный контекст пользователя. -Редакторы полей описаны в [FIELDS.md](FIELDS.md). - -## Действия и ассистент - -Первый пример — `examples/plugins/calculator`. TypeScript-интерфейсы описывают поля и результат. -H3 handler в `server/api/calculator/calculate.post.ts` обёрнут в -`defineModelContext(defineHandler(...), annotate)`. -`defineModelAnnotation` описывает назначение и `middleware: AccessGate.authenticated`. -Сборщик и source loader создают `.asmblyr/models` для runtime-валидации и UI; -для импорта JSON формой нужен `resolveJsonModule: true` в tsconfig. -Сканер подключает HTTP-маршрут по файлу; Core публикует явно отмеченные действия -во внутреннем MCP. `plugin.ts` остаётся `definePlugin({})`. -HTTP и MCP вызывают один handler с одним ограниченным контекстом: -`useActionContext(event)` предоставляет actor, superuser, signal и items. -`useItems(event)` работает и в обычных endpoints: проверяет права действия и полей. -Для MCP также проверяется доступность коллекций и связей в MCP. Привилегированного -storage в model handler нет. `readOnly: true` запрещает все методы записи items. -Форма использует `useAction(generatedModel, props, options)`; -схемы браузера и сервера получены из одних типов, серверный handler в UI не попадает. -Подробности и текущие ограничения — [действия плагинов](../../docs/development/plugin-actions.md). - -## Возможности, hooks и настройки - -Пакет объявляет capabilities в manifest; проект разрешает их через -`asmblyr.pluginPermissions`. Правила и ограничения: [CAPABILITIES.md](CAPABILITIES.md). -Транзакционные обработчики в `server/hooks` и настройки в `server/settings.ts` -описаны в [HOOKS.md](HOOKS.md). Регистрация в `plugin.ts` не требуется. +Routes use /extensions/<namespace>/<id>. The host adds pages under Applications, shows their title, and uses the current layout. IDs must be URL-safe and unique within each extension kind; a page/panel may share an ID. + +PluginPageProps.request is the same namespace-limited HTTP client. Shared Button/Textarea come from Kit. The shell isolates render errors. Authenticated /extensions supplies enabled packages to layout, menu, pages, and panels. + +Users must satisfy existing admin admission rules. Separate page permissions are unsupported; each plugin API enforces its domain rights. Missing/disabled plugins are hidden. Enabling requires rebuilding/restarting UI and Core. + +Only fixed pages are supported, without nested/dynamic routes. Pages also receive preparedAction/onStateChange. UseAction(contract, props, { path, initialInput }) connects form schema, ordinary requests, prepared results, and dirty/busy host state. Overview reads only the current caller's context. See [field editors](FIELDS.md). + + + +## Actions and assistant + +Examples/plugins/calculator declares input/output in TypeScript. Its static POST handler uses defineModelContext<CalculationInput>(defineHandler(...), annotate), with defineModelAnnotation and AccessGate.authenticated. + +Builder/source loader produce .asmblyr/models for runtime validation/UI; enable resolveJsonModule for JSON imports. File discovery registers HTTP; Core exposes annotated handlers to internal MCP. `plugin.ts` stays definePlugin({}). + +HTTP/MCP share one restricted handler context. `useActionContext` exposes actor, superuser, signal, and items. UseItems works in ordinary endpoints too, checking actions/fields. MCP additionally checks publication of collections/relations. Model handlers receive no privileged storage. ReadOnly:true blocks all items writes. + +Forms use useAction<Input, Output>(generatedModel, props, options), deriving browser/server schemas from the same types without importing server handlers. See [action contracts](../../docs/development/plugin-actions.md). + + + +## Capabilities, hooks, and settings + +Declare manifest capabilities and approve them in asmblyr.pluginPermissions. See [capabilities](CAPABILITIES.md). Transactional server/hooks and server/settings.ts are covered in [hooks/settings](HOOKS.md). No registration in plugin.ts is required. diff --git a/packages/kit/LIFECYCLE.ru.md b/packages/kit/LIFECYCLE.ru.md new file mode 100644 index 0000000..79b716f --- /dev/null +++ b/packages/kit/LIFECYCLE.ru.md @@ -0,0 +1,204 @@ + + +# Миграции, хранилище и UI плагина + + + +[English](LIFECYCLE.md) · [Русский](LIFECYCLE.ru.md) + + + + + +## Миграции + +Файлы `server/migrations/YYYYMMDDHHmmss_name.ts` экспортируют `defineMigration()`. +Нужен export `"./migrations": "./dist/migrations.json"` в package.json. + +```ts +import { defineMigration } from "@asmblyr-collaborative/kit"; + +export default defineMigration({ + operations: [ + { + type: "addField", + collection: "entries", + name: "author_id", + field: { type: "uuid", required: false, nullable: true }, + }, + { + type: "addIndex", + collection: "entries", + name: "by_author", + fields: ["author_id"], + }, + ], +}); +``` + +Одновременно обновите актуальное описание в `server/collections`. Миграции содержат +неизменяемые JSON-планы: контрольная сумма не зависит от компиляции TS в JS. +Не импортируйте изменяемое актуальное описание в старую миграцию: её план должен +оставаться прежним. Есть `addField` и обычный составной B-tree `addIndex`. +SQL, `drop`, смены типов, переименований, backfill и автоматического down пока нет. +NOT NULL без default на заполненной таблице завершится ошибкой PostgreSQL и откатом. + +Core под общей блокировкой запускает новые планы по имени файла. Операции принимают +только локальные имена собственных коллекций. Реестр `asmblyr_plugin_migrations` +хранит имя, SHA-256 плана, время и признак baseline. Изменение/удаление выполненного +плана и добавление плана раньше уже применённого запрещены. Ошибка операции или +несовпадение итоговой декларации откатывает всю установку: DDL, метаданные и журнал. + +При новой установке таблица создаётся по актуальному описанию, addField проверяется, +индексы создаются, планы отмечаются как baseline. Новая таблица в существующем +плагине также получает текущую схему. Удаление деклараций пока не поддержано. +Данные не удаляются при отключении. Down реестра запрещён при наличии истории. +Прямые изменения схемы через SQL автоматически не согласуются с декларациями. + + + +## Собственное хранилище + +`storage` доступен внутри аутентифицированного обработчика пакета с namespace +и разрешённой возможностью `storage.own`. +Методы `list/get/create/update/delete` имеют формат `items`, но принимают локальное +имя, например `storage.list("entries")`. Область доступа ограничена коллекциями +этого пакета. Нет SQL, Knex, `commit`, смены actor или получения чужого namespace. +Применяются валидация Core, история с реальным автором и транзакции записи. + +Это **право доверенного серверного плагина на своё хранилище**. Обычный `items` +по-прежнему использует права пользователя. Плагин обязан проверить предметные +условия до `storage`: comments сначала читает обсуждаемую запись через `items.get`, +потом обращается к своим комментариям. Grants на таблицу комментариев не нужны. + +Прямой `/items/plugin_…` остаётся административным путём с обычными grants; выдача +таких grants разрешает работу с таблицей в обход специализированного endpoint. +Для comments выдавать их участникам не нужно. Код плагина доверенный и выполняется +в процессе Core, без песочницы. Пользовательские таблицы через storage недоступны. + + + +## Типизированное хранилище + +Для типизированного доступа к собственной коллекции используйте её декларацию: + +```ts +import { useStorage } from "@asmblyr-collaborative/kit"; +import entries from "./server/collections/entries.js"; + +const comments = useStorage(context, entries); +const row = await comments.get(id); +// row.body, row.author_id и остальные поля выведены из entries. +``` + +`get/create/update` возвращают саму строку, `list` — `{ data, labels, page }`. +Имена и типы полей, обязательность при создании, nullable и ручной первичный ключ +выводятся из `defineCollection`. Дефолты позволяют пропускать поле при создании. +Генерируемые ключи и системные даты не принимаются в типизированной записи. +Даты нормализуются в ISO-строки, decimal/bigint сохраняются строками. Несовпадение +типов прочитанной строки с декларацией даёт ошибку вместо неявного преобразования. +Проверки записи и область доступа остаются у Core. Этот API читает полные строки; +для проекций используйте исходный `context.storage`. Не сохраняйте его между запросами. + + + +## Вкладки редактора записи + +`ui/index.ts` экспортирует `defineUiPlugin({ recordPanels: [...] })` из +**`@asmblyr-collaborative/kit/ui`**. Package.json экспортирует его сборку как `./ui`. +Корневой entry kit и `plugin.ts` в браузер не импортируются. +У вкладки есть `id`, `title`, React `component` и необязательный `supports(record)`. + +Host передаёт `RecordPanelProps`: + +- `record`: техническое имя коллекции, сохранённый ID и отображаемое имя; +- `request(path, init)`: запрос через сессию UI в `/api//…`; +- `onStateChange({ dirty, busy })`: защита текста и выполняющейся записи от закрытия. + +Компоненты shadcn импортируются напрямую из `@asmblyr-collaborative/kit/ui/`. +Админка и плагины используют одну реализацию из Kit, включая полные props и ref. +Дополнительный provider или prop `components` не требуется. +Подробнее: [общие компоненты UI](UI.ru.md). + +Вкладка монтируется при первом открытии и сохраняется при смене вкладок. +Сбой рендера изолируется от формы записи. Новой несохранённой записи вкладки не +предоставляются. Активные пакеты сверяются с аутентифицированным `/extensions`. + +Next при запуске/сборке создаёт статические импорты из exports включённых пакетов +и источники Tailwind для их UI. Локальная разработка читает `ui/index.ts`, production +и npm — собранный `./ui`. Изменение списка пакетов требует перезапуска dev-сервера; +production требует пересборки UI. Изменения TSX используют обычный Fast Refresh. +React — peer dependency. Для UI используйте `.ts`/`.tsx` в относительных импортах +и `rewriteRelativeImportExtensions: true` в tsconfig: TypeScript заменяет их на +`.js` при сборке. Добавьте `ui/**/*` и `shared/**/*` в include. + +Первый полный пример — `packages/plugin-comments`. Серверные пакеты нельзя импортировать в UI. + + + +## Отдельные страницы + +Второй пример — `examples/plugins/overview`. Браузерный entry может экспортировать +страницы, вкладки записи или оба вида расширений: + +```ts +import { defineUiPlugin } from "@asmblyr-collaborative/kit/ui"; +import { OverviewPage } from "./pages/overview-page.tsx"; + +export default defineUiPlugin({ + pages: [{ id: "home", title: "Обзор", component: OverviewPage }], +}); +``` + +Маршрут: `/extensions//`. Host добавляет страницу в группу +«Приложения» бокового меню, отображает её название в шапке и использует текущий +layout. ID проверяются на допустимость в URL и уникальность внутри каждого вида +расширений. Страница и вкладка одного пакета могут иметь одинаковый ID. + +`PluginPageProps` предоставляет `request: PluginRequest` — тот же ограниченный +namespace HTTP-клиент, что у вкладок. `Button` и `Textarea` доступны из kit. +Общая оболочка изолирует ошибки отрисовки от остального интерфейса. +Список активных пакетов загружается для layout через аутентифицированный +`/extensions` и используется страницами, меню и вкладками записи. + +Пользователь должен войти и иметь доступ к админке по её существующим правилам. +Отдельные разрешения на UI-страницы пока не реализованы; API каждого плагина обязан +сам проверять предметные права. Отсутствующий или отключённый плагин не отображается. +Включение пакета требует сборки/перезапуска UI и Core. + +Пока поддержаны фиксированные страницы без вложенных и динамических маршрутов. +Страница получает также `preparedAction` и `onStateChange`. +`useAction(contract, props, { path, initialInput })` использует обычный `request` +и связывает схему формы с серверным действием, подготовленным результатом +и признаками dirty/busy для host. Обзор только читает собственный контекст пользователя. +Редакторы полей описаны в [FIELDS.md](FIELDS.ru.md). + + + +## Действия и ассистент + +Первый пример — `examples/plugins/calculator`. TypeScript-интерфейсы описывают поля и результат. +H3 handler в `server/api/calculator/calculate.post.ts` обёрнут в +`defineModelContext(defineHandler(...), annotate)`. +`defineModelAnnotation` описывает назначение и `middleware: AccessGate.authenticated`. +Сборщик и source loader создают `.asmblyr/models` для runtime-валидации и UI; +для импорта JSON формой нужен `resolveJsonModule: true` в tsconfig. +Сканер подключает HTTP-маршрут по файлу; Core публикует явно отмеченные действия +во внутреннем MCP. `plugin.ts` остаётся `definePlugin({})`. +HTTP и MCP вызывают один handler с одним ограниченным контекстом: +`useActionContext(event)` предоставляет actor, superuser, signal и items. +`useItems(event)` работает и в обычных endpoints: проверяет права действия и полей. +Для MCP также проверяется доступность коллекций и связей в MCP. Привилегированного +storage в model handler нет. `readOnly: true` запрещает все методы записи items. +Форма использует `useAction(generatedModel, props, options)`; +схемы браузера и сервера получены из одних типов, серверный handler в UI не попадает. +Подробности и текущие ограничения — [действия плагинов](../../docs/ru/development/plugin-actions.md). + + + +## Возможности, hooks и настройки + +Пакет объявляет capabilities в manifest; проект разрешает их через +`asmblyr.pluginPermissions`. Правила и ограничения: [CAPABILITIES.md](CAPABILITIES.ru.md). +Транзакционные обработчики в `server/hooks` и настройки в `server/settings.ts` +описаны в [HOOKS.md](HOOKS.ru.md). Регистрация в `plugin.ts` не требуется. diff --git a/packages/kit/README.md b/packages/kit/README.md index 03db895..fad77d3 100644 --- a/packages/kit/README.md +++ b/packages/kit/README.md @@ -1,18 +1,20 @@ # @asmblyr-collaborative/kit -Типизированный API для описания плагинов Asmblyr. + -[Возможности и разрешения](CAPABILITIES.md) · [Hooks и настройки](HOOKS.md) +[English](README.md) · [Русский](README.ru.md) -Плагины также могут предоставлять [редакторы текстовых полей](FIELDS.md) -с настройками и отображением в таблице. Пример — `examples/plugins/color`. + -`@asmblyr-collaborative/kit` содержит серверный контекст, H3-обработчики и сборщик плагинов. -HTTP-клиент для браузера и Node находится в отдельном [@asmblyr-collaborative/sdk](../sdk/README.md). -Общие типы запросов и ответов принадлежат `@asmblyr-collaborative/contracts`; -kit переэкспортирует типы, необходимые авторам обработчиков. +Typed API for Collaborative plugins. -Обязательный `plugin.ts` в корне пакета пока остаётся пустым описанием: +[Capabilities and permissions](CAPABILITIES.md) · [Hooks and settings](HOOKS.md) + +Plugins can also supply [text-field editors](FIELDS.md), field settings, and table displays. See examples/plugins/color. + +Kit contains server context, H3 handlers, and the plugin builder. The browser/Node HTTP client is the separate [SDK](../sdk/README.md). Shared request/response types belong to @asmblyr-collaborative/contracts; Kit re-exports those needed by handler authors. + +The required root plugin.ts currently stays minimal: ```ts import { definePlugin } from "@asmblyr-collaborative/kit"; @@ -20,17 +22,13 @@ import { definePlugin } from "@asmblyr-collaborative/kit"; export default definePlugin({}); ``` -Маршруты обнаруживаются по файлам в `server/api`. Перечислять их в `plugin.ts` -не нужно. Дополнительные возможности этого файла определим отдельно. +Routes are discovered under server/api and are not listed manually in plugin.ts. The same principle applies to the assistant/MCP: defineModelContext<Input> connects an existing H3 handler to defineModelAnnotation. The builder derives schemas from TypeScript. See [plugin actions](../../docs/development/plugin-actions.md). -Тот же принцип действует для ассистента и MCP: `defineModelContext` связывает -существующий h3-обработчик с `defineModelAnnotation`. Сборщик выводит схемы из типов -TypeScript; описание хранится рядом с обработчиком. Контракт, пример и правила проверки — -в [действиях плагинов](../../docs/development/plugin-actions.md). + -## Обработчик +## Handlers -Файл `server/api/comments/status.get.ts`: +server/api/comments/status.get.ts: ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -42,40 +40,21 @@ export default defineHandler((event) => { }); ``` -- `defineHandler` — реэкспорт настоящего обработчика H3 **2.0.1-rc.32**. На входе - `H3Event`; HTTP-утилиты можно импортировать из `h3`, добавив ту же закреплённую - версию в зависимости своего пакета. H3 v1 с этим контрактом несовместим. -- `useAsmblyr(event)` возвращает проверенный контекст: `actor` (`id`, `kind`, - необязательный `displayName`), `requestId`, `logger`, `items` и разрешённые возможности пакета. - `storage` требует namespace и `storage.own`; `displayName` — `identity.profile`. Вызов без контекста Core завершается ошибкой. - Возможность `notifications` добавляет подписки и публикацию событий своего - namespace в личные входящие. Core выбирает получателей и проверяет права на запись. - См. [контракт возможностей](./CAPABILITIES.md). -- Core проверяет access token пользователя или сервисного аккаунта до чтения тела - и запуска обработчика. Анонимных endpoints нет. `Authorization` в `event.req` - не передаётся; Core также удаляет cookies сессии и провайдеров из запроса к плагину. -- Параметры и query читаются функциями H3: `getRouterParam`, `getQuery`, - `getValidatedQuery`. Для декодирования параметра используйте `{ decode: true }` - один раз. Тело читается через `event.req.json()`, `.text()`, `.formData()` или - `readValidatedBody`. Generic-параметр сам по себе не проверяет входные данные. -- Можно возвращать объект, строку, бинарные данные, `Response`, поток или Promise. - Статус и заголовки задаются через `event.res` либо в `Response`. - Строка теперь отправляется как текст; для JSON-строки нужен `Response.json(value)`. - `null`/`undefined` означают пустое тело. Объекты остаются JSON. -- `HTTPError` из H3 поддерживается. Сохранён и `EndpointError` для ошибок с кодом - Asmblyr. Ошибки имеют общий формат `{ code, message, requestId }`; подробности - серверных ошибок остаются в журнале. Клиентские HTTPError сохраняют свои headers. -- Вход не означает доступ к данным коллекций. `items` проверяет право нужного - действия на коллекцию и доступ к её полям через общую с `/items` логику Core. -- `useItems(event)` — общий доступ к этому сервису в обычном endpoint и в model - handler. В model handler `useActionContext(event)` также даёт actor, superuser и - signal; полного контекста `useAsmblyr` и привилегированного storage там нет. - `AccessGate.authenticated` разрешает вход, а права данных проверяет каждый метод - items. У MCP дополнительно учитывается публикация всех затронутых коллекций. - -Это API доверенных серверных пакетов, выполняемых в процессе Core, без песочницы. - -## Чтение данных +- `defineHandler` re-exports H3 2.0.1-rc.32 and receives H3Event. Import HTTP utilities from h3 with the same pinned dependency. H3 v1 is incompatible. +- `useAsmblyr`(event) returns validated actor (id, kind, optional displayName), requestId, logger, items, and approved capabilities. Storage needs namespace/storage.own; displayName needs identity.profile. Calls outside Core context fail. +- Notifications capability adds subscriptions and inbox publication within the plugin namespace. Core selects recipients and checks record access. See [capabilities](./CAPABILITIES.md). +- Core authenticates humans/services before reading the body or invoking handlers. There are no anonymous endpoints. Authorization and session/provider cookies are removed from plugin requests. +- Read parameters/query with H3 getRouterParam, getQuery, or getValidatedQuery. Decode a parameter once with decode:true. Read bodies with event.req.json/text/formData or readValidatedBody. Generics do not validate input. +- Return objects, strings, bytes, Response, streams, or Promises. Set status/headers through event.res or Response. Strings are text; JSON strings require Response.json(value). Null/undefined mean an empty body; objects remain JSON. +- H3 HTTPError and Collaborative EndpointError are supported. Errors share code/message/requestId; server details stay in logs. Client HTTPErrors retain headers. +- Authentication grants no collection access. Items checks actions/fields through Core's shared /items logic. +- UseItems(event) works in ordinary endpoints and model handlers. `useActionContext` adds actor, superuser, and signal, but excludes full useAsmblyr and privileged storage. AccessGate authenticates; each items method authorizes data. MCP additionally checks publication of all affected collections. + +These are trusted in-process server packages, not sandboxed code. + + + +## Reading data ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -92,107 +71,66 @@ export default defineHandler(async (event) => { }); ``` -Одна запись: `await items.get("articles", 4, { fields: ["title"] })`. -`items` из kit вызывает общий сервис Core внутри процесса, без дополнительного HTTP-запроса. - -| Метод | Результат | -| ------------------------------------- | ------------------------------------------------------------------ | -| `items.list(collection, options?)` | `{ data, labels, page: { number, size, total, sort, direction } }` | -| `items.get(collection, id, options?)` | `{ data, label }`; отсутствующая запись — ошибка 404 | - -Контракты `ItemsReader`, `ItemListOptions`, `ItemReadOptions`, `ItemListResult`, -`ItemResult`, `ItemRecord` и типы фильтров экспортируются из `@asmblyr-collaborative/kit`. - -- `collection` и `fields` используют технические имена. Выбор полей относится - к физическим столбцам, включая внешние ключи и системные даты. Разворачивания - связанных записей и выбора виртуальных полей через `fields` пока нет. -- Без `fields` возвращаются все доступные поля. `fields: []` возвращает только - первичный ключ; ключ всегда включён. `*` указывать не нужно. Недоступное поле — - ошибка 403, неизвестное — 400. Проекция выполняется в SQL. -- Сортировка, поиск и фильтры проверяются по полному набору разрешённых полей, - независимо от `fields`. Например, можно вернуть только `id`, сортируя по - доступному `title`. `labels`/`label` тоже могут использовать доступные поля вне - проекции, но не закрытые поля своей или связанной коллекции. -- `page` начинается с 1, `limit` — от 1 до 100 (по умолчанию 100), `total` — строка. - Это те же ограничения, что у HTTP API. Без `sort` используется первичный ключ. -- `id` — строка или безопасное целое JS. Большие `bigserial` передавайте строками; - в результате они также остаются строками. Поля имеют тип `unknown`, поскольку - схемы динамические. Календарный `date` всегда остаётся строкой `YYYY-MM-DD`, - обычный `bigint` — точной десятичной строкой. Внутри обработчика `datetime` может быть `Date`; - при JSON-ответе они сериализуются в ISO-строки, как в `/items`. -- Поддерживаются `q` и объект `filter` с теми же операторами и проверками, что у - HTTP API. Значения условий — строки или массивы строк, включая числа и boolean. - Фильтры по связям ограничены одним переходом и требуют прав на обе стороны. - Действуют общие лимиты: 8192 символа JSON, 3 уровня групп, 20 условий, 30 узлов. -- Права загружаются один раз при входе в endpoint. Изменение политик и отзыв - сессии/сервисного ключа учитываются в следующем запросе. Не сохраняйте `items` - в глобальной переменной и не передавайте в фоновые задачи: это контекст запроса. -- Переопределение actor, `superuser` или прав не предусмотрено. Неизвестные опции - чтения отклоняются. В kit нет доступа к Knex, токенам или системным - таблицам через `items`. Код плагина остаётся доверенным кодом процесса. -- Условия permissions проверяются Core: до count/пагинации, отдельно от фильтра - вызывающей стороны. Поля проецируются по сработавшим веткам; изменения проверяют - исходную и итоговую запись. См. [права и границы первой версии](../../docs/features/access.md). - Пользовательский `filter` сужает выдачу и не заменяет такую политику. - Статусы записей автоматически не отфильтровываются: как в `/items`, нужное - условие передаёт вызывающая сторона. - -Пример фильтра по доступному полю связанной коллекции: +Read one record with items.get("articles", 4, { fields: ["title"] }). Kit calls Core's in-process service without extra HTTP. + +| Method | Result | +| ----------------------------------- | ----------------------------------------------------------------------- | +| items.list(collection, options?) | { data, labels, page: { number, size, total, sort, direction, order } } | +| items.get(collection, id, options?) | { data, label }; missing records return 404 | + +Kit exports ItemsReader, ItemListOptions, ItemReadOptions, ItemListResult, ItemResult, ItemRecord, and filter types. + +- Collection/fields use technical names. Selection covers physical columns, foreign keys, and timestamps, without relation expansion or virtual fields. +- Omitted fields returns all permitted fields. An empty list returns only the primary key, which is always included. No "\*" is needed. Closed fields yield 403; unknown fields 400. SQL performs projection. +- Sorting/search/filtering use all permitted fields independently of projection. You can return id while sorting by accessible title. Labels may use permitted fields outside selection, never closed fields in source/related collections. +- Page starts at 1; limit is 1–100, default 100; total is a string. Without search or explicit sorting, the primary key orders results. Search relevance follows Core's order option. +- IDs are strings or safe JS integers. Large bigserial IDs remain strings. Dynamic fields have unknown type. Date stays YYYY-MM-DD, bigint stays an exact decimal string. Datetime may be Date inside handlers, becoming ISO on JSON serialization. +- Q/filter use HTTP operators and checks. Values are strings/string arrays, including numbers/booleans. Related filters allow one hop with access to both sides. Limits: 8192 JSON characters, 3 group levels, 20 conditions, 30 nodes. +- Grants load at endpoint entry. Policy/session/key revocation applies on the next request. Never store items globally or use it in background tasks. +- Actors, superuser, and grants cannot be overridden. Unknown read options are rejected. Items exposes no Knex, tokens, or system tables; plugin code remains trusted. +- Core applies row permissions before count/pagination, separately from caller filters. Matching rules control projection and original/final write checks. See [access boundaries](../../docs/features/access.md). Record statuses are not implicitly filtered: callers supply the condition as with /items. + +Example with a readable related field: ```ts const result = await items.list("articles", { fields: ["title"], filter: { logic: "and", - children: [{ field: "author_id.name", op: "eq", value: "Анна" }], + children: [{ field: "author_id.name", op: "eq", value: "Anna" }], }, }); ``` -Ошибки чтения проходят через общую обработку Core: 400 — некорректный запрос, -403 — нет разрешения, 404 — коллекция или запись не найдена. До запуска endpoint -невалидный access token даёт 401. Ошибка не превращается в пустую выдачу. +Errors remain errors: 400 invalid request, 403 denied, 404 missing collection/record, and 401 invalid token before invocation. They never become empty results. + +HTTP uses the same projection: GET /items/articles?fields=id,title and GET /items/articles/4?fields=title. Omitting fields preserves its response format. -HTTP API использует ту же проекцию: `GET /items/articles?fields=id,title` и -`GET /items/articles/4?fields=title`. Без параметра формат ответа сохранён. + -## Запись данных +## Writing data ```ts const { items } = useAsmblyr(event); -const result = await items.create("articles", { title: "Статья" }); -await items.update("articles", 4, { title: "Новое название" }); +const result = await items.create("articles", { title: "Article" }); +await items.update("articles", 4, { title: "New title" }); await items.delete("articles", 4); ``` -Контекст предоставляет `ItemsService`, включающий чтение и запись. `ItemsReader` -остаётся отдельным типом для функций, которым требуется только чтение. -Входные значения — JSON-объекты; правила полей и отношений проверяет Core. - -- `create`/`update` возвращают `{ data }` только с читаемыми полями. Если права - чтения нет, успешный результат — `{ data: null }`. `delete` возвращает `void`. -- Core использует общую с HTTP API логику в `items/writer.ts`: права, проверку - связанной коллекции, валидацию, запись и проекцию результата. -- Каждая операция и её история выполняются в транзакции. Автор истории берётся - из текущей сессии пользователя или сервисного аккаунта. ID операции истории - генерирует Core; это UUID, отдельный от HTTP `requestId`. -- Несколько вызовов не образуют одну транзакцию. Для записи со связями есть - `items.commit(collection, draft)`, использующий существующий механизм черновиков. - Возвращает `{ data: { id } }`. Сбой откатывает весь черновик, включая историю. -- `ItemCommitDraft` содержит values/references/relations/records, до 100 изменений - и глубины 5. Для изменения существующей записи через commit нужно и чтение. - `expectedValues` в каждой изменяемой записи включает исходные значения всех - обновляемых полей, включая FK. Core проверяет их под блокировкой строки; - несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. - Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. - Полный формат общий с [HTTP SDK](../sdk/README.md#запись-и-сохранение-связей). -- Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, - 409 — конфликт связи/ключа или вторая запись в single-коллекции. - -Собственные коллекции плагина устанавливаются Core при первом запуске. -Формат описания и ограничения обновления приведены ниже. - -### Пример POST с проверкой тела +ItemsService includes reads/writes; ItemsReader remains a separate type for read-only consumers. Inputs are JSON objects validated by Core's field/relation rules. + +- Create/update return readable data, or data:null on successful write without read access. Delete returns void. +- Shared items/writer.ts performs authorization, related-collection checks, validation, storage, and projection. +- Each operation/history is transactional. The caller supplies authorship; Core generates a separate UUID history-operation ID, distinct from HTTP requestId. +- Multiple calls do not share a transaction. Items.commit(collection, draft) atomically saves linked drafts, returning the root ID and rolling back all changes/history on failure. +- ItemCommitDraft contains values/references/relations/records, capped at 100 changes/depth 5. Existing records in commits also require reads. ExpectedValues on each modified record includes baselines for updated fields/FKs; Core checks under row locks. Mismatch returns ITEM_CHANGED (409) and rolls back everything. Compared fields need read/update; omission preserves legacy behavior. See [SDK writes](../sdk/README.md#writes-and-relationships). +- Errors: 400 invalid values/fields, 403 access, 404 missing record, 409 relationship/key conflict or a second singleton record. + +Core installs plugin-owned collections on first startup; declaration/update rules follow below. + + + +### POST body validation ```ts import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; @@ -208,58 +146,42 @@ export default defineHandler(async (event) => { }); ``` -### Граница HTTP + + +### HTTP boundary + +Fastify routes requests. An isolated parser reads raw bodies up to 1 MiB; H3 parses once. Multipart fits within that limit; large streaming uploads are unsupported. Responses stream through Fastify without JSON conversion/full buffering. Disconnect reaches event.req.signal. + +Core preserves content type, status, plugin cookies, and end-to-end headers. Cookie names prefixed asmblyr\_ or SESSION_COOKIE_PREFIX are reserved; plugins cannot read/replace session/provider cookies. Redirects go to the browser without a server-side follow carrying Core credentials. Use public Location values such as /api/comments/status. -Fastify продолжает маршрутизировать запросы. Изолированный parser читает исходные -байты тела с лимитом **1 МиБ**; H3 разбирает их один раз. Multipart поддерживается -в этом лимите, потоковая загрузка больших файлов пока не реализована. -Ответы идут потоком напрямую через Fastify без преобразования в JSON и без полной -буферизации. Отключение клиента передаётся в `event.req.signal`. +There is no intermediate UI proxy timeout; clients control cancellation. All plugin responses use Cache-Control:no-store. -Core сохраняет content-type, статусы, cookies плагина и end-to-end headers. -Имена cookies с префиксом `asmblyr_` и настроенным `SESSION_COOKIE_PREFIX` -зарезервированы; плагин не читает и не заменяет cookies сессии или провайдеров. -Редирект передаётся браузеру без серверного перехода с токеном Core. -В `Location` следует использовать публичный адрес, например `/api/comments/status`. -Промежуточного UI-прокси и его таймаута нет; клиент управляет отменой запроса. -Все ответы плагинов имеют `Cache-Control: no-store`. +This integrates H3 handlers, not a full Nitro server. Event.app, raw Node req/res, WebSocket upgrades, and background lifecycle are unavailable. H3's definePlugin is a different API from Collaborative's package declaration. -Это интеграция обработчиков H3, а не полноценный Nitro-сервер. `event.app`, -сырой Node req/res, WebSocket upgrades и фоновый lifecycle пока не предоставляются. -`plugin.ts` по-прежнему описывает пакет Asmblyr; `definePlugin` из `h3` — другое API. + -## Адреса +## Addresses -Путь определяется относительно `server/api`, метод — суффиксом файла: +Paths are relative to server/api; suffixes determine methods. -| Файл внутри `server/api` | Публичный маршрут на адресе админки | -| ------------------------------------ | ----------------------------------- | -| `comments/index.get.ts` | `GET /api/comments` | -| `comments/index.post.ts` | `POST /api/comments` | -| `comments/[id].delete.ts` | `DELETE /api/comments/:id` | -| `comments/[id]/replies/index.get.ts` | `GET /api/comments/:id/replies` | +| File under server/api | Public admin-domain route | +| ---------------------------------- | ----------------------------- | +| comments/index.get.ts | GET /api/comments | +| comments/index.post.ts | POST /api/comments | +| comments/[id].delete.ts | DELETE /api/comments/:id | +| comments/[id]/replies/index.get.ts | GET /api/comments/:id/replies | -Core использует те же пути без общего префикса `/api`. Имя npm-пакета в URL не -подставляется. Поддерживаются `.get.ts`, `.post.ts`, `.put.ts`, `.patch.ts` и -`.delete.ts`. Файл без суффикса метода, например `comments/status.ts`, обслуживает -все пять методов. Каждый файл должен экспортировать обработчик по умолчанию. +Core also uses these paths without /api. Npm package names are not inserted in URLs. Supported suffixes: get/post/put/patch/delete.ts. A file without a method suffix serves all five. Every route default-exports a handler. -Вспомогательные функции размещаются вне `server/api`. Файлы `.d.ts` не считаются -маршрутами. Необязательные параметры, catch-all, составные параметры в сегменте -и отдельные HEAD/OPTIONS handlers пока не поддерживаются. +Keep helpers outside server/api. Declaration files are not routes. Optional parameters, catch-all, compound path segments, and separate HEAD/OPTIONS handlers are unsupported. -Первый сегмент пути принадлежит плагину. Core отклоняет пересечения с собственными -разделами, другими плагинами и дубли маршрутов. Сборка также отклоняет неоднозначные -файлы, например `[id].get.ts` и `[key].get.ts` в одном каталоге. -UI автоматически проксирует пути плагинов, используя -свою сессию и существующую проверку Origin для изменений. +The first path segment belongs to the plugin. Core rejects collisions with built-in sections, other plugins, and duplicate routes. Builds reject ambiguous files such as [id].get.ts and [key].get.ts in one directory. Browser calls use the public API with the current session and the existing mutation Origin checks. -## Коллекции плагина + -В `server/collections/entries.ts` плагина comments есть пример с -`defineCollection()` из kit. Core проверяет описание и при первом запуске создаёт -таблицу вместе с метаданными. Файлы лежат непосредственно в `server/collections`, -имя файла совпадает с локальным именем коллекции. Подкаталоги и симлинки запрещены. +## Plugin collections + +Comments' server/collections/entries.ts demonstrates defineCollection. Core validates declarations and creates tables/metadata on initial startup. Files sit directly under server/collections and match local collection names. Subdirectories/symlinks are forbidden. ```ts import { defineCollection } from "@asmblyr-collaborative/kit"; @@ -268,82 +190,51 @@ export default defineCollection({ name: "entries", primaryKey: { name: "id", type: "uuid" }, timestamps: { createdAt: true, updatedAt: true }, - presentation: { displayName: "Комментарии", hidden: true }, + presentation: { displayName: "Comments", hidden: true }, fields: { body: { type: "text", required: true, nullable: false, - presentation: { label: "Комментарий", interface: "textarea" }, + presentation: { label: "Comment", interface: "textarea" }, }, }, }); ``` -- `name` — локальное имя внутри плагина. Namespace задаётся в манифесте, - а Core формирует имя таблицы: `plugin__`, например `plugin_comments_entries`. -- `primaryKey` и `timestamps` используют общие с Core типы из contracts. - Ключ и системные даты не нужно повторять в `fields`. -- Ключи объекта `fields` — технические имена полей. Порядок объявления задаёт - порядок создания столбцов. Метаданные полей сохраняются при установке. -- `required` — обязательность непустого значения в API; `nullable` — допустимость - SQL NULL в БД. Оба параметра явные и независимые: `true` / `true` допустимо. -- Типы полей соответствуют Core. Тип значения `defaultValue` зависит от поля; - `date` и `bigint` используют строки, включая `CollectionRow` и входы storage. - для `file` / `files` дефолт не поддерживается. `searchable` доступен для `text` - и `email`; создание индекса это свойство не описывает. -- `presentation` поля использует существующий `FieldPresentation` из contracts; - можно указать только нужные настройки. У коллекции пока доступны `displayName` - и `hidden`. Скрытие из навигации не заменяет права или защиту структуры. -- `defineCollection` возвращает описание без побочных действий. Это не runtime- - валидатор: допустимость имён, UUID/дат/чисел, сочетаний настроек интерфейса и - конфликтов полей проверяет загрузчик Core через общие валидаторы коллекций. - -Пакет с коллекциями задаёт `asmblyr.manifest.namespace`, например `comments`, -и экспортирует `"./collections": "./dist/collections.json"`. Namespace — стабильное -имя из 1–31 строчных латинских букв, цифр и подчёркиваний, начиная с буквы. -Он уникален между установленными пакетами; `asmblyr` и префиксы `asmblyr_` / `plugin_` -зарезервированы. Полное имя таблицы должно умещаться в 63 символа PostgreSQL. -Сборщик создаёт индекс деклараций. В dev Core сканирует исходники локального пакета, -в production читает индекс и скомпилированные модули. `plugin.ts` остаётся пустым. - -Core хранит namespace и npm-владельца в `asmblyr_plugins`, а локальное имя, -физическое имя и нормализованное описание — в `asmblyr_plugin_collections`. -Реестр создаётся обычной миграцией Core (`pnpm db:migrate`). При старте установка -всех включённых коллекций проходит в одной транзакции под общей блокировкой. -Чужие таблицы и метаданные не присваиваются. Повторный запуск сохраняет данные. - -Изменения установленного описания должны быть воспроизведены явными миграциями -из `server/migrations`. Несовпадение итогового описания останавливает запуск. -Новые коллекции добавляются автоматически. Исчезнувшая -таблица не создаётся заново поверх реестра. Прямые изменения структуры через SQL -не синхронизируются автоматически. Отключение плагина сохраняет таблицы и владельца. - -Префикс `plugin_` зарезервирован в редакторе структуры и API независимо от того, -включён ли пакет. Обычные пользователи и superuser не меняют структуру и настройки -таких коллекций. Строки доступны через `/items` и kit по обычным правам, включая -историю изменений; отключение кода плагина само по себе эти права не отзывает. -MCP при установке выключен. Связи между коллекциями плагина пока не поддержаны -декларациями. Формат пока экспериментальный. - -## Миграции, хранилище и UI - -Полный контракт и ограничения описаны в [жизненном цикле плагина](./LIFECYCLE.md). -Пример со всеми тремя возможностями — `packages/plugin-comments`. - -## Пакет и подключение - -Имя, версия и зависимости плагина остаются в его `package.json`. -`asmblyr.manifest.version` задаёт версию формата манифеста, а не версию kit. - -Пакет плагина экспортирует собранный модуль по `exports["."].default`, типы по -`exports["."].types`, метаданные по `exports["./package.json"]` и сгенерированный -`dist/routes.json` по `exports["./routes"]`. Пакеты с namespace также экспортируют -`dist/collections.json` по `exports["./collections"]`. -Пример настройки находится в `packages/plugin-comments/package.json`. - -Пакеты устанавливаются как зависимости корневого проекта и явно включаются -в его `package.json`: +- Name is local to the plugin. Manifest namespace produces plugin*<namespace>*<name>, such as plugin_comments_entries. +- PrimaryKey/timestamps use shared Contracts types; do not duplicate them in fields. +- Field keys are technical names; declaration order determines column creation order. Installation persists metadata. +- Required means nonempty API input; nullable permits SQL NULL. Both are explicit/independent, including true/true. +- Types match Core. DefaultValue depends on type; date/bigint use strings, including CollectionRow/storage input. File/files have no defaults. Searchable supports text/email and does not itself declare an index. +- Field presentation uses FieldPresentation with optional settings. Collection presentation currently supports displayName/hidden. Navigation hiding is not authorization or structural protection. +- DefineCollection is side-effect-free and not a runtime validator. Core uses shared validators for names, values, interface combinations, and field conflicts. + +Packages with collections specify a namespace and export ./collections as ./dist/collections.json. Namespace is 1–31 lowercase Latin letters/digits/underscores, beginning with a letter, unique among installed packages. Asmblyr and asmblyr*/plugin* prefixes are reserved. Full table names must fit PostgreSQL's 63-character limit. + +The builder generates the declaration index. Development scans local source; production loads indexes/compiled modules. `plugin.ts` remains empty. + +Core stores namespace/npm ownership in asmblyr_plugins and local/physical names plus normalized declarations in asmblyr_plugin_collections. Normal Core migrations create the registry. Startup installs all enabled collections in one transaction under a shared lock. Foreign tables/metadata are never adopted; restarting preserves data. + +Installed declaration changes require explicit server/migrations reproducing the new state. Mismatch stops startup. New collections install automatically. Missing registered tables are not silently recreated. Direct SQL structure edits do not synchronize automatically. Disabling a plugin preserves tables/ownership. + +Plugin\_ remains reserved in UI/API whether the package is enabled or not. Neither ordinary users nor superusers edit these collections' structure/settings. Rows use normal /items/Kit grants and history; disabling plugin code does not revoke those grants. MCP starts disabled. Declaration-based relations between plugin collections are unsupported; the format remains experimental. + + + +## Migrations, storage, and UI + +See [plugin lifecycle](./LIFECYCLE.md). Packages/plugin-comments demonstrates all three. + + + +## Package and installation + +Name, version, and dependencies remain in package.json. Asmblyr.manifest.version is the manifest format version, not Kit's version. + +Export built modules through exports["."].default, types through exports["."].types, metadata through ./package.json, and generated dist/routes.json through ./routes. Namespaced packages also export dist/collections.json through ./collections. See packages/plugin-comments/package.json. + +Install dependencies at the project root and explicitly enable them: ```json { @@ -353,53 +244,32 @@ MCP при установке выключен. Связи между колле } ``` -Core проверяет имена, версии манифестов и наличие обработчиков всех включённых -пакетов перед импортом. Ошибки включённого плагина останавливают запуск. -Сканирование происходит только внутри включённых пакетов; установка сама по себе -не включает плагин. +Core validates enabled package names, manifest versions, and handlers before imports. Enabled-plugin failures stop startup. Discovery scans enabled packages only; installing a dependency alone does not enable it. -## Разработка и сборка + -В разработке `pnpm dev` использует исходники включённых локальных пакетов из -workspace-пакетов внутри проекта. Изменение, добавление и удаление `.ts`-файлов автоматически перезапускает -Core и обновляет маршруты. Это перезапуск процесса, а не замена обработчика на лету. +## Development and build -Сборка плагина выполняется командой kit `asmblyr-plugin build` в каталоге пакета; -в comments она задана в script `build`. TypeScript использует `rootDir: "."`, -`outDir: "dist"`, включая `plugin.ts` и `server/**/*.ts`. -Для типов Web API добавлены `DOM` и `DOM.Iterable` в `lib`. В kit и примере включён -`skipLibCheck`: декларации выбранной RC содержат несовместимости с TypeScript 5.9 -и ссылки на опциональные зависимости. Проверка собственного кода остаётся строгой; -при обновлении H3 это ограничение нужно пересмотреть. -Сборщик проверяет имена маршрутов, default exports и типы, очищает собственный -`dist`, создаёт JavaScript, декларации типов и список маршрутов `dist/routes.json`. +Pnpm dev uses enabled local workspace sources. Adding/changing/removing TypeScript files restarts Core and rebuilds route discovery; it is a process restart, not hot handler replacement. -В production и для установленных npm-пакетов Core читает готовый список и загружает -JavaScript. Сканирования исходников в этом режиме нет. Удалённые исходные маршруты -не сохраняются в следующей сборке. +Run asmblyr-plugin build in the package directory, as the Comments build script does. TypeScript uses rootDir:"." and outDir:"dist", including plugin.ts and server/\*_/_.ts. DOM/DOM.Iterable provide Web API types. Kit/examples use skipLibCheck because the pinned H3 RC declarations conflict with TypeScript 5.9 and refer to optional dependencies; project code stays strictly checked. Revisit this when upgrading H3. -В репозитории Kit и comments связаны как workspace-пакеты. `pnpm build:plugins` собирает оба; -`pnpm dev` выполняет начальную сборку автоматически. `pnpm build` сначала собирает -пакеты, затем Core и UI. Публикация пакетов остаётся отдельным шагом. +The builder checks route names, default exports, and types, then generates JavaScript, declarations, and dist/routes.json in its own dist. Production/npm packages load the index and JavaScript without source scanning. Deleted routes disappear from the next build. -Проверка kit и первого плагина из корня репозитория: +Workspace builds link Kit and bundled plugins. Pnpm build:plugins builds them; pnpm dev performs initial generation. Pnpm build builds packages before Core/UI. Publishing remains separate. ```sh pnpm build:plugins pnpm --filter @asmblyr-collaborative/kit --filter @asmblyr-collaborative/plugin-comments typecheck ``` -## Локализация расширения + -Необязательные плоские JSON-каталоги `locales/ru.json` и `locales/en.json` -располагаются рядом с `package.json`. Включайте `locales/` в опубликованный пакет -вместе с `dist/`. Core читает их одинаково в source и built режиме и добавляет -в защищённый `GET /translations`. Каталоги содержат только публичные подписи: не -добавляйте в них настройки, ключи или пользовательские данные. +## Plugin localization -UI использует тот же каталог через `defineUiPlugin({ translations: { ru, en }, ... })`. -Для JSON-импортов включите `resolveJsonModule` в tsconfig. У страниц, панелей -и интерфейсов поля можно указать `titleKey`; `title` остаётся запасной подписью. +Optional flat locales/ru.json and locales/en.json sit beside package.json. Publish locales alongside dist. Core reads them in source/built modes and includes them in protected GET /translations. Catalogs contain public labels only, never settings, keys, or user data. + +UI uses defineUiPlugin({ translations: { ru, en }, ... }). Enable resolveJsonModule for JSON imports. Pages, record panels, and field interfaces support titleKey with title fallback. ```tsx import { usePluginTranslations } from "@asmblyr-collaborative/kit/ui/i18n"; @@ -410,32 +280,24 @@ function Panel() { } ``` -Хост предоставляет изолированный i18next provider на текущем языке профиля, -пространство имён `plugin.` и русский fallback. React -экранирует текст при отображении. Не импортируйте серверный entry point в UI. -Ключи `collection..label` и -`field...label|description|placeholder` задают подписи -собственных таблиц в UI и API. Для настроек используются `settings.title`, -`settings.description` и `settings..label|description`. -Имена коллекций/полей, значения записей и HTTP-контракты не переводятся. +The host supplies isolated i18next context using the profile locale, plugin.<manifest.namespace>, and Russian fallback. React escapes text. Never import the server entry in UI. + +Collection.<local-name>.label and field.<local-name>.<field>.label|description|placeholder supply owned-table UI/API labels. Settings use settings.title, settings.description, and settings.<field>.label|description. Technical names, record values, and HTTP contracts are not translated. + + + +## Personal external connections -## Личные внешние подключения +Model annotation connection:"google" makes a handler visible only with the current human's active connection. Declare connections.google and obtain explicit project approval. -Model handler с `connection: "google"` в `defineModelAnnotation` виден ассистенту только при активном подключении текущего человека. Пакет запрашивает capability `connections.google`, установка явно одобряет её в `asmblyr.pluginPermissions`. +`useActionContext`(event).connections?.google exposes owner-bound list, readText, sheet, cells, and proposeWrite. Kit exports PersonalConnections, GoogleWriteInput, GoogleFileList, GoogleText, GoogleSheet, and GoogleCells. There are no raw tokens, arbitrary URLs, or confirmation methods. `proposeWrite` saves a proposal; humans confirm actual writes in Core UI. HTTP/MCP share handlers. See packages/plugin-google-workspace and [Google Workspace](../../docs/features/google-workspace.md). -`useActionContext(event).connections?.google` предоставляет owner-bound операции `list`, `readText`, `sheet`, `cells`, `proposeWrite`. Контракты `PersonalConnections`, `GoogleWriteInput`, `GoogleFileList`, `GoogleText`, `GoogleSheet`, `GoogleCells` экспортируются Kit. Нет raw token, произвольного URL или метода подтверждения. `proposeWrite` только сохраняет предложение; реальную запись подтверждает человек в UI Core. HTTP и внутренний MCP используют одинаковый handler. Пример — `packages/plugin-google-workspace`; ограничения и настройка — [Google Workspace](../../docs/features/google-workspace.md). + ## SDK consumer contracts -Built defineModelContext handlers include generated JSON input/output schemas. -Core exports accessible handlers through GET /schema, using their existing -AccessGate and original HTTP address. The generated SDK provides -`client.plugins.namespace[methodId](input)` with inferred output. Method IDs join -route segments with hyphens. No extra SDK handler or registry is needed. +Built defineModelContext handlers include generated input/output JSON schemas. GET /schema exports accessible handlers using AccessGate and original HTTP routes. Generated SDK methods use client.plugins.namespace[methodId](input) with inferred output. IDs join route segments with hyphens; no extra registry is needed. -Contracts describe Kit's bounded JSON model subset. They do not grant data access -or imply that a personal OAuth connection is available. Execution still runs the -original gate and current Core permissions. Legacy defineAction handlers without -a generated outputSchema do not appear in the typed consumer API. +The bounded JSON contract grants no data access and implies no personal OAuth connection. Execution rechecks the original gate/current Core permissions. Legacy defineAction handlers without generated outputSchema are omitted. -[Package checks and release preparation](../../docs/reference/packages.md). +See [package checks and release preparation](../../docs/reference/packages.md). diff --git a/packages/kit/README.ru.md b/packages/kit/README.ru.md new file mode 100644 index 0000000..1c42b66 --- /dev/null +++ b/packages/kit/README.ru.md @@ -0,0 +1,471 @@ +# @asmblyr-collaborative/kit + + + +[English](README.md) · [Русский](README.ru.md) + + + +Типизированный API для описания плагинов Asmblyr. + +[Возможности и разрешения](CAPABILITIES.ru.md) · [Hooks и настройки](HOOKS.ru.md) + +Плагины также могут предоставлять [редакторы текстовых полей](FIELDS.ru.md) +с настройками и отображением в таблице. Пример — `examples/plugins/color`. + +`@asmblyr-collaborative/kit` содержит серверный контекст, H3-обработчики и сборщик плагинов. +HTTP-клиент для браузера и Node находится в отдельном [@asmblyr-collaborative/sdk](../sdk/README.ru.md). +Общие типы запросов и ответов принадлежат `@asmblyr-collaborative/contracts`; +kit переэкспортирует типы, необходимые авторам обработчиков. + +Обязательный `plugin.ts` в корне пакета пока остаётся пустым описанием: + +```ts +import { definePlugin } from "@asmblyr-collaborative/kit"; + +export default definePlugin({}); +``` + +Маршруты обнаруживаются по файлам в `server/api`. Перечислять их в `plugin.ts` +не нужно. Дополнительные возможности этого файла определим отдельно. + +Тот же принцип действует для ассистента и MCP: `defineModelContext` связывает +существующий h3-обработчик с `defineModelAnnotation`. Сборщик выводит схемы из типов +TypeScript; описание хранится рядом с обработчиком. Контракт, пример и правила проверки — +в [действиях плагинов](../../docs/ru/development/plugin-actions.md). + + + +## Обработчик + +Файл `server/api/comments/status.get.ts`: + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; + +export default defineHandler((event) => { + const { actor, logger } = useAsmblyr(event); + logger.info("Status requested", { actorId: actor.id }); + return { data: { status: "scaffold" } }; +}); +``` + +- `defineHandler` — реэкспорт настоящего обработчика H3 **2.0.1-rc.32**. На входе + `H3Event`; HTTP-утилиты можно импортировать из `h3`, добавив ту же закреплённую + версию в зависимости своего пакета. H3 v1 с этим контрактом несовместим. +- `useAsmblyr(event)` возвращает проверенный контекст: `actor` (`id`, `kind`, + необязательный `displayName`), `requestId`, `logger`, `items` и разрешённые возможности пакета. + `storage` требует namespace и `storage.own`; `displayName` — `identity.profile`. Вызов без контекста Core завершается ошибкой. + Возможность `notifications` добавляет подписки и публикацию событий своего + namespace в личные входящие. Core выбирает получателей и проверяет права на запись. + См. [контракт возможностей](CAPABILITIES.ru.md). +- Core проверяет access token пользователя или сервисного аккаунта до чтения тела + и запуска обработчика. Анонимных endpoints нет. `Authorization` в `event.req` + не передаётся; Core также удаляет cookies сессии и провайдеров из запроса к плагину. +- Параметры и query читаются функциями H3: `getRouterParam`, `getQuery`, + `getValidatedQuery`. Для декодирования параметра используйте `{ decode: true }` + один раз. Тело читается через `event.req.json()`, `.text()`, `.formData()` или + `readValidatedBody`. Generic-параметр сам по себе не проверяет входные данные. +- Можно возвращать объект, строку, бинарные данные, `Response`, поток или Promise. + Статус и заголовки задаются через `event.res` либо в `Response`. + Строка теперь отправляется как текст; для JSON-строки нужен `Response.json(value)`. + `null`/`undefined` означают пустое тело. Объекты остаются JSON. +- `HTTPError` из H3 поддерживается. Сохранён и `EndpointError` для ошибок с кодом + Asmblyr. Ошибки имеют общий формат `{ code, message, requestId }`; подробности + серверных ошибок остаются в журнале. Клиентские HTTPError сохраняют свои headers. +- Вход не означает доступ к данным коллекций. `items` проверяет право нужного + действия на коллекцию и доступ к её полям через общую с `/items` логику Core. +- `useItems(event)` — общий доступ к этому сервису в обычном endpoint и в model + handler. В model handler `useActionContext(event)` также даёт actor, superuser и + signal; полного контекста `useAsmblyr` и привилегированного storage там нет. + `AccessGate.authenticated` разрешает вход, а права данных проверяет каждый метод + items. У MCP дополнительно учитывается публикация всех затронутых коллекций. + +Это API доверенных серверных пакетов, выполняемых в процессе Core, без песочницы. + + + +## Чтение данных + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; + +export default defineHandler(async (event) => { + const { items } = useAsmblyr(event); + return items.list("articles", { + fields: ["id", "title"], + limit: 20, + page: 1, + sort: "title", + direction: "asc", + }); +}); +``` + +Одна запись: `await items.get("articles", 4, { fields: ["title"] })`. +`items` из kit вызывает общий сервис Core внутри процесса, без дополнительного HTTP-запроса. + +| Метод | Результат | +| ------------------------------------- | ------------------------------------------------------------------ | +| `items.list(collection, options?)` | `{ data, labels, page: { number, size, total, sort, direction } }` | +| `items.get(collection, id, options?)` | `{ data, label }`; отсутствующая запись — ошибка 404 | + +Контракты `ItemsReader`, `ItemListOptions`, `ItemReadOptions`, `ItemListResult`, +`ItemResult`, `ItemRecord` и типы фильтров экспортируются из `@asmblyr-collaborative/kit`. + +- `collection` и `fields` используют технические имена. Выбор полей относится + к физическим столбцам, включая внешние ключи и системные даты. Разворачивания + связанных записей и выбора виртуальных полей через `fields` пока нет. +- Без `fields` возвращаются все доступные поля. `fields: []` возвращает только + первичный ключ; ключ всегда включён. `*` указывать не нужно. Недоступное поле — + ошибка 403, неизвестное — 400. Проекция выполняется в SQL. +- Сортировка, поиск и фильтры проверяются по полному набору разрешённых полей, + независимо от `fields`. Например, можно вернуть только `id`, сортируя по + доступному `title`. `labels`/`label` тоже могут использовать доступные поля вне + проекции, но не закрытые поля своей или связанной коллекции. +- `page` начинается с 1, `limit` — от 1 до 100 (по умолчанию 100), `total` — строка. + Это те же ограничения, что у HTTP API. Без `sort` используется первичный ключ. +- `id` — строка или безопасное целое JS. Большие `bigserial` передавайте строками; + в результате они также остаются строками. Поля имеют тип `unknown`, поскольку + схемы динамические. Календарный `date` всегда остаётся строкой `YYYY-MM-DD`, + обычный `bigint` — точной десятичной строкой. Внутри обработчика `datetime` может быть `Date`; + при JSON-ответе они сериализуются в ISO-строки, как в `/items`. +- Поддерживаются `q` и объект `filter` с теми же операторами и проверками, что у + HTTP API. Значения условий — строки или массивы строк, включая числа и boolean. + Фильтры по связям ограничены одним переходом и требуют прав на обе стороны. + Действуют общие лимиты: 8192 символа JSON, 3 уровня групп, 20 условий, 30 узлов. +- Права загружаются один раз при входе в endpoint. Изменение политик и отзыв + сессии/сервисного ключа учитываются в следующем запросе. Не сохраняйте `items` + в глобальной переменной и не передавайте в фоновые задачи: это контекст запроса. +- Переопределение actor, `superuser` или прав не предусмотрено. Неизвестные опции + чтения отклоняются. В kit нет доступа к Knex, токенам или системным + таблицам через `items`. Код плагина остаётся доверенным кодом процесса. +- Условия permissions проверяются Core: до count/пагинации, отдельно от фильтра + вызывающей стороны. Поля проецируются по сработавшим веткам; изменения проверяют + исходную и итоговую запись. См. [права и границы первой версии](../../docs/ru/features/access.md). + Пользовательский `filter` сужает выдачу и не заменяет такую политику. + Статусы записей автоматически не отфильтровываются: как в `/items`, нужное + условие передаёт вызывающая сторона. + +Пример фильтра по доступному полю связанной коллекции: + +```ts +const result = await items.list("articles", { + fields: ["title"], + filter: { + logic: "and", + children: [{ field: "author_id.name", op: "eq", value: "Анна" }], + }, +}); +``` + +Ошибки чтения проходят через общую обработку Core: 400 — некорректный запрос, +403 — нет разрешения, 404 — коллекция или запись не найдена. До запуска endpoint +невалидный access token даёт 401. Ошибка не превращается в пустую выдачу. + +HTTP API использует ту же проекцию: `GET /items/articles?fields=id,title` и +`GET /items/articles/4?fields=title`. Без параметра формат ответа сохранён. + + + +## Запись данных + +```ts +const { items } = useAsmblyr(event); +const result = await items.create("articles", { title: "Статья" }); +await items.update("articles", 4, { title: "Новое название" }); +await items.delete("articles", 4); +``` + +Контекст предоставляет `ItemsService`, включающий чтение и запись. `ItemsReader` +остаётся отдельным типом для функций, которым требуется только чтение. +Входные значения — JSON-объекты; правила полей и отношений проверяет Core. + +- `create`/`update` возвращают `{ data }` только с читаемыми полями. Если права + чтения нет, успешный результат — `{ data: null }`. `delete` возвращает `void`. +- Core использует общую с HTTP API логику в `items/writer.ts`: права, проверку + связанной коллекции, валидацию, запись и проекцию результата. +- Каждая операция и её история выполняются в транзакции. Автор истории берётся + из текущей сессии пользователя или сервисного аккаунта. ID операции истории + генерирует Core; это UUID, отдельный от HTTP `requestId`. +- Несколько вызовов не образуют одну транзакцию. Для записи со связями есть + `items.commit(collection, draft)`, использующий существующий механизм черновиков. + Возвращает `{ data: { id } }`. Сбой откатывает весь черновик, включая историю. +- `ItemCommitDraft` содержит values/references/relations/records, до 100 изменений + и глубины 5. Для изменения существующей записи через commit нужно и чтение. + `expectedValues` в каждой изменяемой записи включает исходные значения всех + обновляемых полей, включая FK. Core проверяет их под блокировкой строки; + несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. + Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. + Полный формат общий с [HTTP SDK](../sdk/README.ru.md#запись-и-сохранение-связеи). +- Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, + 409 — конфликт связи/ключа или вторая запись в single-коллекции. + +Собственные коллекции плагина устанавливаются Core при первом запуске. +Формат описания и ограничения обновления приведены ниже. + + + +### Пример POST с проверкой тела + +```ts +import { defineHandler, useAsmblyr } from "@asmblyr-collaborative/kit"; +import { readValidatedBody } from "h3"; + +export default defineHandler(async (event) => { + const body = await readValidatedBody(event, (value: unknown) => { + if (!value || typeof value !== "object" || !("text" in value)) return false; + if (typeof value.text !== "string" || !value.text.trim()) return false; + return { text: value.text.trim() }; + }); + return { data: { text: body.text, actorId: useAsmblyr(event).actor.id } }; +}); +``` + + + +### Граница HTTP + +Fastify продолжает маршрутизировать запросы. Изолированный parser читает исходные +байты тела с лимитом **1 МиБ**; H3 разбирает их один раз. Multipart поддерживается +в этом лимите, потоковая загрузка больших файлов пока не реализована. +Ответы идут потоком напрямую через Fastify без преобразования в JSON и без полной +буферизации. Отключение клиента передаётся в `event.req.signal`. + +Core сохраняет content-type, статусы, cookies плагина и end-to-end headers. +Имена cookies с префиксом `asmblyr_` и настроенным `SESSION_COOKIE_PREFIX` +зарезервированы; плагин не читает и не заменяет cookies сессии или провайдеров. +Редирект передаётся браузеру без серверного перехода с токеном Core. +В `Location` следует использовать публичный адрес, например `/api/comments/status`. +Промежуточного UI-прокси и его таймаута нет; клиент управляет отменой запроса. +Все ответы плагинов имеют `Cache-Control: no-store`. + +Это интеграция обработчиков H3, а не полноценный Nitro-сервер. `event.app`, +сырой Node req/res, WebSocket upgrades и фоновый lifecycle пока не предоставляются. +`plugin.ts` по-прежнему описывает пакет Asmblyr; `definePlugin` из `h3` — другое API. + + + +## Адреса + +Путь определяется относительно `server/api`, метод — суффиксом файла: + +| Файл внутри `server/api` | Публичный маршрут на адресе админки | +| ------------------------------------ | ----------------------------------- | +| `comments/index.get.ts` | `GET /api/comments` | +| `comments/index.post.ts` | `POST /api/comments` | +| `comments/[id].delete.ts` | `DELETE /api/comments/:id` | +| `comments/[id]/replies/index.get.ts` | `GET /api/comments/:id/replies` | + +Core использует те же пути без общего префикса `/api`. Имя npm-пакета в URL не +подставляется. Поддерживаются `.get.ts`, `.post.ts`, `.put.ts`, `.patch.ts` и +`.delete.ts`. Файл без суффикса метода, например `comments/status.ts`, обслуживает +все пять методов. Каждый файл должен экспортировать обработчик по умолчанию. + +Вспомогательные функции размещаются вне `server/api`. Файлы `.d.ts` не считаются +маршрутами. Необязательные параметры, catch-all, составные параметры в сегменте +и отдельные HEAD/OPTIONS handlers пока не поддерживаются. + +Первый сегмент пути принадлежит плагину. Core отклоняет пересечения с собственными +разделами, другими плагинами и дубли маршрутов. Сборка также отклоняет неоднозначные +файлы, например `[id].get.ts` и `[key].get.ts` в одном каталоге. +Браузер вызывает Core через `/api` на общем домене с текущей HttpOnly-сессией. +Core проверяет сессию и Origin для изменений. + + + +## Коллекции плагина + +В `server/collections/entries.ts` плагина comments есть пример с +`defineCollection()` из kit. Core проверяет описание и при первом запуске создаёт +таблицу вместе с метаданными. Файлы лежат непосредственно в `server/collections`, +имя файла совпадает с локальным именем коллекции. Подкаталоги и симлинки запрещены. + +```ts +import { defineCollection } from "@asmblyr-collaborative/kit"; + +export default defineCollection({ + name: "entries", + primaryKey: { name: "id", type: "uuid" }, + timestamps: { createdAt: true, updatedAt: true }, + presentation: { displayName: "Комментарии", hidden: true }, + fields: { + body: { + type: "text", + required: true, + nullable: false, + presentation: { label: "Комментарий", interface: "textarea" }, + }, + }, +}); +``` + +- `name` — локальное имя внутри плагина. Namespace задаётся в манифесте, + а Core формирует имя таблицы: `plugin__`, например `plugin_comments_entries`. +- `primaryKey` и `timestamps` используют общие с Core типы из contracts. + Ключ и системные даты не нужно повторять в `fields`. +- Ключи объекта `fields` — технические имена полей. Порядок объявления задаёт + порядок создания столбцов. Метаданные полей сохраняются при установке. +- `required` — обязательность непустого значения в API; `nullable` — допустимость + SQL NULL в БД. Оба параметра явные и независимые: `true` / `true` допустимо. +- Типы полей соответствуют Core. Тип значения `defaultValue` зависит от поля; + `date` и `bigint` используют строки, включая `CollectionRow` и входы storage. + для `file` / `files` дефолт не поддерживается. `searchable` доступен для `text` + и `email`; создание индекса это свойство не описывает. +- `presentation` поля использует существующий `FieldPresentation` из contracts; + можно указать только нужные настройки. У коллекции пока доступны `displayName` + и `hidden`. Скрытие из навигации не заменяет права или защиту структуры. +- `defineCollection` возвращает описание без побочных действий. Это не runtime- + валидатор: допустимость имён, UUID/дат/чисел, сочетаний настроек интерфейса и + конфликтов полей проверяет загрузчик Core через общие валидаторы коллекций. + +Пакет с коллекциями задаёт `asmblyr.manifest.namespace`, например `comments`, +и экспортирует `"./collections": "./dist/collections.json"`. Namespace — стабильное +имя из 1–31 строчных латинских букв, цифр и подчёркиваний, начиная с буквы. +Он уникален между установленными пакетами; `asmblyr` и префиксы `asmblyr_` / `plugin_` +зарезервированы. Полное имя таблицы должно умещаться в 63 символа PostgreSQL. +Сборщик создаёт индекс деклараций. В dev Core сканирует исходники локального пакета, +в production читает индекс и скомпилированные модули. `plugin.ts` остаётся пустым. + +Core хранит namespace и npm-владельца в `asmblyr_plugins`, а локальное имя, +физическое имя и нормализованное описание — в `asmblyr_plugin_collections`. +Реестр создаётся обычной миграцией Core (`pnpm db:migrate`). При старте установка +всех включённых коллекций проходит в одной транзакции под общей блокировкой. +Чужие таблицы и метаданные не присваиваются. Повторный запуск сохраняет данные. + +Изменения установленного описания должны быть воспроизведены явными миграциями +из `server/migrations`. Несовпадение итогового описания останавливает запуск. +Новые коллекции добавляются автоматически. Исчезнувшая +таблица не создаётся заново поверх реестра. Прямые изменения структуры через SQL +не синхронизируются автоматически. Отключение плагина сохраняет таблицы и владельца. + +Префикс `plugin_` зарезервирован в редакторе структуры и API независимо от того, +включён ли пакет. Обычные пользователи и superuser не меняют структуру и настройки +таких коллекций. Строки доступны через `/items` и kit по обычным правам, включая +историю изменений; отключение кода плагина само по себе эти права не отзывает. +MCP при установке выключен. Связи между коллекциями плагина пока не поддержаны +декларациями. Формат пока экспериментальный. + + + +## Миграции, хранилище и UI + +Полный контракт и ограничения описаны в [жизненном цикле плагина](LIFECYCLE.ru.md). +Пример со всеми тремя возможностями — `packages/plugin-comments`. + + + +## Пакет и подключение + +Имя, версия и зависимости плагина остаются в его `package.json`. +`asmblyr.manifest.version` задаёт версию формата манифеста, а не версию kit. + +Пакет плагина экспортирует собранный модуль по `exports["."].default`, типы по +`exports["."].types`, метаданные по `exports["./package.json"]` и сгенерированный +`dist/routes.json` по `exports["./routes"]`. Пакеты с namespace также экспортируют +`dist/collections.json` по `exports["./collections"]`. +Пример настройки находится в `packages/plugin-comments/package.json`. + +Пакеты устанавливаются как зависимости корневого проекта и явно включаются +в его `package.json`: + +```json +{ + "asmblyr": { + "plugins": ["@asmblyr-collaborative/plugin-comments"] + } +} +``` + +Core проверяет имена, версии манифестов и наличие обработчиков всех включённых +пакетов перед импортом. Ошибки включённого плагина останавливают запуск. +Сканирование происходит только внутри включённых пакетов; установка сама по себе +не включает плагин. + + + +## Разработка и сборка + +В разработке `pnpm dev` использует исходники включённых локальных пакетов из +workspace-пакетов внутри проекта. Изменение, добавление и удаление `.ts`-файлов автоматически перезапускает +Core и обновляет маршруты. Это перезапуск процесса, а не замена обработчика на лету. + +Сборка плагина выполняется командой kit `asmblyr-plugin build` в каталоге пакета; +в comments она задана в script `build`. TypeScript использует `rootDir: "."`, +`outDir: "dist"`, включая `plugin.ts` и `server/**/*.ts`. +Для типов Web API добавлены `DOM` и `DOM.Iterable` в `lib`. В kit и примере включён +`skipLibCheck`: декларации выбранной RC содержат несовместимости с TypeScript 5.9 +и ссылки на опциональные зависимости. Проверка собственного кода остаётся строгой; +при обновлении H3 это ограничение нужно пересмотреть. +Сборщик проверяет имена маршрутов, default exports и типы, очищает собственный +`dist`, создаёт JavaScript, декларации типов и список маршрутов `dist/routes.json`. + +В production и для установленных npm-пакетов Core читает готовый список и загружает +JavaScript. Сканирования исходников в этом режиме нет. Удалённые исходные маршруты +не сохраняются в следующей сборке. + +В репозитории Kit и comments связаны как workspace-пакеты. `pnpm build:plugins` собирает оба; +`pnpm dev` выполняет начальную сборку автоматически. `pnpm build` сначала собирает +пакеты, затем Core и UI. Публикация пакетов остаётся отдельным шагом. + +Проверка kit и первого плагина из корня репозитория: + +```sh +pnpm build:plugins +pnpm --filter @asmblyr-collaborative/kit --filter @asmblyr-collaborative/plugin-comments typecheck +``` + + + +## Локализация расширения + +Необязательные плоские JSON-каталоги `locales/ru.json` и `locales/en.json` +располагаются рядом с `package.json`. Включайте `locales/` в опубликованный пакет +вместе с `dist/`. Core читает их одинаково в source и built режиме и добавляет +в защищённый `GET /translations`. Каталоги содержат только публичные подписи: не +добавляйте в них настройки, ключи или пользовательские данные. + +UI использует тот же каталог через `defineUiPlugin({ translations: { ru, en }, ... })`. +Для JSON-импортов включите `resolveJsonModule` в tsconfig. У страниц, панелей +и интерфейсов поля можно указать `titleKey`; `title` остаётся запасной подписью. + +```tsx +import { usePluginTranslations } from "@asmblyr-collaborative/kit/ui/i18n"; + +function Panel() { + const { t, locale } = usePluginTranslations("comments"); + return

{t("panel.title", "Discussion")}

; +} +``` + +Хост предоставляет изолированный i18next provider на текущем языке профиля, +пространство имён `plugin.` и русский fallback. React +экранирует текст при отображении. Не импортируйте серверный entry point в UI. +Ключи `collection..label` и +`field...label|description|placeholder` задают подписи +собственных таблиц в UI и API. Для настроек используются `settings.title`, +`settings.description` и `settings..label|description`. +Имена коллекций/полей, значения записей и HTTP-контракты не переводятся. + + + +## Личные внешние подключения + +Model handler с `connection: "google"` в `defineModelAnnotation` виден ассистенту только при активном подключении текущего человека. Пакет запрашивает capability `connections.google`, установка явно одобряет её в `asmblyr.pluginPermissions`. + +`useActionContext(event).connections?.google` предоставляет owner-bound операции `list`, `readText`, `sheet`, `cells`, `proposeWrite`. Контракты `PersonalConnections`, `GoogleWriteInput`, `GoogleFileList`, `GoogleText`, `GoogleSheet`, `GoogleCells` экспортируются Kit. Нет raw token, произвольного URL или метода подтверждения. `proposeWrite` только сохраняет предложение; реальную запись подтверждает человек в UI Core. HTTP и внутренний MCP используют одинаковый handler. Пример — `packages/plugin-google-workspace`; ограничения и настройка — [Google Workspace](../../docs/ru/features/google-workspace.md). + + + +## Контракты потребителей SDK + +Собранные defineModelContext handlers содержат JSON-схемы входа/выхода. +Core экспортирует доступные методы через GET /schema по их AccessGate и исходному +HTTP-адресу. SDK создаёт client.plugins.namespace[methodId](input) с выведенным +результатом. ID соединяет сегменты пути дефисами; дополнительный реестр не нужен. + +Контракт описывает ограниченное JSON-подмножество Kit. Он не выдаёт права данных +и не гарантирует наличие личного OAuth-подключения. Выполнение проверяет исходный +gate и текущие права. Старые defineAction без выходной схемы не входят в типизированный API. + +[Проверки и публикация](../../docs/ru/reference/packages.md). diff --git a/packages/kit/UI.md b/packages/kit/UI.md index 8acca29..f5200d4 100644 --- a/packages/kit/UI.md +++ b/packages/kit/UI.md @@ -1,11 +1,16 @@ -# Общие компоненты UI + -Регистрация собственных редакторов полей и их настроек описана в -[FIELDS.md](FIELDS.md). Они используют эти же компоненты и тему хоста. +# Shared UI components -Kit содержит исходники общих shadcn-компонентов в `src/ui`. Админка и плагины -импортируют одну реализацию. Компоненты работают без `PluginUiProvider`; -обычные props, события, ref, `asChild` и варианты shadcn доступны напрямую. + + +[English](UI.md) · [Русский](UI.ru.md) + + + +[Field editors](FIELDS.md) use these same components and host theme. + +Kit owns shared shadcn sources under src/ui. Admin and plugins import one implementation. Components need no PluginUiProvider; ordinary props, events, refs, asChild, and shadcn variants work directly. ```tsx import { Button } from "@asmblyr-collaborative/kit/ui/button"; @@ -27,34 +32,27 @@ import { } from "@asmblyr-collaborative/kit/ui/select"; ``` -`buttonVariants` экспортируется из `@asmblyr-collaborative/kit/ui/button` для ссылок и других -элементов, которым нужен стиль кнопки. `@asmblyr-collaborative/kit/ui` содержит регистрацию -плагина и контракты его UI. Серверный entry `@asmblyr-collaborative/kit` в браузер не импортируется. -React и ReactDOM — совместимые peer dependencies: плагины используют экземпляры -хоста. Серверные потребители Kit не обязаны импортировать UI. +`buttonVariants` from kit/ui/button styles links and other elements. `kit/ui` contains plugin registration/UI contracts. Never import the server root into browsers. React/ReactDOM are compatible peer dependencies shared with the host. Server consumers need not import UI. -## Стили и сборка + -Хост использует Tailwind 4, анимации shadcn и общие переменные темы. В его глобальном -CSS уже подключено: +## Styles and build + +The host uses Tailwind 4, shadcn animations, and shared theme variables. Global CSS already imports: ```css @import "@asmblyr-collaborative/kit/ui/styles.css"; ``` -Этот entry указывает Tailwind на собранные компоненты внутри пакета, поэтому их -классы доступны и из установленного пакета. Плагину повторять импорт не нужно; -его собственные классы хост сканирует через сгенерированный `plugin-ui.css`. -Цвета, радиусы и светлая/тёмная тема принадлежат хосту. +This entry points Tailwind at built package components. Plugins need not repeat it; generated plugin-ui.css includes their own classes. Colors, radii, and light/dark themes belong to the host. + +After Kit changes run pnpm --filter @asmblyr-collaborative/kit build. Pnpm dev/build also builds Kit first. -После изменения Kit выполните из корня `pnpm --filter @asmblyr-collaborative/kit build`. -`pnpm dev` и `pnpm build` также собирают Kit перед приложениями. + -## Выпадающие списки внутри нативного dialog +## Dropdowns inside native dialogs -Radix-портал должен находиться в том же диалоге, чтобы не попасть под его backdrop. -В `SelectContent` можно передать `container={dialogElement}`. Для вложенных контролов -есть общий контекст из `@asmblyr-collaborative/kit/ui/portal-container`: +Radix portals must stay inside the dialog to appear above its backdrop. Pass container={dialogElement} to SelectContent or use the shared kit/ui/portal-container context: ```tsx @@ -62,17 +60,12 @@ Radix-портал должен находиться в том же диалог ``` -`SelectContent` использует явно указанный контейнер или унаследованный контекст. -Popover админки использует этот же контекст. За пределами диалога контейнер не нужен. +SelectContent uses an explicit container or inherited context. Admin popovers share it. No container is needed outside dialogs. + + -## Изменение и добавление компонентов +## Changing or adding components -Общие Button, Input, Textarea, Select, Checkbox и Tabs меняются в Kit. -Сохраняйте `"use client"` на интерактивных модулях и проверяйте компоненты внутри -нативного диалога. Button и `buttonVariants` остаются доступными серверным компонентам. +Change shared Button, Input, Textarea, Select, Checkbox, and Tabs in Kit. Keep "use client" on interactive modules and check native-dialog behavior. Button/buttonVariants remain usable from server components. -`apps/ui/components.json` пока обслуживает остальные локальные компоненты админки. -Не запускайте `shadcn add --overwrite` для уже перенесённых компонентов в `apps/ui`: -это создаст вторую реализацию. При переносе следующего компонента добавьте явный -export в Kit, перенесите необходимые зависимости и замените импорты потребителей. -Импорты внутри Kit не должны зависеть от `@/` или файлов админки. +Apps/ui/components.json still manages other local components. Do not run shadcn add --overwrite for components already moved to Kit: it creates duplicates. When moving another component, add an explicit Kit export, move dependencies, and replace consumer imports. Kit imports must not depend on @/ aliases or admin files. diff --git a/packages/kit/UI.ru.md b/packages/kit/UI.ru.md new file mode 100644 index 0000000..f419b08 --- /dev/null +++ b/packages/kit/UI.ru.md @@ -0,0 +1,92 @@ + + +# Общие компоненты UI + + + +[English](UI.md) · [Русский](UI.ru.md) + + + +Регистрация собственных редакторов полей и их настроек описана в +[FIELDS.md](FIELDS.ru.md). Они используют эти же компоненты и тему хоста. + +Kit содержит исходники общих shadcn-компонентов в `src/ui`. Админка и плагины +импортируют одну реализацию. Компоненты работают без `PluginUiProvider`; +обычные props, события, ref, `asChild` и варианты shadcn доступны напрямую. + +```tsx +import { Button } from "@asmblyr-collaborative/kit/ui/button"; +import { Input } from "@asmblyr-collaborative/kit/ui/input"; +import { Textarea } from "@asmblyr-collaborative/kit/ui/textarea"; +import { Checkbox } from "@asmblyr-collaborative/kit/ui/checkbox"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@asmblyr-collaborative/kit/ui/tabs"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@asmblyr-collaborative/kit/ui/select"; +``` + +`buttonVariants` экспортируется из `@asmblyr-collaborative/kit/ui/button` для ссылок и других +элементов, которым нужен стиль кнопки. `@asmblyr-collaborative/kit/ui` содержит регистрацию +плагина и контракты его UI. Серверный entry `@asmblyr-collaborative/kit` в браузер не импортируется. +React и ReactDOM — совместимые peer dependencies: плагины используют экземпляры +хоста. Серверные потребители Kit не обязаны импортировать UI. + + + +## Стили и сборка + +Хост использует Tailwind 4, анимации shadcn и общие переменные темы. В его глобальном +CSS уже подключено: + +```css +@import "@asmblyr-collaborative/kit/ui/styles.css"; +``` + +Этот entry указывает Tailwind на собранные компоненты внутри пакета, поэтому их +классы доступны и из установленного пакета. Плагину повторять импорт не нужно; +его собственные классы хост сканирует через сгенерированный `plugin-ui.css`. +Цвета, радиусы и светлая/тёмная тема принадлежат хосту. + +После изменения Kit выполните из корня `pnpm --filter @asmblyr-collaborative/kit build`. +`pnpm dev` и `pnpm build` также собирают Kit перед приложениями. + + + +## Выпадающие списки внутри нативного dialog + +Radix-портал должен находиться в том же диалоге, чтобы не попасть под его backdrop. +В `SelectContent` можно передать `container={dialogElement}`. Для вложенных контролов +есть общий контекст из `@asmblyr-collaborative/kit/ui/portal-container`: + +```tsx + + {children} + +``` + +`SelectContent` использует явно указанный контейнер или унаследованный контекст. +Popover админки использует этот же контекст. За пределами диалога контейнер не нужен. + + + +## Изменение и добавление компонентов + +Общие Button, Input, Textarea, Select, Checkbox и Tabs меняются в Kit. +Сохраняйте `"use client"` на интерактивных модулях и проверяйте компоненты внутри +нативного диалога. Button и `buttonVariants` остаются доступными серверным компонентам. + +`apps/ui/components.json` пока обслуживает остальные локальные компоненты админки. +Не запускайте `shadcn add --overwrite` для уже перенесённых компонентов в `apps/ui`: +это создаст вторую реализацию. При переносе следующего компонента добавьте явный +export в Kit, перенесите необходимые зависимости и замените импорты потребителей. +Импорты внутри Kit не должны зависеть от `@/` или файлов админки. diff --git a/packages/plugin-comments/README.md b/packages/plugin-comments/README.md index 4619e17..ec22d6a 100644 --- a/packages/plugin-comments/README.md +++ b/packages/plugin-comments/README.md @@ -1,101 +1,64 @@ # Comments -Встроенный модуль обсуждения записей: комментарии, реальные авторы, редактирование своего текста и вкладка в карточке. - -## Устройство - -Зависимость `@asmblyr/kit` сохранена как workspace-алиас нового -`@asmblyr-collaborative/kit` только для неизменяемого исходника существующей -миграции. Новые модули используют актуальное имя пакета. - -- `plugin.ts` — обязательный `definePlugin({})`. -- `server/collections/entries.ts` — актуальная декларация `plugin_comments_entries`. -- `server/migrations/20261002060000_authors.ts` — добавление авторства и индекса. -- `server/hooks/` — транзакционная очистка при удалении записи или коллекции. -- `server/settings.ts` — разрешение новых комментариев и максимальная длина. -- `server/api/comments/[collection]/[item]` — тонкие обработчики HTTP. -- `server/schemas/comments.ts` — чтение и однократная проверка HTTP-входа. -- `server/services/comments.ts` — `CommentsService`, операции и правила доступа. -- `server/presenters/comment.ts` — преобразование типизированной строки в публичный ответ. -- `shared/comments.ts` — контракты, лимит текста и поддерживаемые коллекции. -- `ui/index.ts` — браузерный entry `defineUiPlugin`, экспортируемый как `./ui`. -- `ui/api/comments.ts` — типизированные HTTP-операции. -- `ui/hooks/use-comments.ts` — загрузка, пагинация, ошибки и состояние операций. -- `ui/hooks/use-comment-composer.ts` — черновик, редактирование и правила отправки формы. -- `ui/components` — вкладка, форма ввода и отдельный комментарий. - -Путь запроса: файловый маршрут → schemas → CommentsService → типизированное -`useStorage(context, entries)` → presenter. Сервис создаётся на каждый запрос; -он проверяет право чтения целевой записи и авторство перед обращением к хранилищу. -Типы строк выводятся из `server/collections/entries.ts`, отдельной ручной модели БД нет. -UI импортирует `Button` из `@asmblyr-collaborative/kit/ui/button` и `Textarea` из -`@asmblyr-collaborative/kit/ui/textarea` и вызывает именованные операции -`createComment`, `updateComment`, `deleteComment`. - -Пакет включён через `asmblyr.plugins` корневого package.json. `pnpm dev` собирает -kit и пакет, Core устанавливает таблицы и выполняет новые миграции при запуске. -После обновления самого Core нужен `pnpm db:migrate` для его реестров. -В production сначала `pnpm build`, затем запуск сервисов. + + +[English](README.md) · [Русский](README.ru.md) + + + +Bundled record discussions with real authors, self-editing, and a record-card tab. + +## Structure + +The old @asmblyr/kit workspace alias remains only for an immutable historical migration. New modules use @asmblyr-collaborative/kit. + +| Location | Responsibility | +| ------------------------------------------- | ---------------------------------------------- | +| plugin.ts | Required definePlugin({}) | +| server/collections/entries.ts | Current plugin_comments_entries declaration | +| server/migrations/20261002060000_authors.ts | Author fields/index | +| server/hooks/ | Transactional record/collection cleanup | +| server/settings.ts | New-comment switch and length limit | +| server/api/comments/[collection]/[item] | Thin HTTP routes | +| server/schemas/comments.ts | Single HTTP input validation | +| server/services/comments.ts | CommentsService operations/access | +| server/presenters/comment.ts | Typed storage row → public response | +| shared/comments.ts | Contracts, text limits, supported collections | +| ui/index.ts | DefineUiPlugin browser entry, exported as ./ui | +| ui/api/comments.ts | Typed HTTP calls | +| ui/hooks/use-comments.ts | Loading, pagination, errors, operation state | +| ui/hooks/use-comment-composer.ts | Drafts, editing, submission | +| ui/components | Panel, composer, individual comment | + +Request flow: route → schemas → per-request CommentsService → useStorage(context, entries) → presenter. The service checks target-record read access and authorship before storage. Types derive from the declaration, without a separate database model. UI uses shared Kit Button/Textarea and named createComment/updateComment/deleteComment calls. + +The root asmblyr.plugins enables the package. Pnpm dev builds Kit/plugins; Core installs tables/applies new plugin migrations at startup. Core upgrades need pnpm db:migrate for its registries. Production builds first, then starts services. ## API -Префикс на UI — `/api`, непосредственно в Core его нет. - -| Метод | Core path | Действие | -| ------ | ------------------------------------ | ----------------------------------- | -| GET | `/comments/:collection/:item?page=1` | Список по 30, новые сначала | -| POST | `/comments/:collection/:item` | Создать, тело `{ "body": "текст" }` | -| PATCH | `/comments/:collection/:item/:id` | Изменить свой текст | -| DELETE | `/comments/:collection/:item/:id` | Удалить свой комментарий | - -Все endpoints требуют сессию или сервисный токен и право чтения обсуждаемой записи. -Читатель может комментировать без права менять её поля. Изменять/удалять комментарий -через эти endpoints может его автор; отдельной модерации superuser пока нет. -Core проверяет доступ к записи при каждом запросе. Отзыв политики действует со -следующего запроса. Комментарии к системным и plugin-коллекциям не поддерживаются. -Чтение целевой записи учитывает действующие условия доступа к строкам. - -Автор и его имя берутся из контекста. Имя сохраняется на момент отправки, email не -раскрывается. Старым записям автор не выдумывается: поля остаются NULL. -Текст — строка длиной 1–10 000 символов после trim; HTML не исполняется. -Дополнительные поля тела запроса отклоняются. Пагинация имеет досортировку по ID. -Индекс покрывает адрес записи, дату и ID. - -`items.get` проверяет доступ к предметной записи, `storage` даёт плагину доступ -только к собственной коллекции. Валидация, даты и история используются из Core. -Grants на `plugin_comments_entries` для участников выдавать не нужно. Прямой -`/items` остаётся административным путём по обычным grants и позволяет обойти -правила специализированного API при явной выдаче этих прав. - -Комментарии сохраняются сразу, отдельно от черновика карточки. Текст сохраняется -при смене вкладок; закрытие с неотправленным текстом требует подтверждения. -Есть редактирование, подтверждение удаления, повтор загрузки и страницы списка. -Ошибки загрузки и сохранения независимы. Черновик защищён от перезаписи при -редактировании другого комментария; удаление другого комментария его не блокирует. -Признак `isOwn` отделён от прав `canEdit/canDelete`. -Новые несохранённые записи пока не имеют обсуждений. Нет тредов ответов, упоминаний, -вложений, real-time, email и push. Кнопка «Следить» и первый отправленный комментарий -подписывают на новые комментарии других участников. Явно отключённая подписка -не включается автоматически. Отдельный колокольчик показывает непрочитанные и -открывает конкретный комментарий; требуется одобренная возможность `notifications`. - -При удалении записи или коллекции через Core включённый плагин удаляет обсуждение -в той же транзакции; ошибка hook откатывает удаление. Сохранение комментария -удерживает целевую запись, исключая гонку с её удалением. Ключ UUID нормализуется -по сохранённой записи. Прямой SQL, каскады PostgreSQL и удаления при отключённом -плагине не порождают hooks: автоматической фоновой очистки таких сирот пока нет. -Перед повторным использованием их адресов нужна отдельная очистка; старые данные -при запуске автоматически не удаляются. - -В **Система → Плагины → Комментарии** доступны `allowNewComments` и `maxLength` -(100–10 000). Изменения применяются к следующим запросам. Отключение создания -оставляет чтение, редактирование и удаление собственных комментариев. Сервер -проверяет лимит при создании и изменении; UI получает `canCreate` и `maxLength`. - -Возможности пакета объявлены в manifest и разрешены в корневом package.json. -`identity.profile` нужен для имени автора, `items.read` — проверки доступа к записи, -`collections.manage` — собственных таблиц и миграций, `storage.own` — комментариев, -`hooks.items/hooks.collections` — очистки, `settings` — настроек. Права `items.write` -плагину не нужны. - -Подробный контракт: [жизненный цикл плагина](../kit/LIFECYCLE.md). +The public admin domain adds /api; direct Core also supports unprefixed paths. + +| Method | Core path | Operation | +| ------ | ---------------------------------- | ------------------------------ | +| GET | /comments/:collection/:item?page=1 | Latest first, pages of 30 | +| POST | /comments/:collection/:item | Create with { "body": "text" } | +| PATCH | /comments/:collection/:item/:id | Edit own text | +| DELETE | /comments/:collection/:item/:id | Delete own comment | + +Every endpoint requires a human/session or service token and target-record read access, including row conditions. Readers need no record-update grant to comment. Only authors edit/delete through these endpoints; separate superuser moderation is unsupported. Every request rechecks access, so policy revocation applies next request. System/plugin collection discussions are unsupported. + +Author/name come from context; the name is snapshotted on submission, without exposing email. Historical unknown authors remain NULL. Text is 1–10000 trimmed characters; HTML never executes. Extra body keys are rejected. Pagination ties use ID; the index covers record address, date, and ID. + +Items.get authorizes the target; storage accesses only the plugin's collection with Core validation/timestamps/history. Participants need no plugin_comments_entries grants. Direct /items remains an ordinary-grant administrative path that bypasses specialized API rules if explicitly granted. + +Comments save immediately, independently of record drafts. Text survives tab switches; closing unsent text requires confirmation. Editing, delete confirmation, retry, and pagination are supported. Load/save errors are separate. Editing another comment protects the current draft; deleting another does not block it. IsOwn is distinct from canEdit/canDelete. + +New unsaved records have no discussions. Replies, mentions, attachments, realtime, email, and push are unsupported. Follow and the first submitted comment subscribe to others' comments; explicit unfollow is not automatically reversed. The separate bell opens unread comments and requires approved notifications capability. + +Enabled hooks delete discussions in the same transaction as Core record/collection deletion; failures roll back. Saving holds the target record against concurrent deletion and normalizes UUID from the stored row. Direct SQL, database cascades, and deletions while disabled emit no hooks. There is no background orphan cleanup; clean separately before reusing their addresses. Startup does not silently delete old data. + +Settings → Plugins → Comments exposes allowNewComments and maxLength (100–10000), applying to subsequent requests. Disabling creation preserves reading/editing/deleting your own comments. Core checks limits on create/update; UI receives canCreate/maxLength. + +Manifest/project approvals cover identity.profile, items.read, collections.manage, storage.own, hooks.items, hooks.collections, settings, and notifications. Items.write is unnecessary. + +See [plugin lifecycle](../kit/LIFECYCLE.md). diff --git a/packages/plugin-comments/README.ru.md b/packages/plugin-comments/README.ru.md new file mode 100644 index 0000000..e778d73 --- /dev/null +++ b/packages/plugin-comments/README.ru.md @@ -0,0 +1,107 @@ +# Comments + + + +[English](README.md) · [Русский](README.ru.md) + + + +Встроенный модуль обсуждения записей: комментарии, реальные авторы, редактирование своего текста и вкладка в карточке. + +## Устройство + +Зависимость `@asmblyr/kit` сохранена как workspace-алиас нового +`@asmblyr-collaborative/kit` только для неизменяемого исходника существующей +миграции. Новые модули используют актуальное имя пакета. + +- `plugin.ts` — обязательный `definePlugin({})`. +- `server/collections/entries.ts` — актуальная декларация `plugin_comments_entries`. +- `server/migrations/20261002060000_authors.ts` — добавление авторства и индекса. +- `server/hooks/` — транзакционная очистка при удалении записи или коллекции. +- `server/settings.ts` — разрешение новых комментариев и максимальная длина. +- `server/api/comments/[collection]/[item]` — тонкие обработчики HTTP. +- `server/schemas/comments.ts` — чтение и однократная проверка HTTP-входа. +- `server/services/comments.ts` — `CommentsService`, операции и правила доступа. +- `server/presenters/comment.ts` — преобразование типизированной строки в публичный ответ. +- `shared/comments.ts` — контракты, лимит текста и поддерживаемые коллекции. +- `ui/index.ts` — браузерный entry `defineUiPlugin`, экспортируемый как `./ui`. +- `ui/api/comments.ts` — типизированные HTTP-операции. +- `ui/hooks/use-comments.ts` — загрузка, пагинация, ошибки и состояние операций. +- `ui/hooks/use-comment-composer.ts` — черновик, редактирование и правила отправки формы. +- `ui/components` — вкладка, форма ввода и отдельный комментарий. + +Путь запроса: файловый маршрут → schemas → CommentsService → типизированное +`useStorage(context, entries)` → presenter. Сервис создаётся на каждый запрос; +он проверяет право чтения целевой записи и авторство перед обращением к хранилищу. +Типы строк выводятся из `server/collections/entries.ts`, отдельной ручной модели БД нет. +UI импортирует `Button` из `@asmblyr-collaborative/kit/ui/button` и `Textarea` из +`@asmblyr-collaborative/kit/ui/textarea` и вызывает именованные операции +`createComment`, `updateComment`, `deleteComment`. + +Пакет включён через `asmblyr.plugins` корневого package.json. `pnpm dev` собирает +kit и пакет, Core устанавливает таблицы и выполняет новые миграции при запуске. +После обновления самого Core нужен `pnpm db:migrate` для его реестров. +В production сначала `pnpm build`, затем запуск сервисов. + +## API + +Префикс на UI — `/api`, непосредственно в Core его нет. + +| Метод | Core path | Действие | +| ------ | ------------------------------------ | ----------------------------------- | +| GET | `/comments/:collection/:item?page=1` | Список по 30, новые сначала | +| POST | `/comments/:collection/:item` | Создать, тело `{ "body": "текст" }` | +| PATCH | `/comments/:collection/:item/:id` | Изменить свой текст | +| DELETE | `/comments/:collection/:item/:id` | Удалить свой комментарий | + +Все endpoints требуют сессию или сервисный токен и право чтения обсуждаемой записи. +Читатель может комментировать без права менять её поля. Изменять/удалять комментарий +через эти endpoints может его автор; отдельной модерации superuser пока нет. +Core проверяет доступ к записи при каждом запросе. Отзыв политики действует со +следующего запроса. Комментарии к системным и plugin-коллекциям не поддерживаются. +Чтение целевой записи учитывает действующие условия доступа к строкам. + +Автор и его имя берутся из контекста. Имя сохраняется на момент отправки, email не +раскрывается. Старым записям автор не выдумывается: поля остаются NULL. +Текст — строка длиной 1–10 000 символов после trim; HTML не исполняется. +Дополнительные поля тела запроса отклоняются. Пагинация имеет досортировку по ID. +Индекс покрывает адрес записи, дату и ID. + +`items.get` проверяет доступ к предметной записи, `storage` даёт плагину доступ +только к собственной коллекции. Валидация, даты и история используются из Core. +Grants на `plugin_comments_entries` для участников выдавать не нужно. Прямой +`/items` остаётся административным путём по обычным grants и позволяет обойти +правила специализированного API при явной выдаче этих прав. + +Комментарии сохраняются сразу, отдельно от черновика карточки. Текст сохраняется +при смене вкладок; закрытие с неотправленным текстом требует подтверждения. +Есть редактирование, подтверждение удаления, повтор загрузки и страницы списка. +Ошибки загрузки и сохранения независимы. Черновик защищён от перезаписи при +редактировании другого комментария; удаление другого комментария его не блокирует. +Признак `isOwn` отделён от прав `canEdit/canDelete`. +Новые несохранённые записи пока не имеют обсуждений. Нет тредов ответов, упоминаний, +вложений, real-time, email и push. Кнопка «Следить» и первый отправленный комментарий +подписывают на новые комментарии других участников. Явно отключённая подписка +не включается автоматически. Отдельный колокольчик показывает непрочитанные и +открывает конкретный комментарий; требуется одобренная возможность `notifications`. + +При удалении записи или коллекции через Core включённый плагин удаляет обсуждение +в той же транзакции; ошибка hook откатывает удаление. Сохранение комментария +удерживает целевую запись, исключая гонку с её удалением. Ключ UUID нормализуется +по сохранённой записи. Прямой SQL, каскады PostgreSQL и удаления при отключённом +плагине не порождают hooks: автоматической фоновой очистки таких сирот пока нет. +Перед повторным использованием их адресов нужна отдельная очистка; старые данные +при запуске автоматически не удаляются. + +В **Система → Плагины → Комментарии** доступны `allowNewComments` и `maxLength` +(100–10 000). Изменения применяются к следующим запросам. Отключение создания +оставляет чтение, редактирование и удаление собственных комментариев. Сервер +проверяет лимит при создании и изменении; UI получает `canCreate` и `maxLength`. + +Возможности пакета объявлены в manifest и разрешены в корневом package.json. +`identity.profile` нужен для имени автора, `items.read` — проверки доступа к записи, +`collections.manage` — собственных таблиц и миграций, `storage.own` — комментариев, +`hooks.items/hooks.collections` — очистки, `settings` — настроек. Права `items.write` +плагину не нужны. + +Подробный контракт: [жизненный цикл плагина](../kit/LIFECYCLE.ru.md). diff --git a/packages/plugin-google-workspace/README.md b/packages/plugin-google-workspace/README.md index 66db0bd..e1a8a43 100644 --- a/packages/plugin-google-workspace/README.md +++ b/packages/plugin-google-workspace/README.md @@ -1,5 +1,11 @@ # Google Workspace assistant tools + + +[English](README.md) · [Русский](README.ru.md) + + + The admin extension list displays **Google Workspace** using this package's RU/EN translation catalogs. Its API namespace stays `google`. diff --git a/packages/plugin-google-workspace/README.ru.md b/packages/plugin-google-workspace/README.ru.md new file mode 100644 index 0000000..39945f5 --- /dev/null +++ b/packages/plugin-google-workspace/README.ru.md @@ -0,0 +1,22 @@ +# Инструменты Google Workspace для ассистента + + + +[English](README.md) · [Русский](README.ru.md) + + + +В списке расширений пакет называется Google Workspace благодаря каталогам RU/EN. +Namespace API остаётся google. + +Опциональный плагин для личных подключений Google Drive и Sheets. Пять типизированных +H3 model routes: поиск файлов, чтение текста, описание таблицы, чтение диапазона и +предложение записи. Core отвечает за OAuth, шифрование и подтверждение человеком; +плагин не получает Google-токены. + +Добавьте @asmblyr-collaborative/plugin-google-workspace в asmblyr.plugins и +одобрите connections.google в asmblyr.pluginPermissions. Сборка: +pnpm --filter @asmblyr-collaborative/plugin-google-workspace build. + +[Настройка и ограничения](../../docs/ru/features/google-workspace.md) · +[Контракт действий](../../docs/ru/development/plugin-actions.md). diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 6148e69..d578037 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,14 +1,18 @@ # @asmblyr-collaborative/sdk -Типизированный HTTP-клиент Asmblyr для браузера и Node.js 22+. -Каждая операция вызывает основной API. В пакете нет H3, Knex, PostgreSQL, -компилятора TypeScript или загрузчика плагинов во время исполнения. -Для разработки расширений используется отдельный `@asmblyr-collaborative/kit`. + -## Профиль пользователя +[English](README.md) · [Русский](README.ru.md) -Собственный профиль доступен с пользовательским access token. Статус, права, -пароли и системные даты нельзя менять через профиль. Все новые поля необязательны. + + +Typed Collaborative HTTP client for browsers and Node.js 22+. Every operation calls the main API. Runtime dependencies do not include H3, Knex, PostgreSQL, the TypeScript compiler, or a plugin loader. Use the separate @asmblyr-collaborative/kit to develop extensions. + + + +## User profile + +A human access token can access its own profile. Profile updates cannot change status, permissions, passwords, or system timestamps. New profile fields are optional. ```ts await client.users.updateMe({ @@ -20,30 +24,22 @@ await client.users.updatePreferences({ timezone: "Asia/Yekaterinburg" }); const { data: me } = await client.users.me(); ``` -Дополнительные поля потребитель настраивает в обычной UUID-коллекции и выбирает -её в настройках пользователей. `asm connect` / `asm generate` включает её поля -в схему проекта. Для клиента с этой схемой: +Configure extra fields in an ordinary UUID collection and select it in user settings. asm connect / asm generate includes its fields in the project schema: ```ts const { data: profile } = await client.users.extension("user_profiles"); await client.users.saveExtension("user_profiles", { bio: "Hello" }); ``` -`saveExtension` создаёт запись с ID текущего пользователя или обновляет её. -Для первого сохранения нужны `create` и `read`, затем `update` и `read`. -Действуют обычные правила полей и строк, права на связи и файлы. Передайте только -поля сгенерированной схемы; имя коллекции должно совпадать с выбранным расширением. -Скрытые поля могут отсутствовать в результате, а `data` — быть `null`. -После первого сохранения запись и её связи доступны через обычный `client.items` -и fluent API; произвольное создание профиля через `items.create` запрещено. +`saveExtension` creates/updates the current user's ID. First save needs create/read; later changes need update/read. Normal row/field, relationship, and file rules apply. Supply generated-schema fields only, and use the selected extension's collection name. Hidden fields may be absent and data may be null. After initial creation, ordinary items/fluent APIs edit its records and relationships. Arbitrary profile creation through items.create is forbidden. -## Подключение клиента + -Личные уведомления доступны через `client.notifications.list()`, `.read(id)` и -`.readAll(result.readBefore)`. Они требуют человеческой сессии; сервисные ключи -не дают доступ к личным входящим. Core хранит последние 200 событий пользователя. +## Connect a client -Предварительная версия устанавливается с тегом `beta`: +Personal notifications use client.notifications.list(), .read(id), and .readAll(result.readBefore). They require a human session; service keys cannot access an inbox. Core retains the latest 200 events per user. + +Install prereleases with the beta tag: ```sh npm install @asmblyr-collaborative/sdk@beta @@ -53,7 +49,7 @@ npm install --save-dev @asmblyr-collaborative/cli@beta ```ts import { createClient } from "@asmblyr-collaborative/sdk"; -// В браузере: сессия админки остаётся в HttpOnly cookies. +// Browser sessions remain in HttpOnly cookies. const client = createClient({ baseUrl: "/api" }); const records = await client.items.list("articles", { @@ -67,11 +63,9 @@ const record = await client.items.get("articles", 4); const profile = await client.users.me(); ``` -`baseUrl` — корень API, например `http://localhost:3001` для прямого доступа -к Core или `http://localhost:3000/api` для браузерной админки. Префикс `/api` -автоматически не добавляется. Путь `/api` без origin работает в браузере. +`baseUrl` is the API root: `http://localhost:3001` for direct Core or `http://localhost:3000/api` on the admin domain. The SDK does not add /api automatically. A relative /api works in browsers. -Для прямых запросов к Core передайте access token: +Direct Core requests can supply an access token: ```ts const client = createClient({ @@ -81,119 +75,85 @@ const client = createClient({ }); ``` -`accessToken` может быть строкой или асинхронной функцией. Функция вызывается для -каждого запроса. Поддерживаются access tokens пользователя и сервисного аккаунта; -права проверяет Core. `/users/me` предназначен для пользователя. -SDK не хранит токены в браузере, не выполняет вход и не обновляет токены сам. -В админке браузерная сессия хранится в HttpOnly cookie, которую проверяет Core. - -## Реализованные методы - -| Метод | HTTP | Ответ | -| ------------------------------------------------ | -------------------------------- | -------------------------- | -| `items.list(collection, options?, request?)` | `GET /items/:collection` | `{ data, labels, page }` | -| `items.get(collection, id, options?, request?)` | `GET /items/:collection/:id` | `{ data, label }` | -| `users.me(request?)` | `GET /users/me` | `{ data: CurrentUser }` | -| `items.create(collection, values, request?)` | `POST /items/:collection` | `{ data: row \| null }` | -| `items.update(collection, id, values, request?)` | `PATCH /items/:collection/:id` | `{ data: row \| null }` | -| `items.delete(collection, id, request?)` | `DELETE /items/:collection/:id` | `void` (204) | -| `items.commit(collection, draft, request?)` | `POST /items/:collection/commit` | `{ data: { id: string } }` | - -Остальные системные ресурсы и массовые операции пока не включены. -Прямого доступа к базе данных и переключения actor/superuser нет. - -`list` поддерживает `fields`, `page`, `limit`, `sort`, `direction`, `q`, `filter`. -SDK сериализует фильтр и query, Core проверяет значения, права и ограничения. -Фильтр — группа `{ logic: "and" | "or", children: [...] }`, значения условий — -строки или массивы строк. Поиск и фильтры по связям используют правила основного API. -Для списка предел страницы — 100 записей. `page.total` остаётся строкой. -Core дополнительно применяет условия permissions; SDK не передаёт доверенный -контекст. В одной странице условно разрешённые поля могут отсутствовать у части -записей. См. [права и ограничения](../../docs/features/access.md). - -Без `fields` выдаются доступные поля. `fields: []` отправляется как `fields=` -и возвращает только первичный ключ. Ключ всегда добавляет Core. Явно выбранное -закрытое поле приводит к 403. Названия коллекций и полей — технические. -Подписи `label`/`labels` могут использовать другие разрешённые поля. - -Большие целые ID передавайте строками. SDK отклоняет небезопасные JS-числа. -Строковые ключи кодируются как один сегмент URL; ключи `.` и `..` не поддерживаются, -поскольку HTTP-клиенты нормализуют такие сегменты. Даты HTTP-ответов — ISO-строки, -значения `bigserial` — строки. SDK не преобразует их в `Date` или `number`. - -## Запись и сохранение связей +`accessToken` may be a string or async function called per request. Human and service tokens are supported; Core checks permissions. `users.me` requires a human. SDK does not store browser tokens, sign in, or refresh tokens itself. Admin sessions use HttpOnly cookies validated by Core. + + + +## Available methods + +| Method | HTTP | Response | +| ---------------------------------------------- | ------------------------------ | ------------------------ | +| items.list(collection, options?, request?) | GET /items/:collection | { data, labels, page } | +| items.get(collection, id, options?, request?) | GET /items/:collection/:id | { data, label } | +| users.me(request?) | GET /users/me | { data: CurrentUser } | +| items.create(collection, values, request?) | POST /items/:collection | { data: row or null } | +| items.update(collection, id, values, request?) | PATCH /items/:collection/:id | { data: row or null } | +| items.delete(collection, id, request?) | DELETE /items/:collection/:id | void (204) | +| items.commit(collection, draft, request?) | POST /items/:collection/commit | { data: { id: string } } | + +This is the central record API, not complete coverage of every system resource or bulk operation. There is no direct database access or actor/superuser switching. + +List supports fields, page, limit, sort, direction, order, q, and filter. SDK serializes query/filter values; Core validates access and limits. Filters are logic:and/or groups with children; condition values are strings or string arrays. Relationship search/filtering follows the main API. Pages are capped at 100, and page.total remains a string. + +Core additionally applies permission conditions; SDK cannot provide trusted context. Conditionally permitted fields may be missing from some rows. See [access rules](../../docs/features/access.md). + +Omitting fields returns permitted fields. Fields:[] sends fields= and returns only the primary key, always added by Core. Explicitly requesting a closed field yields 403. Collection/field names are technical identifiers. Label/labels may use other permitted fields. + +Pass large integer IDs as strings. Unsafe JavaScript numbers are rejected. String IDs are encoded as one URL segment; "." and ".." are unsupported because HTTP clients normalize them. HTTP dates are ISO strings and bigserial values are strings; SDK does not convert them to Date/number. + + + +## Writes and relationships ```ts -const created = await client.items.create("articles", { title: "Статья" }); -await client.items.update("articles", 4, { title: "Новое название" }); +const created = await client.items.create("articles", { title: "Article" }); +await client.items.update("articles", 4, { title: "New title" }); await client.items.delete("articles", 4); -// Одна транзакция для основной записи и связанных изменений. +// One transaction for the root record and related changes. await client.items.commit("articles", { id: "4", - values: { title: "Статья с категорией" }, - references: { category_id: { values: { title: "Новая категория" } } }, + values: { title: "Article with a category" }, + references: { category_id: { values: { title: "New category" } } }, }); ``` -Core применяет права create/update/delete, ограничения полей, проверки связей, -валидацию и историю изменений. `create`/`update` возвращают только читаемые поля; -`{ data: null }` означает успешную запись без права чтения, а не ошибку. -`delete` разрешается на уровне записи, без списка удаляемых полей. +Core enforces create/update/delete, field constraints, related-record access, validation, and history. Create/update return readable fields only; data:null means a successful write without read access. Delete authorizes the record, not a field list. -Каждый вызов — отдельная транзакция. Последовательность `create` и `update` -не становится одной транзакцией. Для черновика со связями используется `commit`: -ошибка откатывает все его изменения и историю. Существующие записи в черновике -требуют права чтения; обычный `update` допускает право записи без чтения. +Each call is a separate transaction. Sequential create/update calls do not become atomic. Commit saves a related draft and rolls back all changes/history on failure. Existing records in a draft require read access; ordinary update can work with write-only grants. -`ItemCommitDraft` описывает `references`, `relations` (attach/detach/create/links) -и `records` — изменения существующих записей. Core ограничивает черновик -100 изменениями и глубиной 5. `commit` возвращает только ID корневой записи, -в том числе при создании без права чтения. UI-поля preview/label/key не передаются. +ItemCommitDraft contains references, relations (attach/detach/create/links), and records for existing changes. Limits: 100 changes and depth 5. Commit returns only the root ID, including creation without read access. UI-only preview/label/key values are not sent. -Для защиты от перезаписи передайте исходные значения изменяемых полей: +For overwrite protection, supply original values of modified fields: ```ts const previous = await client.items.get("articles", "4"); await client.items.commit("articles", { id: "4", - values: { title: "Моя правка" }, + values: { title: "My edit" }, expectedValues: { title: previous.data.title }, }); ``` -При несовпадении Core возвращает `ApiError` со `status: 409` и -`code: "ITEM_CHANGED"`; весь commit откатывается. Сохраните локальный черновик, -прочитайте актуальные данные и предложите пользователю выбрать значение. -Автоматически повторять запрос с новым исходным значением нельзя: это заменит -чужую правку. Изменения разных полей не конфликтуют; уже применённое такое же -значение считается успешным. Для вложенных записей задайте их собственные -`expectedValues`, включая обновляемые FK. Проверка требует read/update каждого -сравниваемого поля. Пропуск параметра и обычный `items.update` сохраняют -прежнее безусловное поведение. JSON и массивы сравниваются целиком. +A mismatch returns ApiError with status:409 and code:ITEM_CHANGED and rolls back the commit. Keep the draft, fetch current data, and let the user choose. Do not automatically retry with a new baseline: that would overwrite another person's change. -## Участники страницы +Different-field edits do not conflict; an already-applied identical value succeeds. Nested records need their own expectedValues, including modified foreign keys. Every compared field needs read/update. Omitting expectedValues or using items.update retains unconditional behavior. JSON/arrays compare as whole values. + + + +## Page participants ```ts -const clientId = crypto.randomUUID(); // одно открытое окно +const clientId = crypto.randomUUID(); // one open window const scope = { kind: "record", collection: "articles", id: "4" } as const; const { data } = await client.presence.touch({ clientId, scope }); -// data.participants: ID, имя, аватар, число окон, self; data.total +// data.participants: ID, name, avatar, window count, self; data.total await client.presence.leave(clientId); ``` -`touch` продлевает отметку на 30 секунд и возвращает до 50 пользователей; -текущий пользователь идёт первым, окна одного человека объединяются. Повторяйте -вызов, пока окно открыто: этот HTTP API требует самостоятельного таймера. -Realtime API ниже обновляет присутствие сам. SDK сам не создаёт -таймеры. `leave` удаляет только окно текущей человеческой сессии и безопасен при -повторении. До 32 активных окон на одну сессию; превышение возвращает 429. +Touch extends a 30-second lease and returns up to 50 users, current user first, merging windows of one human. Repeat while the window is open: this legacy HTTP API has no SDK-managed timer. Realtime below maintains presence automatically. -Область — разрешённая `page`, `collection` либо `record`; каждый `touch` заново -проверяет права просмотра. Пользовательские поля, email, токены и ID сессий не -возвращаются. Сервисные principal не участвуют. При недоступной сети индикатор -может отставать до истечения отметки; присутствие не блокирует запись и не -показывает редактируемое поле. +Leave removes only the current human session's window and is idempotent. Sessions support up to 32 active windows; excess returns 429. Scopes are accessible page, collection, or record; each touch rechecks read access. Responses omit custom fields, email, tokens, and session IDs. Services do not participate. Network loss may leave presence until expiry; presence does not lock writes or identify active fields. ## Collaborative Live @@ -203,7 +163,7 @@ const unsubscribe = live.subscribe( { kind: "record", collection: "articles", id: "4" }, (event) => { if (event.type === "record.updated") { - // Перечитайте запись через items.get; payload не содержит значения полей. + // Reread with items.get; events contain no field values. void client.items.get(event.payload.collection, event.payload.recordId); } }, @@ -216,22 +176,20 @@ const lock = { clientId: crypto.randomUUID(), }; await live.locks.acquire(lock); -// Пока редактор открыт: await live.locks.refresh(lock) не реже 30 секунд. +// While editing, refresh the lock before its 30-second lease expires. await live.locks.release(lock); unsubscribe(); offState(); live.close(); ``` -`subscribe` работает для page, collection и record. Состояния — `connecting`, -`connected`, `reconnecting`, `offline`. SDK повторяет подписку с backoff и -jitter. После переподключения он отправляет `collection.changed`, чтобы клиент -перечитал данные. Повторные события с тем же ID в потоке пропускаются. Через -админку используется HttpOnly cookie; внешнему API-клиенту нужен пользовательский -Bearer token. Сервисный ключ для realtime не подходит. Блокировки — временный -UX-сигнал, а `items.commit` с `expectedValues` остаётся защитой записи. +Subscribe supports page, collection, and record scopes. States: connecting, connected, reconnecting, offline. SDK resubscribes with backoff/jitter and emits collection.changed after reconnect so clients reread. Duplicate event IDs within a stream are ignored. + +Admin browsers use HttpOnly cookies; external clients need human Bearer tokens. Service keys are unsupported. Locks are temporary UX signals; items.commit with expectedValues remains the write safeguard. + + -## Типизация коллекций +## Collection typing ```ts interface Schema { @@ -250,35 +208,19 @@ const result = await client.items.list("articles", { // result.data[0].title: string | undefined ``` -TypeScript проверяет имя коллекции, выбор и сортировку полей; возвращаемая -проекция учитывает `fields`. Поля конкретной схемы остаются опциональными: -их наличие зависит от прав. Неявно добавленный первичный ключ не выводится -из имени `id`: у коллекции может быть другой ключ. Чтобы получить его в типе -проекции, перечислите его в `fields`. - -Без `Schema` работает динамический режим со словарём JSON-значений. -В своей схеме задавайте `date` как `string` (`YYYY-MM-DD`), обычный `bigint` -как `string` с точным целым значением, а `integer` с вариантами — как `number` -или числовой union. SDK передаёт эти значения без преобразования в `Date`/`Number`. -Связанные пути в фильтрах пока проверяются Core во время запроса. -Схему можно описать вручную или получить из Core через CLI `asm connect`: -подробнее — [генерация типов и fluent-запросы](#generated-types-fluent-queries-and-plugin-methods). -SDK не валидирует структуру ответов во время исполнения; текущие права и -ограничения данных проверяет Core. - -При вручную описанной схеме данные `create`/`update` и корневые `values` в `commit` типизируются как -`Partial`: проверяются имена, типы и nullable из вашей схемы. -Обязательность при создании, значения по умолчанию, неизменяемые ключи и системные -поля проверяет Core по метаданным. Тип строки сам по себе их не описывает. -Сгенерированная схема отдельно описывает чтение, создание и обновление, включая -обязательные поля и доступные действия. Вложенные черновики пока используют -JSON-контракт без вывода типов связанных коллекций. - -`CurrentUser`, контракты записей, страниц, фильтров и ошибок переэкспортируются -из `@asmblyr-collaborative/contracts`. Core использует этот же пакет. Строки БД, хеши паролей -и внутренние модели Core не являются публичными типами SDK. - -## Ошибки и отмена +TypeScript checks collection names, selection, and sorting; projections reflect fields. Schema properties remain optional because access can hide them. A primary key is not inferred from the name id; collections may use another key. Include it explicitly in fields to have it in the projected type. + +Without Schema, the client uses dynamic JSON dictionaries. Model date as string (YYYY-MM-DD), ordinary bigint as an exact integer string, and integer choices as number or numeric unions. SDK never converts these to Date/Number. Core validates related filter paths at request time. + +Write a schema manually or obtain it with asm connect; see [generated types and fluent queries](#generated-types-fluent-queries-and-plugin-methods). SDK does not validate response structure at runtime; Core enforces current permissions and constraints. + +Manual schemas type create/update and root commit values as Partial<Schema[collection]>, checking names, types, and nullability. Core metadata still checks required create fields, defaults, immutable keys, and system fields. Generated schemas separately model read/create/update with required fields and available actions. Nested drafts still use the JSON contract without inferred related-collection types. + +CurrentUser and record/page/filter/error contracts are re-exported from @asmblyr-collaborative/contracts, also used by Core. Database rows, password hashes, and internal models are not public SDK types. + + + +## Errors and cancellation ```ts import { ApiError } from "@asmblyr-collaborative/sdk"; @@ -300,28 +242,17 @@ try { } ``` -HTTP-ошибка становится `ApiError`. Ошибки сети и отмены остаются исходными -ошибками `fetch`; локально некорректный путь или ID — `TypeError`. -Таймаут по умолчанию — 10 секунд, включая чтение тела ответа; `timeoutMs: 0` -отключает его. `request.signal` позволяет отменить конкретный запрос. -`request.timeoutMs` переопределяет таймаут одного вызова; редактор админки передаёт -`0` для commit, не ограничивая длительность запроса таймаутом SDK. -SDK не повторяет запросы автоматически и отклоняет HTTP-редиректы. -Ответы запрашиваются с `cache: "no-store"`. +HTTP failures become ApiError. Network/cancellation errors retain their fetch errors; locally invalid paths/IDs throw TypeError. Default timeout is ten seconds including body reads; timeoutMs:0 disables it. Request.signal cancels one call; request.timeoutMs overrides its timeout. The admin uses zero for commits so SDK does not limit save duration. + +Requests are never retried automatically, HTTP redirects are rejected, and fetch uses cache:no-store. Cancellation/connection failure does not prove rollback of a submitted write: inspect the result before retrying. -Отмена или ошибка соединения не подтверждает откат уже отправленной записи: -сервер мог успеть сохранить данные. Перед повтором проверьте результат. +Custom fetch, headers, and credentials are supported. Credentials defaults to same-origin; SDK does not configure server CORS. On servers, create clients per user/request to avoid sharing tokens across users. -Можно передать `fetch`, `headers` и `credentials` для собственного HTTP-окружения. -По умолчанию `credentials: "same-origin"`; SDK не настраивает CORS сервера. -На сервере создавайте клиент для конкретного пользователя/запроса, чтобы не -разделять его токен между пользователями. + -## Сборка и проверки +## Build and checks -Проверка установки архивов вне workspace: `pnpm packages:check`. -После выпуска `pnpm packages:check --registry` проверяет установку той же версии -из npm без локальных алиасов зависимостей. +Run pnpm packages:check to verify tarball installation outside the workspace. After publishing, pnpm packages:check --registry verifies the same version from npm without local aliases. ```sh pnpm --filter @asmblyr-collaborative/sdk build @@ -330,10 +261,11 @@ pnpm --filter @asmblyr-collaborative/sdk test node scripts/test.mjs core-sdk ``` -Интеграционный набор запускает Core на временном HTTP-порту с отдельной тестовой -БД и сравнивает SDK с вызовом через kit, включая ограничения прав. +Integration tests start Core on a temporary port with a disposable database and compare SDK/Kit behavior, including permissions. -## Переводы + + +## Translations ```ts const { data } = await client.translations.get("en"); @@ -341,13 +273,11 @@ console.log(data.core["appearance.ocean"]); console.log(data.schema.articles?.fields.title?.label); ``` -`GET /translations?locale=ru|en` (в админке `/api/translations`) требует -активной сессии или сервисного ключа. Ответ версии 1 содержит язык, -`fallbackLocale: "ru"`, плоский каталог `core`, каталоги активных `plugins` -по namespace и подписи доступной `schema`. Схема использует права каталога -коллекций, включая поля создания/изменения; это не разрешение читать записи. -Ответ не содержит записей, значений по умолчанию или настроек плагинов. -Пропуск языка выбирает русский; неподдержанный язык возвращает 400. +GET /translations?locale=ru|en (or /api/translations on the admin domain) requires an active human or service credential. Version 1 returns locale, fallbackLocale:ru, flat core strings, active plugin catalogs by namespace, and accessible schema labels. + +Schema visibility follows the collection catalog, including create/update metadata, without granting record reads. Responses contain no records, defaults, or plugin settings. Omitted locale defaults to Russian; unsupported locales return 400. + + ## Generated types, fluent queries and plugin methods @@ -394,10 +324,10 @@ The example assumes those fields exist. Aliases derive from technical collection names, independent of translated labels. Query builders are immutable. where calls combine with AND; predicates support and/or groups. exec returns rows; result returns the existing ItemListResult envelope. orderBy supports one field, matching -the Core API. `search("корм")` selects relevance unless a field order was already +the Core API. `search("feed")` selects relevance unless a field order was already explicitly chosen. `orderBy` selects field order; `orderByRelevance()` restores relevance and keeps the chosen field as a tie-breaker. The low-level list API -accepts `{ q: "корм", order: "relevance" }`, and `page.order` reports the effective +accepts `{ q: "feed", order: "relevance" }`, and `page.order` reports the effective mode. Relevance is calculated by Core before pagination using readable searchable fields and their configured priority, with exact, word-prefix, whole-word and substring matches. Without `q`, Core uses field order. diff --git a/packages/sdk/README.ru.md b/packages/sdk/README.ru.md new file mode 100644 index 0000000..955fc87 --- /dev/null +++ b/packages/sdk/README.ru.md @@ -0,0 +1,457 @@ +# @asmblyr-collaborative/sdk + + + +[English](README.md) · [Русский](README.ru.md) + + + +Типизированный HTTP-клиент Asmblyr для браузера и Node.js 22+. +Каждая операция вызывает основной API. В пакете нет H3, Knex, PostgreSQL, +компилятора TypeScript или загрузчика плагинов во время исполнения. +Для разработки расширений используется отдельный `@asmblyr-collaborative/kit`. + + + +## Профиль пользователя + +Собственный профиль доступен с пользовательским access token. Статус, права, +пароли и системные даты нельзя менять через профиль. Все новые поля необязательны. + +```ts +await client.users.updateMe({ + firstName: "Ivan", + lastName: "Example", + description: "About me", +}); +await client.users.updatePreferences({ timezone: "Asia/Yekaterinburg" }); +const { data: me } = await client.users.me(); +``` + +Дополнительные поля потребитель настраивает в обычной UUID-коллекции и выбирает +её в настройках пользователей. `asm connect` / `asm generate` включает её поля +в схему проекта. Для клиента с этой схемой: + +```ts +const { data: profile } = await client.users.extension("user_profiles"); +await client.users.saveExtension("user_profiles", { bio: "Hello" }); +``` + +`saveExtension` создаёт запись с ID текущего пользователя или обновляет её. +Для первого сохранения нужны `create` и `read`, затем `update` и `read`. +Действуют обычные правила полей и строк, права на связи и файлы. Передайте только +поля сгенерированной схемы; имя коллекции должно совпадать с выбранным расширением. +Скрытые поля могут отсутствовать в результате, а `data` — быть `null`. +После первого сохранения запись и её связи доступны через обычный `client.items` +и fluent API; произвольное создание профиля через `items.create` запрещено. + + + +## Подключение клиента + +Личные уведомления доступны через `client.notifications.list()`, `.read(id)` и +`.readAll(result.readBefore)`. Они требуют человеческой сессии; сервисные ключи +не дают доступ к личным входящим. Core хранит последние 200 событий пользователя. + +Предварительная версия устанавливается с тегом `beta`: + +```sh +npm install @asmblyr-collaborative/sdk@beta +npm install --save-dev @asmblyr-collaborative/cli@beta +``` + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; + +// В браузере: сессия админки остаётся в HttpOnly cookies. +const client = createClient({ baseUrl: "/api" }); + +const records = await client.items.list("articles", { + fields: ["id", "title"], + limit: 20, + page: 1, + sort: "title", + direction: "asc", +}); +const record = await client.items.get("articles", 4); +const profile = await client.users.me(); +``` + +`baseUrl` — корень API, например `http://localhost:3001` для прямого доступа +к Core или `http://localhost:3000/api` для браузерной админки. Префикс `/api` +автоматически не добавляется. Путь `/api` без origin работает в браузере. + +Для прямых запросов к Core передайте access token: + +```ts +const client = createClient({ + baseUrl: "https://asmblyr.example.com/api", + accessToken: () => currentAccessToken, + timeoutMs: 10_000, +}); +``` + +`accessToken` может быть строкой или асинхронной функцией. Функция вызывается для +каждого запроса. Поддерживаются access tokens пользователя и сервисного аккаунта; +права проверяет Core. `/users/me` предназначен для пользователя. +SDK не хранит токены в браузере, не выполняет вход и не обновляет токены сам. +В админке браузерная сессия хранится в HttpOnly cookie, которую проверяет Core. + + + +## Реализованные методы + +| Метод | HTTP | Ответ | +| ------------------------------------------------ | -------------------------------- | -------------------------- | +| `items.list(collection, options?, request?)` | `GET /items/:collection` | `{ data, labels, page }` | +| `items.get(collection, id, options?, request?)` | `GET /items/:collection/:id` | `{ data, label }` | +| `users.me(request?)` | `GET /users/me` | `{ data: CurrentUser }` | +| `items.create(collection, values, request?)` | `POST /items/:collection` | `{ data: row \| null }` | +| `items.update(collection, id, values, request?)` | `PATCH /items/:collection/:id` | `{ data: row \| null }` | +| `items.delete(collection, id, request?)` | `DELETE /items/:collection/:id` | `void` (204) | +| `items.commit(collection, draft, request?)` | `POST /items/:collection/commit` | `{ data: { id: string } }` | + +Остальные системные ресурсы и массовые операции пока не включены. +Прямого доступа к базе данных и переключения actor/superuser нет. + +`list` поддерживает `fields`, `page`, `limit`, `sort`, `direction`, `q`, `filter`. +SDK сериализует фильтр и query, Core проверяет значения, права и ограничения. +Фильтр — группа `{ logic: "and" | "or", children: [...] }`, значения условий — +строки или массивы строк. Поиск и фильтры по связям используют правила основного API. +Для списка предел страницы — 100 записей. `page.total` остаётся строкой. +Core дополнительно применяет условия permissions; SDK не передаёт доверенный +контекст. В одной странице условно разрешённые поля могут отсутствовать у части +записей. См. [права и ограничения](../../docs/ru/features/access.md). + +Без `fields` выдаются доступные поля. `fields: []` отправляется как `fields=` +и возвращает только первичный ключ. Ключ всегда добавляет Core. Явно выбранное +закрытое поле приводит к 403. Названия коллекций и полей — технические. +Подписи `label`/`labels` могут использовать другие разрешённые поля. + +Большие целые ID передавайте строками. SDK отклоняет небезопасные JS-числа. +Строковые ключи кодируются как один сегмент URL; ключи `.` и `..` не поддерживаются, +поскольку HTTP-клиенты нормализуют такие сегменты. Даты HTTP-ответов — ISO-строки, +значения `bigserial` — строки. SDK не преобразует их в `Date` или `number`. + + + +## Запись и сохранение связей + +```ts +const created = await client.items.create("articles", { title: "Статья" }); +await client.items.update("articles", 4, { title: "Новое название" }); +await client.items.delete("articles", 4); + +// Одна транзакция для основной записи и связанных изменений. +await client.items.commit("articles", { + id: "4", + values: { title: "Статья с категорией" }, + references: { category_id: { values: { title: "Новая категория" } } }, +}); +``` + +Core применяет права create/update/delete, ограничения полей, проверки связей, +валидацию и историю изменений. `create`/`update` возвращают только читаемые поля; +`{ data: null }` означает успешную запись без права чтения, а не ошибку. +`delete` разрешается на уровне записи, без списка удаляемых полей. + +Каждый вызов — отдельная транзакция. Последовательность `create` и `update` +не становится одной транзакцией. Для черновика со связями используется `commit`: +ошибка откатывает все его изменения и историю. Существующие записи в черновике +требуют права чтения; обычный `update` допускает право записи без чтения. + +`ItemCommitDraft` описывает `references`, `relations` (attach/detach/create/links) +и `records` — изменения существующих записей. Core ограничивает черновик +100 изменениями и глубиной 5. `commit` возвращает только ID корневой записи, +в том числе при создании без права чтения. UI-поля preview/label/key не передаются. + +Для защиты от перезаписи передайте исходные значения изменяемых полей: + +```ts +const previous = await client.items.get("articles", "4"); +await client.items.commit("articles", { + id: "4", + values: { title: "Моя правка" }, + expectedValues: { title: previous.data.title }, +}); +``` + +При несовпадении Core возвращает `ApiError` со `status: 409` и +`code: "ITEM_CHANGED"`; весь commit откатывается. Сохраните локальный черновик, +прочитайте актуальные данные и предложите пользователю выбрать значение. +Автоматически повторять запрос с новым исходным значением нельзя: это заменит +чужую правку. Изменения разных полей не конфликтуют; уже применённое такое же +значение считается успешным. Для вложенных записей задайте их собственные +`expectedValues`, включая обновляемые FK. Проверка требует read/update каждого +сравниваемого поля. Пропуск параметра и обычный `items.update` сохраняют +прежнее безусловное поведение. JSON и массивы сравниваются целиком. + + + +## Участники страницы + +```ts +const clientId = crypto.randomUUID(); // одно открытое окно +const scope = { kind: "record", collection: "articles", id: "4" } as const; +const { data } = await client.presence.touch({ clientId, scope }); +// data.participants: ID, имя, аватар, число окон, self; data.total +await client.presence.leave(clientId); +``` + +`touch` продлевает отметку на 30 секунд и возвращает до 50 пользователей; +текущий пользователь идёт первым, окна одного человека объединяются. Повторяйте +вызов, пока окно открыто: этот HTTP API требует самостоятельного таймера. +Realtime API ниже обновляет присутствие сам. SDK сам не создаёт +таймеры. `leave` удаляет только окно текущей человеческой сессии и безопасен при +повторении. До 32 активных окон на одну сессию; превышение возвращает 429. + +Область — разрешённая `page`, `collection` либо `record`; каждый `touch` заново +проверяет права просмотра. Пользовательские поля, email, токены и ID сессий не +возвращаются. Сервисные principal не участвуют. При недоступной сети индикатор +может отставать до истечения отметки; присутствие не блокирует запись и не +показывает редактируемое поле. + +## Collaborative Live + +```ts +const live = client.realtime.connect(); +const unsubscribe = live.subscribe( + { kind: "record", collection: "articles", id: "4" }, + (event) => { + if (event.type === "record.updated") { + // Перечитайте запись через items.get; payload не содержит значения полей. + void client.items.get(event.payload.collection, event.payload.recordId); + } + }, +); +const offState = live.onState((state) => console.log(state)); +const lock = { + collection: "articles", + recordId: "4", + field: "title", + clientId: crypto.randomUUID(), +}; +await live.locks.acquire(lock); +// Пока редактор открыт: await live.locks.refresh(lock) не реже 30 секунд. +await live.locks.release(lock); +unsubscribe(); +offState(); +live.close(); +``` + +`subscribe` работает для page, collection и record. Состояния — `connecting`, +`connected`, `reconnecting`, `offline`. SDK повторяет подписку с backoff и +jitter. После переподключения он отправляет `collection.changed`, чтобы клиент +перечитал данные. Повторные события с тем же ID в потоке пропускаются. Через +админку используется HttpOnly cookie; внешнему API-клиенту нужен пользовательский +Bearer token. Сервисный ключ для realtime не подходит. Блокировки — временный +UX-сигнал, а `items.commit` с `expectedValues` остаётся защитой записи. + + + +## Типизация коллекций + +```ts +interface Schema { + articles: { + id: number; + title: string; + created_at: string | null; + }; +} + +const client = createClient({ baseUrl: "/api" }); +const result = await client.items.list("articles", { + fields: ["title"], + sort: "id", +}); +// result.data[0].title: string | undefined +``` + +TypeScript проверяет имя коллекции, выбор и сортировку полей; возвращаемая +проекция учитывает `fields`. Поля конкретной схемы остаются опциональными: +их наличие зависит от прав. Неявно добавленный первичный ключ не выводится +из имени `id`: у коллекции может быть другой ключ. Чтобы получить его в типе +проекции, перечислите его в `fields`. + +Без `Schema` работает динамический режим со словарём JSON-значений. +В своей схеме задавайте `date` как `string` (`YYYY-MM-DD`), обычный `bigint` +как `string` с точным целым значением, а `integer` с вариантами — как `number` +или числовой union. SDK передаёт эти значения без преобразования в `Date`/`Number`. +Связанные пути в фильтрах пока проверяются Core во время запроса. +Схему можно описать вручную или получить из Core через CLI `asm connect`: +подробнее — [генерация типов и fluent-запросы](#генерируемые-типы-fluent-запросы-и-методы-плагинов). +SDK не валидирует структуру ответов во время исполнения; текущие права и +ограничения данных проверяет Core. + +При вручную описанной схеме данные `create`/`update` и корневые `values` в `commit` типизируются как +`Partial`: проверяются имена, типы и nullable из вашей схемы. +Обязательность при создании, значения по умолчанию, неизменяемые ключи и системные +поля проверяет Core по метаданным. Тип строки сам по себе их не описывает. +Сгенерированная схема отдельно описывает чтение, создание и обновление, включая +обязательные поля и доступные действия. Вложенные черновики пока используют +JSON-контракт без вывода типов связанных коллекций. + +`CurrentUser`, контракты записей, страниц, фильтров и ошибок переэкспортируются +из `@asmblyr-collaborative/contracts`. Core использует этот же пакет. Строки БД, хеши паролей +и внутренние модели Core не являются публичными типами SDK. + + + +## Ошибки и отмена + +```ts +import { ApiError } from "@asmblyr-collaborative/sdk"; + +try { + await client.items.get("articles", 4, undefined, { + signal: controller.signal, + }); +} catch (error) { + if (error instanceof ApiError) { + console.error( + error.status, + error.code, + error.requestId, + error.message, + error.details, + ); + } +} +``` + +HTTP-ошибка становится `ApiError`. Ошибки сети и отмены остаются исходными +ошибками `fetch`; локально некорректный путь или ID — `TypeError`. +Таймаут по умолчанию — 10 секунд, включая чтение тела ответа; `timeoutMs: 0` +отключает его. `request.signal` позволяет отменить конкретный запрос. +`request.timeoutMs` переопределяет таймаут одного вызова; редактор админки передаёт +`0` для commit, не ограничивая длительность запроса таймаутом SDK. +SDK не повторяет запросы автоматически и отклоняет HTTP-редиректы. +Ответы запрашиваются с `cache: "no-store"`. + +Отмена или ошибка соединения не подтверждает откат уже отправленной записи: +сервер мог успеть сохранить данные. Перед повтором проверьте результат. + +Можно передать `fetch`, `headers` и `credentials` для собственного HTTP-окружения. +По умолчанию `credentials: "same-origin"`; SDK не настраивает CORS сервера. +На сервере создавайте клиент для конкретного пользователя/запроса, чтобы не +разделять его токен между пользователями. + + + +## Сборка и проверки + +Проверка установки архивов вне workspace: `pnpm packages:check`. +После выпуска `pnpm packages:check --registry` проверяет установку той же версии +из npm без локальных алиасов зависимостей. + +```sh +pnpm --filter @asmblyr-collaborative/sdk build +pnpm --filter @asmblyr-collaborative/sdk typecheck +pnpm --filter @asmblyr-collaborative/sdk test +node scripts/test.mjs core-sdk +``` + +Интеграционный набор запускает Core на временном HTTP-порту с отдельной тестовой +БД и сравнивает SDK с вызовом через kit, включая ограничения прав. + + + +## Переводы + +```ts +const { data } = await client.translations.get("en"); +console.log(data.core["appearance.ocean"]); +console.log(data.schema.articles?.fields.title?.label); +``` + +`GET /translations?locale=ru|en` (в админке `/api/translations`) требует +активной сессии или сервисного ключа. Ответ версии 1 содержит язык, +`fallbackLocale: "ru"`, плоский каталог `core`, каталоги активных `plugins` +по namespace и подписи доступной `schema`. Схема использует права каталога +коллекций, включая поля создания/изменения; это не разрешение читать записи. +Ответ не содержит записей, значений по умолчанию или настроек плагинов. +Пропуск языка выбирает русский; неподдержанный язык возвращает 400. + + + +## Генерируемые типы, fluent-запросы и методы плагинов + +[CLI](../cli/README.ru.md) подключается через согласие в админке или API-токен +из окружения/stdin. Credentials не сохраняются. + +```sh +pnpm exec asm connect --url https://asmblyr.example.test +pnpm exec asm schema pull +pnpm exec asm generate +pnpm exec asm schema check --offline +``` + +GET /schema (также client.schema.pull()) экспортирует доступные wire-типы, +фактические права полей, виды фильтров и сгенерированные model-контракты плагинов. +Defaults, записи, секреты и условия политик исключены. Согласие браузера выдаёт +временный доступ только к схеме; рабочим запросам нужны отдельные credentials/сессия. + +Файл содержит обычные TS-типы и небольшое описание для браузера. Особый компилятор +или плагин сборщика не требуется. Node-only генератор изолирован в +@asmblyr-collaborative/sdk/schema. + +JSON с interface:tags экспортируется как свободный string[] (либо string[] | null), +без закрытого enum. После смены редактора выполните asm schema pull и asm generate. + +```ts +import { createClient } from "@asmblyr-collaborative/sdk"; +import { schema } from "./asmblyr.schema.js"; + +const client = createClient({ baseUrl: "/api", schema }); +const articles = await client.Articles.select((a) => [a.id, a.title]) + .where((a) => a.status.eq("published").and(a.price.gte("1000.00"))) + .orderBy((a) => a.created_at.desc()) + .limit(20) + .exec(); +// Аналогичный вход с подсказками имени коллекции: +const result = await client.collection("articles").limit(10).result(); +await client.items.create("articles", { title: "Hello" }); +// Если model handler калькулятора установлен и доступен: +const calculation = await client.plugins.calculator.calculate(input); +``` + +Пример предполагает наличие указанных полей. Псевдонимы выводятся из технических +имён независимо от переведённых подписей. Конструкторы запросов неизменяемы. +Where объединяется через AND; предикаты поддерживают and/or. Exec возвращает строки, +result — прежний ItemListResult. OrderBy поддерживает одно поле, как Core API. + +Search("корм") выбирает релевантность, если ранее не выбрана явная сортировка. +OrderBy включает порядок поля; orderByRelevance() возвращает релевантность и +сохраняет поле для равных совпадений. Низкоуровневый list принимает +{ q: "корм", order: "relevance" }; page.order показывает фактический режим. +Core ранжирует до пагинации по читаемым поисковым полям/приоритету: +точное совпадение, начало целого слова, целое слово, подстрока. Без q сортирует по полю. + +Limit принимает 1–100, также поддержаны page/search. Подсказки операторов зависят +от возможностей поля. JSON/multiselect не дают скалярных предикатов. Старые снимки +без filterKind предоставляют только консервативные equality/list-операторы. +Decimal/bigint остаются строками. + +Read/Create/Update разделяют обязательные create-поля, optional update, +управляемые поля и union вариантов. Ответ содержит выбранные ключи, но значения +остаются optional из-за прав. Core проверяет текущие права/row conditions при вызове. +Ручные схемы строк и items-клиент остаются совместимыми. + +Методы плагинов используют Kit JSON-схемы и исходные HTTP-маршруты без новой +регистрации. AccessGate фильтрует экспорт, выполнение перепроверяет доступ. +Старые действия без output-схемы пропускаются. Ограниченное JSON-подмножество +поддерживает закрытые объекты, массивы, литералы, union и примитивы; +произвольные refs, открытые объекты и defaults запрещены. Личное подключение +проверяется при вызове даже при наличии статического контракта. + +Materialized view имеет sourceKind:materialized-view и только read-действия. +Create/Update генерируются как never, delete недоступен. Fluent, items.list/get +работают. Core запрещает все записи, включая bulk/nested commit суперпользователя. +SQL-создание и refresh выполняются внешними средствами. + +Asm schema check проверяет хеши и изменения удалённого контракта. --offline +сверяет типы с сохранённой схемой; asm generate пересоздаёт их из снимка. +Имена, варианты и аннотации могут раскрывать структуру проекта: коммитьте их +только при допустимости. Npm-публикация — отдельное действие. +[Подготовка пакетов](../../docs/ru/reference/packages.md). diff --git a/scripts/docs/generate.mjs b/scripts/docs/generate.mjs index 05f25b6..deea341 100644 --- a/scripts/docs/generate.mjs +++ b/scripts/docs/generate.mjs @@ -4,6 +4,15 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { root, routeInventory } from "./route-inventory.mjs"; import { operationContract } from "./http-contracts.mjs"; +import { generateGuides } from "./guides.mjs"; +import { + checkPageCoverage, + httpText, + localizeSpec, + localizeTypeDoc, + markdownFiles, +} from "./localization.mjs"; +import { swaggerPage } from "./swagger-page.mjs"; import { itemCommitSchemas } from "./item-commit-contract.mjs"; const require = createRequire(path.join(root, "package.json")); @@ -38,14 +47,18 @@ async function output(relative, content) { const paths = {}; const delegated = []; -const rows = []; +const rows = { en: [], ru: [] }; for (const route of routes) { const key = `${route.method} ${route.path}`; const entry = catalog[key]; if (!entry.access || typeof entry.public !== "boolean") { throw new Error(`Invalid access declaration: ${key}`); } - rows.push(`| \`${key}\` | ${entry.access} | \`${route.source}\` |`); + for (const locale of ["en", "ru"]) { + rows[locale].push( + `| \`${key}\` | ${httpText(entry.access, locale)} | \`${route.source}\` |`, + ); + } if (route.method === "ALL") { delegated.push({ ...route, ...entry }); continue; @@ -85,7 +98,7 @@ for (const route of routes) { const spec = { openapi: "3.1.0", info: { - title: "Asmblyr Core HTTP API", + title: "Collaborative Core HTTP API", version: "0.0.0", description: "Все статические маршруты Core и их требования доступа. Часть операций имеет только route-only описание; спецификация не является полной схемой для генерации клиента. OAuth protocol и динамические маршруты плагинов описаны отдельно.", @@ -118,51 +131,38 @@ const spec = { }, "x-delegated-routes": delegated, }; -await output("docs/public/openapi.json", JSON.stringify(spec, null, 2) + "\n"); -await output( - "docs/reference/routes.md", - `# Матрица HTTP-маршрутов\n\nСгенерировано из Core и проверенного каталога доступа. ${routes.length} деклараций.\n\nЭто описание границ; их исполнение проверяют интеграционные тесты. Динамические маршруты плагинов и внутренние endpoints oidc-provider не перечисляются отдельно.\n\n| Метод и путь | Доступ | Источник в репозитории |\n| --- | --- | --- |\n${rows.join("\n")}\n`, -); - -const guides = { - "sdk-guide": "packages/sdk/README.md", - "cli-guide": "packages/cli/README.md", - "kit-guide": "packages/kit/README.md", - "kit-hooks": "packages/kit/HOOKS.md", - "kit-fields": "packages/kit/FIELDS.md", - "kit-lifecycle": "packages/kit/LIFECYCLE.md", - "kit-capabilities": "packages/kit/CAPABILITIES.md", - "kit-ui": "packages/kit/UI.md", - "plugin-color": "examples/plugins/color/README.md", -}; -for (const [name, source] of Object.entries(guides)) { - let content = await readFile(path.join(root, source), "utf8"); - content = content - .replace(/\((?:\.\/)?HOOKS\.md\)/g, "(./kit-hooks.md)") - .replace(/\((?:\.\/)?FIELDS\.md\)/g, "(./kit-fields.md)") - .replace(/\((?:\.\/)?LIFECYCLE\.md\)/g, "(./kit-lifecycle.md)") - .replace(/\((?:\.\/)?CAPABILITIES\.md\)/g, "(./kit-capabilities.md)") - .replace(/\((?:\.\/)?UI\.md\)/g, "(./kit-ui.md)") - .replaceAll("../sdk/README.md", "./sdk-guide.md") - .replaceAll("../cli/README.md", "./cli-guide.md") - .replaceAll("../../docs/reference/packages.md", "./packages.md") - .replaceAll( - "../../docs/development/plugin-actions.md", - "../development/plugin-actions.md", - ) - .replaceAll("../../examples/plugins/color/README.md", "./plugin-color.md") - .replaceAll("../../../packages/kit/FIELDS.md", "./kit-fields.md"); - if (name === "plugin-color") { - content = content.replaceAll( - "(../README.md)", - "(https://github.com/Asmblyr/Collaborative/blob/main/examples/plugins/README.md)", - ); - } +for (const locale of ["en", "ru"]) { + const prefix = locale === "ru" ? "ru/" : ""; + await output( + "docs/public/" + prefix + "openapi.json", + JSON.stringify(localizeSpec(spec, locale), null, 2) + "\n", + ); + await output("docs/public/" + prefix + "api/index.html", swaggerPage(locale)); + const intro = + locale === "ru" + ? "# Матрица HTTP-маршрутов\n\nСгенерировано из Core и проверенного каталога доступа. " + + routes.length + + " деклараций.\n\nЭто описание границ; их исполнение проверяют интеграционные тесты. Динамические маршруты плагинов и внутренние endpoints oidc-provider не перечисляются отдельно.\n\n| Метод и путь | Доступ | Источник в репозитории |" + : "# HTTP route matrix\n\nGenerated from Core and the reviewed access catalog. " + + routes.length + + " declarations.\n\nThis describes access boundaries; integration tests verify enforcement. Dynamic plugin routes and internal oidc-provider endpoints are not listed individually.\n\n| Method and path | Access | Source in the repository |"; await output( - `docs/reference/${name}.md`, - `\n\n${content.replaceAll("\r\n", "\n")}`, + "docs/" + prefix + "reference/routes.md", + intro + "\n| --- | --- | --- |\n" + rows[locale].join("\n") + "\n", ); } +await generateGuides(output); +await output( + "docs/public/assets/brand/collaborative-symbol-dark.svg", + ( + await readFile( + path.join(root, "docs/assets/brand/collaborative-symbol.svg"), + "utf8", + ) + ) + .replaceAll("\r\n", "\n") + .replaceAll("#1B2930", "#F5F3EE"), +); if (!check) { const swagger = path.dirname(require.resolve("swagger-ui-dist/package.json")); await mkdir(path.join(root, "docs/public/api"), { recursive: true }); @@ -177,6 +177,18 @@ if (!check) { path.join(root, "docs/public/api", file), ); } + await mkdir(path.join(root, "docs/public/assets/brand"), { recursive: true }); + for (const file of [ + "collaborative-symbol.svg", + "collaborative-logo-light.svg", + "collaborative-logo-dark.svg", + "collaborative-social.png", + ]) { + await cp( + path.join(root, "docs/assets/brand", file), + path.join(root, "docs/public/assets/brand", file), + ); + } for (const name of ["sdk", "kit"]) { const typedocRoot = path.dirname(require.resolve("typedoc/package.json")); const result = spawnSync( @@ -208,6 +220,17 @@ if (!check) { } } } +for (const name of ["sdk", "kit"]) { + const directory = path.join(root, "docs/reference", name); + for (const file of await markdownFiles(directory)) { + const relative = path.relative(directory, file).replaceAll("\\", "/"); + await output( + "docs/ru/reference/" + name + "/" + relative, + localizeTypeDoc(await readFile(file, "utf8")), + ); + } +} +await checkPageCoverage(); console.log( `Documentation ${check ? "checked" : "generated"}: ${routes.length} routes.`, ); diff --git a/scripts/docs/guides.mjs b/scripts/docs/guides.mjs new file mode 100644 index 0000000..86489e0 --- /dev/null +++ b/scripts/docs/guides.mjs @@ -0,0 +1,74 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { root } from "./route-inventory.mjs"; + +export const guides = { + "sdk-guide": "packages/sdk/README.md", + "cli-guide": "packages/cli/README.md", + "kit-guide": "packages/kit/README.md", + "kit-hooks": "packages/kit/HOOKS.md", + "kit-fields": "packages/kit/FIELDS.md", + "kit-lifecycle": "packages/kit/LIFECYCLE.md", + "kit-capabilities": "packages/kit/CAPABILITIES.md", + "kit-ui": "packages/kit/UI.md", + "plugin-color": "examples/plugins/color/README.md", +}; + +export function guideLinks(content, source, locale) { + const prefix = locale === "ru" ? "docs/ru" : "docs"; + return content.replace( + /(\[[^\]\n]+\]\()([^\s)]+)(\))/g, + (all, start, href, end) => { + if (/^(?:[a-z]+:|\/|#)/i.test(href)) return all; + const [address, fragment] = href.split("#"); + const resolved = path.posix.normalize( + path.posix.join(path.posix.dirname(source), address), + ); + const canonical = resolved + .replace("/ru/", "/") + .replace(/\.ru\.md$/, ".md"); + const guide = Object.entries(guides).find( + ([, file]) => file === canonical, + ); + let target; + if (guide) target = prefix + "/reference/" + guide[0] + ".md"; + else if (canonical.startsWith("docs/")) + target = prefix + canonical.slice(4); + else + return ( + start + + "https://github.com/Asmblyr/Collaborative/blob/main/" + + resolved + + (fragment ? "#" + fragment : "") + + end + ); + const relative = path.posix.relative(prefix + "/reference", target); + return start + relative + (fragment ? "#" + fragment : "") + end; + }, + ); +} + +export async function generateGuides(output) { + for (const locale of ["en", "ru"]) { + for (const [name, original] of Object.entries(guides)) { + const source = + locale === "ru" ? original.replace(/\.md$/, ".ru.md") : original; + const raw = await readFile(path.join(root, source), "utf8"); + const content = guideLinks( + raw + .replaceAll("\r\n", "\n") + .replace(/[\s\S]*?\n*/g, ""), + source, + locale, + ); + await output( + "docs/" + (locale === "ru" ? "ru/" : "") + "reference/" + name + ".md", + "\n\n" + + content.trimEnd() + + "\n", + ); + } + } +} diff --git a/scripts/docs/locales/http.en.json b/scripts/docs/locales/http.en.json new file mode 100644 index 0000000..2b8d32e --- /dev/null +++ b/scripts/docs/locales/http.en.json @@ -0,0 +1,219 @@ +{ + "Все статические маршруты Core и их требования доступа. Часть операций имеет только route-only описание; спецификация не является полной схемой для генерации клиента. OAuth protocol и динамические маршруты плагинов описаны отдельно.": "All static Core routes and access requirements. Some operations are route-only; this is not a complete client-generation schema. OAuth protocol and dynamic plugin routes are documented separately.", + "Публичный API на домене админки": "Public API on the admin domain", + "Самостоятельный Core": "Standalone Core", + "Активный человек с доступом к чату; только собственные сессии": "Active human with chat access; own conversations only", + "Личная история текущего пользователя": "Current user's personal history", + "Сессия отсутствует или принадлежит другому пользователю": "Conversation missing or owned by another user", + "Активный человек с доступом к чату; создаёт собственную сессию с пустым контекстом": "Active human with chat access; creates an own conversation with empty context", + "Новая сессия с пустым контекстом": "New conversation with empty context", + "Активный человек с доступом к чату; только собственная неактивная сессия": "Active human with chat access; own idle conversation only", + "Сессия, сообщения и краткий контекст удалены": "Conversation, messages, and summary deleted", + "В сессии ещё готовится ответ": "A response is still running in this conversation", + "Активный человек с доступом к чату; только собственная история, включая superuser": "Active human with chat access; own history only, including for superusers", + "Человек: superuser ИЛИ хотя бы один grant read/create/update; инструменты проверяют права отдельно": "Human: superuser OR at least one read/create/update grant; tools authorize separately", + "Контракт ответа смотрите в обработчике; эта операция описана на уровне маршрута и доступа.": "See the handler for the response contract; this operation documents route and access only.", + "Человек с доступом к ассистенту. conversationId/messageId/content сохраняют сообщение в собственной сессии; legacy messages остаётся stateless. dataAccess отдельно включает инструменты базы, Google и плагинов; workspaceId должен совпадать с context. Без dataAccess сохраняется прежнее поведение: инструменты только при context. История для модели изолирована по странице и режиму доступа. NDJSON выдаёт started/progress/activity/text-delta/answer/error.": "Human with assistant access. conversationId/messageId/content persist a message in an owned conversation; legacy messages is stateless. dataAccess separately enables database, Google, and plugin tools; workspaceId must match context. Without dataAccess, tools require context for compatibility. Model history is isolated by page and access mode. NDJSON emits started/progress/activity/text-delta/answer/error.", + "JSON-ответ либо поток NDJSON; в сохранённой сессии включает receipt": "JSON response or NDJSON stream; persisted conversations include a receipt", + "Сессия занята либо messageId уже отправлен с другим содержимым": "Conversation busy or messageId already used with different content", + "Лимит запросов, модели или контекста": "Request, model, or context limit", + "Контекст раздела, таблицы или сохранённой записи. Core проверяет workspace, MCP и права чтения. Данные черновика не принимаются.": "Section, table, or saved-record context. Core checks workspace, MCP, and read permissions. Draft values are not accepted.", + "Активный пользователь, точный Origin AUTH_UI_URL; proof в HttpOnly cookie.": "Active user, exact AUTH_UI_URL Origin; proof in an HttpOnly cookie.", + "URL для авторизации; browser proof хранится только в HttpOnly cookie": "Authorization URL; browser proof stays only in an HttpOnly cookie", + "HttpOnly; SameSite=Lax; Secure при HTTPS. Имя использует SESSION_COOKIE_PREFIX.": "HttpOnly; SameSite=Lax; Secure on HTTPS. Name uses SESSION_COOKIE_PREFIX.", + "Некорректный запрос": "Invalid request", + "Данные входа или browser proof недействительны": "Invalid credentials or browser proof", + "Origin не совпадает с AUTH_UI_URL": "Origin does not match AUTH_UI_URL", + "Лимит попыток": "Attempt limit", + "Точный Origin AUTH_UI_URL; credential rate limits для входа. HttpOnly session cookie; выход идемпотентен, отзывает только текущую сессию.": "Exact AUTH_UI_URL Origin; credential rate limits for sign-in. HttpOnly session cookie; logout is idempotent and revokes only the current session.", + "Браузерная сессия создана. API tokens не возвращаются.": "Browser session created. No API tokens returned.", + "Текущая сессия отозвана, cookies удалены": "Current session revoked and cookies removed", + "Успех": "Success", + "Подписанный GitLab CI assertion и активная федерация": "Signed GitLab CI assertion and active federation", + "Одноразовый invitation token и новый пароль": "One-time invitation token and new password", + "Одноразовый invitation/recovery token; создаёт человеческую сессию. Recovery отзывает прежние сеансы и способы входа": "One-time invitation/recovery token creates a human session. Recovery revokes prior sessions and sign-in methods", + "Истёкшая/использованная ссылка или отключённый пользователь": "Expired/consumed link or disabled user", + "Проверка email и пароля; ограничение частоты": "Email/password verification with rate limiting", + "Токены передаются напрямую, без обёртки data.": "Tokens are returned directly, without a data envelope.", + "Ошибка проверки, авторизации или исполнения; код HTTP определяет результат.": "Validation, authorization, or execution failure; HTTP status determines the outcome.", + "Refresh token в теле либо Bearer сессии": "Refresh token in body or session Bearer token", + "Человек, только собственный профиль/ресурс; workspace дополнительно проверяет видимость": "Human, own profile/resource only; workspace additionally checks visibility", + "Ошибка проверки, входа, доступа, конфликта или сервера": "Validation, authentication, access, conflict, or server error", + "Одноразовый challenge, подпись сохранённого passkey, точный origin/RP ID и user verification; активный пользователь": "One-time challenge, stored passkey signature, exact origin/RP ID, user verification, and active user", + "Публичный WebAuthn challenge, пять минут; общий credential rate limit": "Public five-minute WebAuthn challenge; shared credential rate limit", + "Публичные ID и подписи провайдеров": "Public provider IDs and labels", + "Действительный refresh token; ротация и защита от повторного использования": "Valid refresh token; rotation and replay protection", + "Действительный сервисный ключ": "Valid service key", + "Setup secret и отсутствие созданных пользователей; ограничение частоты": "Setup secret and no existing users; rate-limited", + "Публичный статус первой настройки": "Public initial-setup status", + "Проверка SSO flow, state/PKCE и ответа провайдера": "SSO flow, state/PKCE, and provider-response validation", + "Начало SSO; привязка требует действующей пользовательской сессии": "Start SSO; linking requires an active user session", + "Активный principal; только доступные коллекции и поля": "Active principal; accessible collections and fields only", + "Человек: superuser; системная и plugin-owned структура дополнительно защищена": "Human superuser; system and plugin-owned structure has additional protection", + "Активная человеческая сессия; удаляет собственные токены, flows и предложения, пытается отозвать Google grant": "Active human session; deletes own tokens, flows, and proposals and attempts Google grant revocation", + "Неверный OAuth flow или параметры": "Invalid OAuth flow or parameters", + "Нужна активная человеческая сессия": "Active human session required", + "Сервисным аккаунтам подключение недоступно": "Service accounts cannot connect", + "Предложение недоступно, истекло или принадлежит другому пользователю": "Proposal unavailable, expired, or owned by another user", + "Нужно переподключение, провайдер выключен или изменились исходные данные": "Reconnect required, provider disabled, or source data changed", + "Превышен лимит": "Limit exceeded", + "Провайдер или шифрование недоступны": "Provider or encryption unavailable", + "Активная человеческая сессия; состояние только собственного подключения, без токенов": "Active human session; own connection status without tokens", + "Редирект результата; подключение требует активную сессию, одноразовые state и browser proof, совпадение владельца.": "Result redirect; connection requires active session, one-time state/browser proof, and matching owner.", + "Проверены одноразовое состояние и browser proof; результат в перенаправлении. Для linking требуется текущая сессия.": "One-time state/browser proof validated; outcome in redirect. Linking requires the current session.", + "Публичный origin без /api; callback сохраняет зарегистрированный путь": "Public origin without /api; callback preserves its registered path", + "Активная человеческая сессия; собственный flow, browser proof, state/nonce/PKCE и та же конфигурация клиента": "Active human session; own flow, browser proof, state/nonce/PKCE, and unchanged client configuration", + "Активная человеческая сессия; одноразовый OAuth flow владельца с browser proof и PKCE": "Active human session; owner's one-time OAuth flow with browser proof and PKCE", + "Только для HttpOnly cookie UI, не для ассистента": "For the UI HttpOnly cookie only, not the assistant", + "Активная человеческая сессия; отмена собственного ожидающего предложения": "Active human session; cancel own pending proposal", + "Активная человеческая сессия; однократное подтверждение собственного предложения, перепроверка подключения и исходных данных; не MCP-инструмент": "Active human session; one-time confirmation of own proposal with connection/source revalidation; not an MCP tool", + "Ограниченный ответ Google: ID файла/таблицы или число изменённых ячеек": "Bounded Google response: file/spreadsheet ID or changed-cell count", + "Активный principal; метаданные включённых UI-плагинов": "Active principal; enabled UI-plugin metadata", + "Активный principal; подготовленный черновик только своего principal и namespace, с TTL": "Active principal; prepared draft bound to its principal and namespace, with TTL", + "Человек + files/read либо update; superuser bypass. Общая библиотека команды": "Human with files/read or update; superuser bypass. Shared team library", + "Человек + files/update; superuser bypass. Загрузка до 25 MiB, валидация метаданных, защита используемых ссылок": "Human with files/update; superuser bypass. Upload up to 25 MiB, metadata validation, and protection of used references", + "Активный principal: superuser, человек с files/read/update либо доступная ссылка в разрешённом поле записи; чужие файлы скрыты": "Active principal: superuser, human with files/read/update, or accessible reference in a readable record field; other files are hidden", + "Человек + files/update; superuser bypass. Изменение title/description и visibility private/public одного ready-файла; публикация и отзыв ссылки записываются в историю.": "Human with files/update; superuser bypass. Update title/description and private/public visibility of one ready file; publication/revocation is audited.", + "Обновлён ready-файл. visibility=private отзывает будущие публичные скачивания.": "Ready file updated. visibility=private revokes future public downloads.", + "Человек и доступ к коллекции; личный владелец, общие виды изменяет superuser": "Human with collection access; personal owner, shared views editable by superusers", + "Публичная проверка процесса": "Public process liveness check", + "Активный principal; действие и поля коллекции, связанные данные и история проверяются отдельно": "Active principal; collection action/fields, related data, and history checked separately", + "Имена через запятую; пустое значение возвращает только ключ. Пропуск — все доступные поля.": "Comma-separated names; empty value returns only the key. Omission selects accessible fields.", + "Буквальный поиск по читаемым настроенным полям и связям.": "Literal search over readable configured fields and relationships.", + "При q без явных sort/direction используется relevance; иначе field. Без q эффективный режим field. При relevance sort/direction разрешают равные совпадения.": "With q and no explicit sort/direction, relevance is used; otherwise field. Without q the effective mode is field. In relevance mode, sort/direction break ties.", + "JSON группы {logic: and|or, children: [...]}; это фильтр запроса, не grant.": "JSON group {logic: and|or, children: [...]}; a query filter, not an access grant.", + "Сохранено; data=null при отсутствии права чтения.": "Saved; data=null without read permission.", + "Удалено, без тела ответа.": "Deleted, no response body.", + "Активный principal; личные read/create/update и поля всех затронутых записей. expectedValues требует чтения обновляемых полей; конфликт откатывает весь commit": "Active principal; own read/create/update grants and fields of every affected record. expectedValues requires reading changed fields; conflict rolls back the whole commit", + "Конфликт, включая ITEM_CHANGED. Весь черновик и его история откатываются. Ответ не содержит закрытых значений.": "Conflict, including ITEM_CHANGED. Entire draft/history rolled back. Response excludes protected values.", + "Активная человеческая сессия; только публичный браузерный DSN и разрешённые флаги, без серверного DSN. Сервисные токены не допускаются": "Active human session; public browser DSN and permitted flags only, never server DSN. Service tokens rejected", + "Успех; private, no-store. Метрики только текущего экземпляра Core.": "Success; private, no-store. Metrics cover only the current Core instance.", + "Только публичный браузерный DSN; пустая строка при отключении. Серверный DSN не возвращается.": "Public browser DSN only; empty when disabled. Server DSN is never returned.", + "Нужна активная человеческая сессия; сервисные токены не допускаются.": "Active human session required; service tokens rejected.", + "База/конфигурация недоступна.": "Database/configuration unavailable.", + "Активная человеческая сессия; собственные уведомления включённых плагинов, доступ к каждой записи проверяется заново": "Active human session; own enabled-plugin notifications, with record access rechecked", + "Последние доступные уведомления; до 200 на пользователя": "Latest accessible notifications; up to 200 per user", + "Активная человеческая сессия; собственное уведомление и текущий доступ к записи": "Active human session; own notification and current record access", + "Прочитано": "Marked as read", + "Уведомление чужое, отсутствует или запись недоступна": "Notification foreign, missing, or record inaccessible", + "Активная человеческая сессия; только собственные уведомления до границы снимка списка": "Active human session; own notifications up to the list snapshot boundary", + "Некорректная граница снимка": "Invalid snapshot boundary", + "readBefore из ответа списка; будущие даты запрещены": "readBefore from the list response; future dates forbidden", + "Человек: oauth/update; superuser bypass": "Human with oauth/update; superuser bypass", + "Человек: oauth/read ИЛИ update; superuser bypass": "Human with oauth/read or update; superuser bypass", + "Человек; UID, собственная сессия, правила приложения и согласие": "Human; interaction UID, own session, application rules, and consent", + "Текущий пользователь, подписанные cookies взаимодействия OAuth и совпадающий Origin; идентификатор пользователя должен совпадать с сессией": "Current user, signed OAuth interaction cookies, and matching Origin; user ID must match the session", + "Решение принято; перейти на redirectTo": "Decision accepted; navigate to redirectTo", + "Человек: policies/read ИЛИ update; superuser bypass": "Human with policies/read or update; superuser bypass", + "Только человек-superuser: изменение состава политик и permissions": "Human superuser only: edit policy contents and permissions", + "Активный principal; только собственные права": "Active principal; own permissions only", + "Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; атомарная замена назначений без изменения себя и superuser": "Human superuser OR policies/update with policy in allowed set; atomic assignment replacement without changing self or superusers", + "Назначения заменены атомарно. Менеджер не меняет собственные назначения или назначения superuser.": "Assignments replaced atomically. Managers cannot change their own or superusers' assignments.", + "Человек: superuser ИЛИ policies/update + политика в разрешённом наборе; менеджер не меняет себя и superuser": "Human superuser OR policies/update with policy in allowed set; manager cannot change self or superusers", + "Активная человеческая сессия; каждый heartbeat проверяет read коллекции/строки или доступ раздела. /files требует files/read либо update у человека, или superuser. Возвращает только ID, имя, аватар и число окон; без email/сессий/значений записи": "Active human session; each heartbeat checks collection/row read or section access. Files requires human files/read or update, or superuser. Returns only ID, name, avatar, and window count; no email, sessions, or record values", + "Продление присутствия на 30 секунд и доступные участники. До 32 активных окон на сессию. Текущий пользователь идёт первым.": "Renew 30-second presence and return accessible participants. Up to 32 active windows per session. Current user first.", + "Количество разных пользователей, включая текущего.": "Distinct user count, including the current user.", + "Случайный UUID одной открытой страницы; пользователь определяется только по Bearer-сессии.": "Random UUID for one open page; identity is determined only by the authenticated session.", + "Активная человеческая сессия; удаляет только собственное окно этой сессии, без требования сохраняющегося read": "Active human session; remove only its own window without requiring continued read access", + "Собственное окно удалено; повтор безопасен. Другие сессии не затрагиваются.": "Own window removed; safe to repeat. Other sessions unaffected.", + "Без авторизации: только явно опубликованный ready-файл (visibility=public). Приватный/удалённый файл — 404; no-store/CSP/nosniff. Без списка и метаданных.": "Unauthenticated: explicitly public ready files only. Private/deleted files return 404; no-store/CSP/nosniff. No listing or metadata.", + "Показать только поддержанное растровое превью": "Show only supported raster previews", + "Байты явно опубликованного ready-файла; no-store, nosniff и ограничительная CSP. Attachment по умолчанию.": "Bytes of an explicitly public ready file; no-store, nosniff, restrictive CSP. Attachment by default.", + "Публичная проверка готовности, без секретов": "Public readiness check without secrets", + "Активная человеческая сессия; освобождает только собственную lease с совпадающим clientId, даже после потери read/update": "Active human session; release only own lease with matching clientId, even after losing read/update", + "Собственная lease освобождена или уже отсутствует": "Own lease released or already absent", + "Активная человеческая сессия; read и update указанного поля и строки. Атомарная 30-секундная lease, конфликт 409 FIELD_LOCKED": "Active human session; read/update on the specified field and row. Atomic 30-second lease; 409 FIELD_LOCKED on conflict", + "Получена или продлена lease поля": "Field lease acquired or renewed", + "Активная человеческая сессия через Bearer token или браузерную HttpOnly cookie; scope проверяется теми же collection/row read правилами, что GET /items. Поток повторно проверяет доступ и закрывается при его потере. Presence только для этой сессии": "Active human session through Bearer or HttpOnly cookie; scope uses the same collection/row read rules as GET /items. Stream rechecks access and closes when lost. Presence belongs only to this session", + "ID открытого окна": "Open-window ID", + "JSON scope из PresenceInput": "JSON scope from PresenceInput", + "SSE envelope; heartbeat-комментарии, событие presence.changed при подключении и изменении присутствия": "SSE envelope; heartbeat comments and presence.changed on connection/presence changes", + "Человек: services/read ИЛИ update; superuser bypass": "Human with services/read or update; superuser bypass", + "Человек: superuser ИЛИ services/update; все текущие и запрошенные политики сервиса в разрешённом наборе, включая ключи и федерации": "Human superuser OR services/update; all current/requested service policies must be in the allowed set, including key/federation operations", + "Последний успешный обмен ключа на токен": "Last successful key-to-token exchange", + "Последний успешный обмен или аутентификация HTTP-запроса токеном этого ключа": "Last successful exchange or HTTP authentication with this key's token", + "Точный bigint строкой: HTTP-запросы с проверенным токеном, включая последующий отказ в правах. Обмены, неверные и истёкшие токены не учитываются; история до включения статистики не восстанавливается.": "Exact bigint string: HTTP requests with validated tokens, including later permission denial. Exchanges, invalid/expired tokens excluded; activity predating statistics is not reconstructed.", + "Одноразовое значение; больше не возвращается": "One-time value; never returned again", + "Активный человек; разделы, режим изменения и разрешённый набор назначаемых политик из актуальной БД": "Active human; view/edit sections and allowed policy-assignment set from current database state", + "Только человек-superuser может менять состав политик и permissions.": "Only a human superuser may edit policy contents and permissions.", + "Явный набор готовых политик для назначения. Не даёт прав на соответствующий раздел и не назначает их пользователю. Superuser не ограничен этим списком.": "Explicit set of existing policies allowed for assignment. Grants neither section access nor those policies to the user. Superusers are not restricted by this list.", + "Человек: assistant/read ИЛИ update; superuser bypass": "Human with assistant/read or update; superuser bypass", + "Человек: assistant/update; superuser bypass": "Human with assistant/update; superuser bypass", + "Только активная человеческая сессия superuser; безопасные параметры и наличие ключей, без значений секретов": "Active human superuser only; safe parameters and key-presence flags, without secret values", + "Без пробелов, слешей и зарезервированного имени None.": "No whitespace, slashes, or reserved name None.", + "Неверные параметры": "Invalid parameters", + "Нужна человеческая сессия": "Human session required", + "Нужен superuser": "Superuser required", + "Провайдер или ключ шифрования недоступен; секреты не возвращаются": "Provider or encryption key unavailable; secrets are not returned", + "Только superuser; полный запрет изменения env-группы, revision, атомарное перешифрование при смене защиты": "Superuser only; environment groups cannot change; revision checks and atomic re-encryption on protection changes", + "Настройки изменились, группа заблокирована env, подключение выключено или хранилище содержит файлы": "Settings changed, group locked by env, connection disabled, or storage contains files", + "До 8000 байт UTF-8. Пропуск сохраняет прежний ключ; null удаляет его. Пустая строка запрещена.": "Up to 8000 UTF-8 bytes. Omission preserves the key; null removes it. Empty strings are forbidden.", + "Совместимый общий DSN мониторинга. Допускается отдельно от других ключей; задаёт общий DSN и очищает отдельные. null удаляет все DSN. Используйте serverDsn и browserDsn для независимой настройки.": "Compatible shared monitoring DSN. Supply separately from other keys; sets shared DSN and clears per-side values. Null removes all DSNs. Use serverDsn/browserDsn for independent configuration.", + "Только superuser; credential rate limit, проверка соединения без сохранения настроек": "Superuser only; credential rate limit; test without saving settings", + "Слишком много проверок": "Too many connection tests", + "Для подключения из env сервер проверяет текущую конфигурацию": "Environment-defined connections test the current server configuration", + "Только human superuser; локальные метрики текущего экземпляра Core, без DSN и пользовательских данных": "Human superuser only; current-instance local metrics, without DSNs or user data", + "Нужен superuser; право не делегируется.": "Superuser required; permission is not delegated.", + "Человек: policies; только проекция схемы без записей; read ИЛИ update соответствующего раздела": "Human with policies/read or update; schema projection without records", + "Человек: users ИЛИ policies ИЛИ services; только ID/названия; read ИЛИ update соответствующего раздела": "Human with users, policies, or services read/update; IDs and names only", + "Человек: oauth; только ID/email; read ИЛИ update соответствующего раздела": "Human with oauth/read or update; IDs and emails only", + "Человек: plugins/read ИЛИ update; superuser bypass": "Human with plugins/read or update; superuser bypass", + "Человек: plugins/update; superuser bypass": "Human with plugins/update; superuser bypass", + "Человек: terms/read ИЛИ update; superuser bypass": "Human with terms/read or update; superuser bypass", + "Человек: terms/update; superuser bypass": "Human with terms/update; superuser bypass", + "Точный Origin AUTH_UI_URL; настроенный провайдер; для link необходима активная сессия.": "Exact AUTH_UI_URL Origin; configured provider; linking requires an active session.", + "Перенаправление к провайдеру или на страницу ошибки; proof в HttpOnly cookie": "Redirect to provider or error page; proof in HttpOnly cookie", + "Провайдер не настроен": "Provider not configured", + "Одноразовые state/PKCE и browser proof; для link проверяется сессия владельца.": "One-time state/PKCE and browser proof; linking checks owner session.", + "Активная человеческая сессия суперпользователя. Только разрешённые системные сущности и зарегистрированные пользовательские поля; встроенные колонки защищены": "Active human superuser. Allowed system entities and registered custom fields only; built-in columns protected", + "Активный пользователь или сервисный аккаунт; схема ограничена доступными вызывающему коллекциями и полями, статические каталоги активных плагинов, без записей и defaults": "Active human or service account; schema limited to accessible collections/fields, static enabled-plugin catalogs, no records/defaults", + "Проверка входных данных, аутентификация или ошибка сервера": "Input validation, authentication, or server error", + "Человек: users/read или update ИЛИ policies/read или update; superuser bypass": "Human with users/read or update OR policies/read or update; superuser bypass", + "Человек: users/update; superuser bypass": "Human with users/update; superuser bypass", + "Человек: users/read ИЛИ update; superuser bypass": "Human with users/read or update; superuser bypass", + "Только человек-superuser: явный набор готовых политик, которые пользователь вправе назначать": "Human superuser only: explicit allowed set of existing policies for a user's assignments", + "Активный пользователь с users/read и обычными read-правами коллекции": "Active user with users/read and ordinary collection read grants", + "Активный пользователь с users/update и обычными read/create/update-правами коллекции": "Active user with users/update and ordinary collection read/create/update grants", + "Человек: superuser ИЛИ users/update; повторное приглашение только в пределах разрешённого набора, без доступа к аккаунтам с собственным делегированием": "Human superuser OR users/update; re-invitation within the allowed set only, excluding accounts with their own delegation", + "Активный пользователь с users/read; публичные сведения профиля без credentials": "Active user with users/read; public profile information without credentials", + "Активный пользователь с users/update; только поля профиля, без изменения статуса и привилегий": "Active user with users/update; profile fields only, without status/privilege changes", + "Активный человек с users/read или superuser; настроенная проекция профиля целевого пользователя": "Active human with users/read or superuser; configured target-user profile projection", + "Ошибка входа, доступа, проверки или сервера": "Authentication, access, validation, or server error", + "Свежий человек-superuser; только другой активный обычный пользователь. Одноразовая ссылка на 30 минут, аудит; не делегируется": "Fresh human superuser; another active ordinary user only. One-time 30-minute recovery link, audited and not delegated", + "Собственная человеческая сессия; приватное растровое изображение до 2 MiB, настроенное файловое хранилище и credential rate limit": "Own human session; private raster image up to 2 MiB, configured storage, and credential rate limit", + "Собственная человеческая сессия и обычные read-права коллекции; скрытые поля и строки не раскрываются": "Own human session and ordinary collection read grants; hidden rows/fields remain hidden", + "Собственная человеческая сессия; read и create/update, обычные правила строк, полей, связей и файлов": "Own human session; read plus create/update, with ordinary row/field/relation/file rules", + "Собственная активная человеческая сессия; только публичные сведения о ключах": "Own active human session; public passkey information only", + "Собственный session-bound challenge, свежий вход и WebAuthn verification; до десяти ключей": "Own session-bound challenge, recent sign-in, WebAuthn verification; up to ten passkeys", + "Собственный ключ, свежий человеческий вход; нельзя удалить последний способ входа": "Own passkey, recent human sign-in; cannot remove the last sign-in method", + "Собственный ключ удалён": "Own passkey removed", + "Последний способ входа нельзя удалить": "Cannot remove the last sign-in method", + "Собственная активная человеческая сессия не старше пяти минут": "Own active human session authenticated within five minutes", + "Собственная свежая человеческая сессия; только если пароля ещё нет": "Own fresh human session; only if no password exists", + "Первый пароль добавлен": "First password added", + "Пароль уже существует": "Password already exists", + "Активный человек; только собственные значения заранее настроенных selfVisible путей, без права произвольного чтения коллекций": "Active human; own values from preconfigured selfVisible paths only, without arbitrary collection reads", + "Только human superuser; конфигурация блока профиля": "Human superuser only; profile-block configuration", + "Только human superuser; явно разрешает ограниченную проекцию выбранных полей управляющим пользователями и при selfVisible владельцу профиля": "Human superuser only; explicitly grants a bounded projection to user managers and, with selfVisible, the profile owner", + "Только human superuser; разрешённые пользовательские поля и пути M2O, без данных записей": "Human superuser only; permitted custom fields and M2O paths without record data", + "Префикс одиночных связей через точку; пустой — дополнительные поля пользователя.": "Dot-separated to-one relation prefix; empty means user custom fields.", + "Активный пользователь с users/read; имя выбранной коллекции и ключа": "Active user with users/read; selected collection/key names", + "Только human superuser; связывает обычную UUID-коллекцию, не выдаёт права и не удаляет данные": "Human superuser only; binds an ordinary UUID collection without granting access or deleting data", + "Человек; только пространства с доступными коллекциями (superuser видит все)": "Human; workspaces containing accessible collections only (superusers see all)", + "Человек: superuser": "Human superuser", + "Одна транзакция; до 100 операций, глубина до 5. Существующая запись требует read. Каждая запись и поле проверяются по личным правам.": "One transaction; up to 100 operations and depth 5. Existing records require read. Each record/field uses the caller's permissions.", + "Исходные значения всех обновляемых полей, включая изменяемые FK. Требуют read. Несовпадение с актуальным значением возвращает ITEM_CHANGED (409), если новое значение ещё не применено. Пропуск сохраняет прежнее безусловное поведение.": "Original values for all changed fields, including foreign keys. Require read. Mismatch returns ITEM_CHANGED (409) unless the desired value is already applied. Omission preserves unconditional behavior.", + "HttpOnly browser session; префикс меняется SESSION_COOKIE_PREFIX. Записывающие запросы требуют Origin = AUTH_UI_URL. Cookie не отменяет права конкретной операции.": "HttpOnly browser session; SESSION_COOKIE_PREFIX changes its prefix. Mutations require Origin = AUTH_UI_URL. Cookies do not bypass operation permissions.", + "Непрозрачный access token пользователя или сервиса. Разделы настроек доступны только человеку.": "Opaque human or service access token. Settings sections require a human.", + "Протокольные endpoints oidc-provider: discovery/JWKS публичны, остальные используют проверки OAuth/OIDC": "oidc-provider protocol endpoints: discovery/JWKS are public; other routes use OAuth/OIDC checks", + "Человек: policies/read ИЛИ update; каталог приложений с управлением через политики, без секретов": "Human with policies/read or update; policy-managed application catalog without secrets", + "Активный пользователь или сервисный аккаунт: доступные коллекции, wire-типы и разрешения на поля. Без записей, defaults и условий политик; private/no-store; accepts scoped CLI schema:read access and includes permission-gated generated plugin model contracts": "Active human or service account: accessible collections, wire types, field grants. No records, defaults, or policy conditions; private/no-store. Accepts scoped CLI schema:read and includes permission-gated generated plugin model contracts", + "Активная человеческая сессия; собственное неистекшее предложение и то же активное подключение": "Active human session; own unexpired proposal and the same active connection", + "Человек-superuser; обнаружение существующих materialized views в public без регистрации/REFRESH; Core/plugin объекты исключены": "Human superuser; discovers existing public materialized views without registration/REFRESH, excluding Core/plugin objects", + "Человек-superuser; атомарное подключение заполненного public MV с поддерживаемыми полями и устойчивым уникальным ключом. Только метаданные отображения/расположения, без SQL/DDL/REFRESH; права другим пользователям не выдаются": "Human superuser; atomically imports a populated public materialized view with supported fields and a stable unique key. Presentation/location metadata only, no SQL/DDL/REFRESH or grants to others", + "Человек-superuser; отключает MV из каталога с проверкой зависимостей, удаляет метаданные/права; сохраняет PostgreSQL объект и данные": "Human superuser; dependency-checked catalog disconnection removes metadata/grants and preserves the PostgreSQL object/data", + "Public CLI discovery; configured admin consent URL only. No credential.": "Public CLI discovery; configured admin consent URL only. No credential.", + "Active human session approves a 60-second one-use PKCE S256 code for an exact loopback callback.": "Active human session approves a 60-second one-use PKCE S256 code for an exact loopback callback.", + "One-use code + PKCE verifier + exact callback. Issues 10-minute schema:read access only; rate-limited.": "One-use code + PKCE verifier + exact callback. Issues 10-minute schema:read access only; rate-limited.", + "Человеческая сессия: superuser или чтение раздела users; только ID и подпись пользователя, ограниченные поиск и пагинация; сервисные ключи запрещены": "Human session: superuser or users-section read; only user IDs/labels with bounded search/pagination; service keys forbidden" +} diff --git a/scripts/docs/locales/typedoc.ru.json b/scripts/docs/locales/typedoc.ru.json new file mode 100644 index 0000000..5d5f267 --- /dev/null +++ b/scripts/docs/locales/typedoc.ru.json @@ -0,0 +1,84 @@ +{ + "`Error.captureStackTrace()` was called.": "был вызван `Error.captureStackTrace()`.", + "Attach ordinary TS row types to a small, browser-safe field descriptor.": "Связывает обычные TypeScript-типы строк с небольшим описанием полей для браузера.", + "API root, e.g. https://example.com/api or /api in a browser.": "Корень API, например https://example.com/api или /api в браузере.", + "Defaults to 10 seconds. Zero disables the timeout.": "По умолчанию 10 секунд. Ноль отключает таймаут.", + "Rows only. Use result() when pagination and display labels are needed.": "Только строки. Для пагинации и подписей используйте result().", + "Public /users/me response, independent of the database's user row.": "Публичный ответ /users/me, независимый от строки пользователя в БД.", + "A record and its related changes, saved in one transaction. IDs are strings.": "Запись и связанные изменения в одной транзакции. ID передаются строками.", + "Original values of every field being updated. A mismatch returns ITEM_CHANGED (409).": "Исходные значения всех изменяемых полей. Несовпадение возвращает ITEM_CHANGED (409).", + "Omit for all readable fields. The primary key is always included.": "Пропустите для всех читаемых полей. Первичный ключ включается всегда.", + "Defaults to relevance for q without explicit sort/direction, otherwise field.": "По умолчанию relevance при q без явной сортировки; иначе field.", + "One-based page number. Defaults to 1.": "Номер страницы от 1. По умолчанию 1.", + "Labels may use readable fields outside the requested projection.": "Подписи могут использовать читаемые поля вне выбранной проекции.", + "Successful writes can return no data when the caller has no read permission.": "Успешная запись может вернуть пустые данные, если у вызывающего нет чтения.", + "Effective ordering; sort remains the physical tie-breaker.": "Фактический порядок; sort разрешает равные совпадения.", + "String to preserve PostgreSQL bigint precision.": "Строка сохраняет точность PostgreSQL bigint.", + "Join or renew a short-lived view; only human sessions may participate.": "Создаёт или продлевает краткоживущее присутствие; только человеческие сессии.", + "Number of open views belonging to this person, including other tabs.": "Число открытых представлений этого пользователя, включая другие вкладки.", + "Overrides the client timeout for this call. Zero disables it.": "Переопределяет таймаут одного вызова. Ноль отключает его.", + "User-owned records are initialized through /users/me/extension.": "Записи профиля создаются через /users/me/extension.", + "IANA zone; null follows the device's zone.": "Часовой пояс IANA; null использует пояс устройства.", + "Partial own-profile update. Account identity and access are never writable here.": "Частичное изменение своего профиля. Идентичность аккаунта и доступ здесь не изменяются.", + "The collection must match the installation's selected profile extension.": "Коллекция должна совпадать с выбранным расширением профиля установки.", + "The same operators and string operands as the /items filter API.": "Те же операторы и строковые операнды, что в фильтрах /items.", + "Also usable for in-process reads, whose dates may still be Date objects.": "Также подходит для чтения внутри процесса, где даты могут быть объектами Date.", + "Permissions can omit fields even when they are required in the collection schema.": "Права могут скрывать поля, даже если схема коллекции требует их наличия.", + "The `Error.stackTraceLimit` property specifies the number of stack frames": "Свойство Error.stackTraceLimit задаёт число кадров стека", + "collected by a stack trace (whether generated by `new Error().stack` or": "в трассировке, созданной через new Error().stack или", + "The default value is `10` but may be set to any valid JavaScript number. Changes": "По умолчанию 10; допускается любое корректное число JavaScript. Изменение", + "will affect any stack trace captured _after_ the value has been changed.": "влияет на трассировки, созданные после смены значения.", + "If set to a non-number value, or set to a negative number, stack traces will": "Нечисловое или отрицательное значение отключает", + "not capture any frames.": "сбор кадров стека.", + "Creates a `.stack` property on `targetObject`, which when accessed returns": "Создаёт свойство .stack объекта targetObject, возвращающее при чтении", + "a string representing the location in the code at which": "строку с местом в коде, в котором", + "The first line of the trace will be prefixed with": "Первая строка трассировки начинается с", + "The optional `constructorOpt` argument accepts a function. If given, all frames": "Необязательный constructorOpt принимает функцию. Если она задана, кадры", + "above `constructorOpt`, including `constructorOpt`, will be omitted from the": "выше constructorOpt, включая сам constructorOpt, исключаются из", + "generated stack trace.": "полученной трассировки.", + "The `constructorOpt` argument is useful for hiding implementation": "Аргумент constructorOpt позволяет скрыть детали", + "details of error generation from the user. For instance:": "создания ошибки от пользователя. Например:", + "https://v8.dev/docs/stack-trace-api#customizing-stack-traces": "https://v8.dev/docs/stack-trace-api#customizing-stack-traces", + "An expected client error. Unexpected failures should use ordinary Error.": "Ожидаемая ошибка клиента. Для неожиданных сбоев используйте обычный Error.", + "Used by the host loader for source and built packages. Not a second route registration.": "Используется загрузчиком исходных и собранных пакетов; это не второй реестр маршрутов.", + "Use defineModelContext\\(handler, defineModelAnnotation(...)).": "Используйте defineModelContext\\(handler, defineModelAnnotation(...)).", + "Describes a collection without validating it at runtime or changing the database.": "Описывает коллекцию без runtime-валидации и изменения базы.", + "Runs inside the mutation transaction. Throwing rolls back the whole operation.": "Выполняется внутри транзакции изменения. Исключение откатывает всю операцию.", + "Core validates and executes the plan in its installation transaction.": "Core проверяет и выполняет план в транзакции установки.", + "The builder resolves Input and the inner handler's return type before TS erases them.": "Сборщик определяет Input и результат внутреннего обработчика до стирания типов.", + "Describes a plugin without registering or running it.": "Описывает плагин без регистрации или запуска.", + "Calculations receive the same authenticated, restricted context over HTTP and MCP.": "Вычисления получают один аутентифицированный ограниченный контекст через HTTP и MCP.", + "Returns the authenticated host context; standalone H3 calls have no Asmblyr identity.": "Возвращает аутентифицированный контекст хоста; отдельный вызов H3 не имеет личности Collaborative.", + "The same caller-bound data API in regular endpoints and model handlers.": "Один API данных с правами вызывающего для обычных endpoints и model handlers.", + "Core validates and snapshots the current package's settings before invoking it.": "Core проверяет и фиксирует настройки пакета перед вызовом.", + "Bind an installed declaration to the current package's request-scoped storage.": "Связывает установленную декларацию с хранилищем текущего запроса пакета.", + "Record subscriptions and inbox publication; requires notifications.": "Подписки на записи и публикация уведомлений; требует notifications.", + "Requires storage.own and a namespace. Domain authorization belongs to the endpoint.": "Требует storage.own и namespace. Предметную авторизацию проверяет endpoint.", + "Holds a readable target row while updating related plugin data atomically.": "Удерживает читаемую целевую запись при атомарном изменении связанных данных плагина.", + "Declaration installed by Core within the owning plugin's namespace.": "Декларация устанавливается Core в namespace плагина-владельца.", + "Keys are field names; primary key and system dates are declared above.": "Ключи — имена полей; первичный ключ и системные даты объявлены выше.", + "Local name matching the filename; Core prefixes it with plugin_\\_.": "Локальное имя совпадает с файлом; Core добавляет plugin_\\_.", + "Navigation visibility only; this does not grant or restrict access.": "Только видимость навигации; не выдаёт и не ограничивает доступ.", + "Full rows only. Use raw storage for field projections.": "Только полные строки. Для проекций используйте исходный storage.", + "Core path without /api. The first segment belongs to this plugin.": "Путь Core без /api. Первый сегмент принадлежит плагину.", + "Reads use the caller's permissions and the current transaction.": "Чтение использует права вызывающего и текущую транзакцию.", + "Trusted lifecycle cleanup is limited to this package's collections.": "Доверенная очистка ограничена коллекциями этого пакета.", + "Bound to the caller of the current endpoint. Do not cache between requests.": "Привязано к вызывающему текущего endpoint. Не сохраняйте между запросами.", + "Pass large integer IDs as strings; numbers must be safe integers.": "Большие целые ID передавайте строками; числа должны быть безопасными целыми.", + "All operations run with the permissions and identity of the current caller.": "Все операции используют права и личность текущего вызывающего.", + "Immutable, ordered operations. Names refer only to this plugin's collections.": "Неизменяемые упорядоченные операции. Имена относятся только к коллекциям плагина.", + "Hide this tool until the current human has an active personal connection.": "Скрывает инструмент до активного личного подключения текущего человека.", + "Also prevents writes through the action's items capability. Defaults to false.": "Также запрещает запись через items действия. По умолчанию false.", + "Request-bound inbox events. Core chooses recipients from record subscriptions.": "Уведомления текущего запроса. Core выбирает получателей по подпискам на запись.", + "Owner-bound host broker. Never exposes credentials or arbitrary authenticated URLs.": "Ограниченный владельцем broker хоста. Не раскрывает credentials и произвольные авторизованные URL.", + "Creates a proposal only; Google writes require a separate human confirmation.": "Только создаёт предложение; запись в Google отдельно подтверждает человек.", + "Host-resolved contract for a JSON model handler.": "Контракт JSON model handler, разрешённый хостом.", + "Plain text excerpt; never HTML or a destination URL.": "Обычный текст, без HTML и адреса перехода.", + "Requires the notifications capability. Not available to model handlers.": "Требует notifications. Недоступно model handlers.", + "Auto-follows the human author unless explicitly muted; never notifies themself.": "Автоматически подписывает автора, если он явно не отказался; не уведомляет самого автора.", + "Remove an event owned by this plugin, e.g. when its comment is deleted.": "Удаляет событие этого плагина, например при удалении комментария.", + "Refresh a plain-text excerpt without delivering a new event.": "Обновляет текст выдержки без доставки нового события.", + "Full storage row. Datetimes are ISO strings; decimal and bigint precision is preserved.": "Полная строка хранилища. Datetime — ISO-строки; точность decimal/bigint сохраняется.", + "Endpoints and their action metadata are discovered in server/api.": "Endpoints и метаданные действий обнаруживаются в server/api.", + "Trusted server capability: plugin-owned collections only, local names, no SQL or actor override.": "Доверенная серверная возможность: только свои коллекции и локальные имена, без SQL и замены actor." +} diff --git a/scripts/docs/localization.mjs b/scripts/docs/localization.mjs new file mode 100644 index 0000000..ea859f7 --- /dev/null +++ b/scripts/docs/localization.mjs @@ -0,0 +1,192 @@ +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { root } from "./route-inventory.mjs"; + +const catalogs = {}; +for (const [name, file] of Object.entries({ + en: "http.en.json", + types: "typedoc.ru.json", +})) { + catalogs[name] = JSON.parse( + await readFile(new URL("./locales/" + file, import.meta.url), "utf8"), + ); +} + +const russianHttp = { + "Public CLI discovery; configured admin consent URL only. No credential.": + "Публичное обнаружение CLI; только настроенный URL согласия, без credentials.", + "Active human session approves a 60-second one-use PKCE S256 code for an exact loopback callback.": + "Активная человеческая сессия подтверждает одноразовый код PKCE S256 на 60 секунд для точного loopback callback.", + "One-use code + PKCE verifier + exact callback. Issues 10-minute schema:read access only; rate-limited.": + "Одноразовый код, PKCE verifier и точный callback. Только schema:read на 10 минут; действует лимит запросов.", +}; + +export function httpText(value, locale) { + if (locale === "ru") return russianHttp[value] ?? value; + if (!/[А-Яа-яЁё]/.test(value)) return value; + if (catalogs.en[value]) return catalogs.en[value]; + const source = value.match(/^(.*?)\. (Схемы тела.*? )?Источник: (.+)$/s); + if (source) { + const [, access, incomplete, file] = source; + return ( + httpText(access, locale) + + ". " + + (incomplete + ? "Body/response schemas are not yet detailed; see the handler. " + : "") + + "Source: " + + file + ); + } + throw new Error("Missing English HTTP translation: " + value); +} + +// Translate prose only. Identifiers, schemas, examples, and security values stay intact. +export function localizeSpec(value, locale) { + if (Array.isArray(value)) + return value.map((item) => localizeSpec(item, locale)); + if (!value || typeof value !== "object") return value; + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ + key, + ["example", "examples", "default", "enum", "const"].includes(key) + ? item + : ["description", "summary", "title", "access"].includes(key) && + typeof item === "string" + ? httpText(item, locale) + : localizeSpec(item, locale), + ]), + ); +} + +const headings = { + Classes: "Классы", + Interfaces: "Интерфейсы", + "Type Aliases": "Псевдонимы типов", + Functions: "Функции", + Variables: "Переменные", + "Type Parameters": "Параметры типов", + Parameters: "Параметры", + Returns: "Результат", + "Type Declaration": "Объявление типа", + Extends: "Наследует", + Constructors: "Конструкторы", + Constructor: "Конструктор", + Properties: "Свойства", + Methods: "Методы", + "Inherited from": "Унаследовано от", + Overrides: "Переопределяет", + See: "См. также", + Deprecated: "Устарело", + "Call Signature": "Сигнатура вызова", + "Index Signature": "Индексная сигнатура", + "Index Signatures": "Индексные сигнатуры", + Example: "Пример", +}; +const kinds = { + "Type Alias": "Псевдоним типа", + Function: "Функция", + Interface: "Интерфейс", + Class: "Класс", + Variable: "Переменная", +}; +const columns = { + Name: "Имя", + Type: "Тип", + Description: "Описание", + "Type Parameter": "Параметр типа", + "Default type": "Тип по умолчанию", + "Default value": "Значение по умолчанию", + Parameter: "Параметр", + Function: "Функция", + Interface: "Интерфейс", + Class: "Класс", + "Type Alias": "Псевдоним типа", + Variable: "Переменная", +}; + +export function localizeTypeDoc(content) { + let fenced = false; + const anchors = new Map(); + return content + .split("\n") + .map((line) => { + if (line.startsWith("```")) { + fenced = !fenced; + return line; + } + if (fenced) return line; + if (catalogs.types[line]) return catalogs.types[line]; + const heading = line.match(/^(#+) (.+)$/); + if (heading) { + const [, level, title] = heading; + const base = title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + const occurrence = anchors.get(base) ?? 0; + anchors.set(base, occurrence + 1); + const id = base + (occurrence ? "-" + occurrence : ""); + const anchor = " {#" + id + "}"; + if (headings[title]) return level + " " + headings[title] + anchor; + for (const [from, to] of Object.entries(kinds)) { + if (title.startsWith(from + ": ")) + return level + " " + to + title.slice(from.length) + anchor; + } + } + if (line.startsWith("|")) { + return line + .split("|") + .map((cell) => { + const text = cell.trim(); + const translation = columns[text] ?? catalogs.types[text]; + if (translation) return " " + translation + " "; + if (/^[A-Za-z]+\s+[A-Za-z]/.test(text)) { + throw new Error("Missing Russian TypeDoc translation: " + text); + } + return cell; + }) + .join("|"); + } + if (/^[A-Za-z]/.test(line) && !line.startsWith("https://")) { + throw new Error("Missing Russian TypeDoc translation: " + line); + } + return line; + }) + .join("\n"); +} + +export async function markdownFiles(directory) { + const result = []; + for (const item of await readdir(directory, { withFileTypes: true })) { + if ([".vitepress", "public"].includes(item.name)) continue; + const target = path.join(directory, item.name); + if (item.isDirectory()) result.push(...(await markdownFiles(target))); + else if (item.name.endsWith(".md")) result.push(target); + } + return result.sort(); +} + +export function assertPairs(english, russian) { + const missing = english.filter((name) => !russian.includes(name)); + const orphaned = russian.filter((name) => !english.includes(name)); + if (missing.length || orphaned.length) { + throw new Error( + "Documentation locale coverage: missing RU " + + missing.join(", ") + + "; missing EN " + + orphaned.join(", "), + ); + } +} + +export async function checkPageCoverage() { + const directory = path.join(root, "docs"); + const files = (await markdownFiles(directory)).map((file) => + path.relative(directory, file).replaceAll("\\", "/"), + ); + assertPairs( + files.filter((file) => !file.startsWith("ru/")), + files.filter((file) => file.startsWith("ru/")).map((file) => file.slice(3)), + ); +} diff --git a/scripts/docs/localization.test.mjs b/scripts/docs/localization.test.mjs new file mode 100644 index 0000000..6f2c41a --- /dev/null +++ b/scripts/docs/localization.test.mjs @@ -0,0 +1,79 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + httpText, + localizeSpec, + localizeTypeDoc, + assertPairs, +} from "./localization.mjs"; +import { guideLinks } from "./guides.mjs"; + +test("OpenAPI translation preserves executable contract and fails on missing prose", () => { + const original = { + description: "Успех", + security: [{ bearerAuth: [] }], + properties: { + title: { type: "string", enum: ["Успех"], example: "Успех" }, + }, + example: { description: "Успех", title: "Успех" }, + }; + const localized = localizeSpec(original, "en"); + assert.equal(localized.description, "Success"); + assert.deepEqual(localized.security, original.security); + assert.deepEqual(localized.properties, original.properties); + assert.deepEqual(localized.example, original.example); + assert.equal(original.description, "Успех"); + assert.throws( + () => httpText("Новая непереведённая операция", "en"), + /Missing/, + ); +}); + +test("route source and route-only disclaimer survive translation", () => { + assert.equal( + httpText( + "Успех. Схемы тела и ответа пока не детализированы; см. реализацию обработчика. Источник: handler.ts", + "en", + ), + "Success. Body/response schemas are not yet detailed; see the handler. Source: handler.ts", + ); +}); + +test("guide links resolve source relationships in the selected locale", () => { + assert.equal( + guideLinks( + "[SDK](../sdk/README.ru.md#пример) [Data](../../docs/ru/features/data.md)", + "packages/kit/README.ru.md", + "ru", + ), + "[SDK](sdk-guide.md#пример) [Data](../features/data.md)", + ); + assert.equal( + guideLinks("[Hooks](HOOKS.md)", "packages/kit/README.md", "en"), + "[Hooks](kit-hooks.md)", + ); +}); + +test("locale coverage catches missing and orphaned pages", () => { + assertPairs(["a.md"], ["a.md"]); + assert.throws(() => assertPairs(["a.md"], []), /missing RU a.md/); + assert.throws(() => assertPairs([], ["b.md"]), /missing EN b.md/); +}); + +test("TypeDoc keeps code and signatures while translating headings", () => { + assert.equal( + localizeTypeDoc("## Parameters\n\n```ts\nEnglish code\n```"), + "## Параметры {#parameters}\n\n```ts\nEnglish code\n```", + ); + assert.throws( + () => localizeTypeDoc("A new undocumented comment."), + /Missing Russian/, + ); + assert.match(localizeTypeDoc("## Returns\n## Returns"), /\{#returns-1\}/); + assert.match( + localizeTypeDoc( + "| Description |\n| Successful writes can return no data when the caller has no read permission. |", + ), + /Успешная запись/, + ); +}); diff --git a/scripts/docs/swagger-page.mjs b/scripts/docs/swagger-page.mjs new file mode 100644 index 0000000..21109f5 --- /dev/null +++ b/scripts/docs/swagger-page.mjs @@ -0,0 +1,32 @@ +// Swagger's operation names remain API identifiers. Localized prose comes from the spec. +export function swaggerPage(locale) { + const ru = locale === "ru"; + const assets = ru ? "../../api/" : "./"; + return ` + + + + + Collaborative HTTP API + + + + + +
+ + + + +`; +} From d91a6854a904de2a67f5efed5b4244eb3ee856f7 Mon Sep 17 00:00:00 2001 From: Ivan Chikishev Date: Fri, 9 Oct 2026 03:44:06 +0500 Subject: [PATCH 3/5] docs: localize CLI access descriptions in Russian OpenAPI --- scripts/docs/localization.mjs | 8 +++++++- scripts/docs/localization.test.mjs | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/docs/localization.mjs b/scripts/docs/localization.mjs index ea859f7..f111dcc 100644 --- a/scripts/docs/localization.mjs +++ b/scripts/docs/localization.mjs @@ -22,7 +22,13 @@ const russianHttp = { }; export function httpText(value, locale) { - if (locale === "ru") return russianHttp[value] ?? value; + if (locale === "ru") { + if (russianHttp[value]) return russianHttp[value]; + const prefix = Object.keys(russianHttp).find((text) => + value.startsWith(text + ". "), + ); + return prefix ? russianHttp[prefix] + value.slice(prefix.length) : value; + } if (!/[А-Яа-яЁё]/.test(value)) return value; if (catalogs.en[value]) return catalogs.en[value]; const source = value.match(/^(.*?)\. (Схемы тела.*? )?Источник: (.+)$/s); diff --git a/scripts/docs/localization.test.mjs b/scripts/docs/localization.test.mjs index 6f2c41a..1835c4f 100644 --- a/scripts/docs/localization.test.mjs +++ b/scripts/docs/localization.test.mjs @@ -39,6 +39,15 @@ test("route source and route-only disclaimer survive translation", () => { ); }); +test("Russian OpenAPI localizes an English access declaration with its source suffix", () => { + const description = httpText( + "Public CLI discovery; configured admin consent URL only. No credential.. Источник: apps/core/src/cli-auth/routes.ts", + "ru", + ); + assert.match(description, /^Публичное обнаружение CLI/); + assert.match(description, /Источник: apps\/core\/src\/cli-auth\/routes.ts$/); +}); + test("guide links resolve source relationships in the selected locale", () => { assert.equal( guideLinks( From 6c05d3e60e633d4badf60094b95c8a376b01a387 Mon Sep 17 00:00:00 2001 From: Ivan Chikishev Date: Fri, 9 Oct 2026 03:46:47 +0500 Subject: [PATCH 4/5] docs: keep Russian anchors compatible with GitHub and VitePress --- docs/ru/development/operations.md | 2 ++ docs/ru/guide/deployment.md | 2 +- docs/ru/reference/http.md | 2 +- docs/ru/reference/kit-guide.md | 2 +- docs/ru/reference/sdk-guide.md | 2 ++ packages/kit/README.ru.md | 2 +- packages/sdk/README.ru.md | 2 ++ 7 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/ru/development/operations.md b/docs/ru/development/operations.md index e6da8c3..0d52633 100644 --- a/docs/ru/development/operations.md +++ b/docs/ru/development/operations.md @@ -29,6 +29,8 @@ rollback миграций не заменяет восстановление. + + ## Копии локальной Docker-установки Следующие инструменты обслуживают только `deploy/local/compose.yaml`: diff --git a/docs/ru/guide/deployment.md b/docs/ru/guide/deployment.md index 9dec64e..5c43067 100644 --- a/docs/ru/guide/deployment.md +++ b/docs/ru/guide/deployment.md @@ -66,6 +66,6 @@ PostgreSQL 17+ и S3-совместимое хранилище подключа Старые миграции не редактируются; `db:rollback` может удалить данные и не заменяет восстановление из копии. Откат образа возможен только при совместимой схеме. Для дополнительного локального стенда доступны -[команды backup/restore и проверка статуса](../development/operations.md#копии-локальнои-docker-установки). +[команды backup/restore и проверка статуса](../development/operations.md#копии-локальной-docker-установки). Рекомендации по защите установки — в [границах безопасности](../security/overview.md). diff --git a/docs/ru/reference/http.md b/docs/ru/reference/http.md index 6aa0462..70fdcea 100644 --- a/docs/ru/reference/http.md +++ b/docs/ru/reference/http.md @@ -15,7 +15,7 @@ Для первого запроса и типизированного клиента начните с [подключения SDK](sdk-guide.md#подключение-клиента). Там же описаны [фильтры и пагинация](sdk-guide.md#реализованные-методы), -[сохранение связей](sdk-guide.md#запись-и-сохранение-связеи) и +[сохранение связей](sdk-guide.md#запись-и-сохранение-связей) и [ошибки](sdk-guide.md#ошибки-и-отмена). Поведение коллекций и пределы поиска описаны в [данных](../features/data.md), права — в [матрице доступа](../security/access-matrix.md). diff --git a/docs/ru/reference/kit-guide.md b/docs/ru/reference/kit-guide.md index 48eb83c..e53dd30 100644 --- a/docs/ru/reference/kit-guide.md +++ b/docs/ru/reference/kit-guide.md @@ -193,7 +193,7 @@ await items.delete("articles", 4); обновляемых полей, включая FK. Core проверяет их под блокировкой строки; несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. - Полный формат общий с [HTTP SDK](sdk-guide.md#запись-и-сохранение-связеи). + Полный формат общий с [HTTP SDK](sdk-guide.md#запись-и-сохранение-связей). - Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, 409 — конфликт связи/ключа или вторая запись в single-коллекции. diff --git a/docs/ru/reference/sdk-guide.md b/docs/ru/reference/sdk-guide.md index 40d6d94..801ed45 100644 --- a/docs/ru/reference/sdk-guide.md +++ b/docs/ru/reference/sdk-guide.md @@ -131,6 +131,8 @@ Core дополнительно применяет условия permissions; S + + ## Запись и сохранение связей ```ts diff --git a/packages/kit/README.ru.md b/packages/kit/README.ru.md index 1c42b66..5bd62f5 100644 --- a/packages/kit/README.ru.md +++ b/packages/kit/README.ru.md @@ -197,7 +197,7 @@ await items.delete("articles", 4); обновляемых полей, включая FK. Core проверяет их под блокировкой строки; несовпадение возвращает `ITEM_CHANGED` (409) и откатывает весь черновик. Сравниваемые поля требуют read/update. Пропуск сохраняет прежнее поведение. - Полный формат общий с [HTTP SDK](../sdk/README.ru.md#запись-и-сохранение-связеи). + Полный формат общий с [HTTP SDK](../sdk/README.ru.md#запись-и-сохранение-связей). - Ошибки: 400 — значения или поля, 403 — права, 404 — отсутствующая запись, 409 — конфликт связи/ключа или вторая запись в single-коллекции. diff --git a/packages/sdk/README.ru.md b/packages/sdk/README.ru.md index 955fc87..f446dd9 100644 --- a/packages/sdk/README.ru.md +++ b/packages/sdk/README.ru.md @@ -135,6 +135,8 @@ Core дополнительно применяет условия permissions; S + + ## Запись и сохранение связей ```ts From 330fff15f78da5dffcc98eab254624c200fdd01b Mon Sep 17 00:00:00 2001 From: Ivan Chikishev Date: Fri, 9 Oct 2026 04:04:34 +0500 Subject: [PATCH 5/5] docs: allow localized readmes in package archives --- scripts/package-smoke.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 5798d64..a523fc0 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -58,7 +58,12 @@ for (const [name, folders] of Object.entries(allowed)) { }); for (const entry of entries.trim().split(/\r?\n/)) { const file = entry.replace(/^package\//, ""); - const topLevel = ["package.json", "README.md", "LICENSE"].includes(file); + const topLevel = [ + "package.json", + "README.md", + "README.ru.md", + "LICENSE", + ].includes(file); assert.ok( topLevel || folders.some((folder) => file.startsWith(folder)), `${name}: unexpected packed file ${file}`,