@ykurilov/remontnik-browser 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -2
- package/dist/actions/index.d.ts +33 -0
- package/dist/actions/index.js +383 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/attempts.d.ts +11 -0
- package/dist/attempts.js +26 -0
- package/dist/attempts.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +210 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/egress/index.d.ts +43 -0
- package/dist/egress/index.js +401 -0
- package/dist/egress/index.js.map +1 -0
- package/dist/guard/index.d.ts +101 -0
- package/dist/guard/index.js +298 -0
- package/dist/guard/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/live/control.d.ts +34 -0
- package/dist/live/control.js +111 -0
- package/dist/live/control.js.map +1 -0
- package/dist/live/page/index.d.ts +2 -0
- package/dist/live/page/index.js +200 -0
- package/dist/live/page/index.js.map +1 -0
- package/dist/live/server.d.ts +25 -0
- package/dist/live/server.js +267 -0
- package/dist/live/server.js.map +1 -0
- package/dist/live/stream.d.ts +39 -0
- package/dist/live/stream.js +82 -0
- package/dist/live/stream.js.map +1 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.js +19 -0
- package/dist/log.js.map +1 -0
- package/dist/policy/index.d.ts +43 -0
- package/dist/policy/index.js +80 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/profiles/blocked-journal.d.ts +8 -0
- package/dist/profiles/blocked-journal.js +32 -0
- package/dist/profiles/blocked-journal.js.map +1 -0
- package/dist/profiles/index.d.ts +31 -0
- package/dist/profiles/index.js +131 -0
- package/dist/profiles/index.js.map +1 -0
- package/dist/service/index.d.ts +15 -0
- package/dist/service/index.js +86 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/start.d.ts +31 -0
- package/dist/service/start.js +90 -0
- package/dist/service/start.js.map +1 -0
- package/dist/snapshot/index.d.ts +28 -0
- package/dist/snapshot/index.js +100 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/render.d.ts +17 -0
- package/dist/snapshot/render.js +82 -0
- package/dist/snapshot/render.js.map +1 -0
- package/dist/vault/index.d.ts +34 -0
- package/dist/vault/index.js +146 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/output-filter.d.ts +5 -0
- package/dist/vault/output-filter.js +89 -0
- package/dist/vault/output-filter.js.map +1 -0
- package/package.json +36 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yury Kurilov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ykurilov/remontnik-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Свой браузер врача Ремонтника: снимки страниц с номерами элементов, действия, сохранённые входы, живой экран владельца. Основа — `playwright-core` (точная версия) и Chromium. Пароль никогда не попадает к модели: ни в снимок, ни в ответ, ни в журнал.
|
|
4
|
+
|
|
5
|
+
## Запуск
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
remontnik-browser serve
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Окружение:
|
|
12
|
+
|
|
13
|
+
| Переменная | Что это |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `REMONTNIK_BROWSER_TOKEN` | ключ врача, не короче 16 знаков |
|
|
16
|
+
| `REMONTNIK_BROWSER_VAULT_KEY` | ключ хранилища входов: 32 байта в base64 |
|
|
17
|
+
| `REMONTNIK_BROWSER_DATA_DIR` | профили, входы (`logins.json`), снимки экрана |
|
|
18
|
+
| `REMONTNIK_BROWSER_POLICY` | путь к JSON: `allowedOrigins`, `extraOrigins`, `allowedMutations`, `profiles` |
|
|
19
|
+
| `REMONTNIK_BROWSER_LISTEN` | адрес службы, по умолчанию `127.0.0.1:7700`; «все адреса» — только явно (в образе — `0.0.0.0:7700`) |
|
|
20
|
+
| `REMONTNIK_BROWSER_LIVE_LISTEN` | адрес живого экрана, только частная сеть; не задан — живого экрана нет |
|
|
21
|
+
| `REMONTNIK_BROWSER_OWNER_TOKEN` | ключ владельца, не совпадает с ключом врача |
|
|
22
|
+
| `REMONTNIK_BROWSER_URL` | для консольных команд: адрес службы, по умолчанию `http://127.0.0.1:7700` |
|
|
23
|
+
|
|
24
|
+
Политика:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"allowedOrigins": ["https://app.example.com"],
|
|
29
|
+
"extraOrigins": ["https://cdn.example.net"],
|
|
30
|
+
"allowedMutations": [{ "method": "POST", "pathPattern": "/api/chat" }],
|
|
31
|
+
"profiles": [{ "name": "remontnik", "viewport": { "width": 1280, "height": 800 } }]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`extraOrigins` — сторонние адреса, с которых страницы приложения грузят картинки, скрипты и шрифты (CDN). Точные origin, без масок и путей, только `https://` или `http://`, не внутренние адреса. После первого ввода пароля сеанс профиля заперт до конца: всё, чего нет в `allowedOrigins` и `extraOrigins`, не грузится. Переходы, фреймы и WebSocket — только `allowedOrigins`. Неизвестное поле (опечатка вроде `extraOrgins`) — ошибка настройки. Не вписывайте сюда счётчики аналитики: туда пароль мог бы уйти. Проверка файла без запуска службы: `remontnik-browser check-policy [путь]`; её же зовёт `remontnik-doctor check-config`.
|
|
36
|
+
|
|
37
|
+
В `pathPattern` знак `*` — один кусок пути, `**` — весь остаток. Прокси задаётся в профиле (`proxy.server`, имя и пароль — через имена переменных `usernameEnv`, `passwordEnv`).
|
|
38
|
+
|
|
39
|
+
Образ: `deploy/browser.Dockerfile`. Порт службы публикуйте только во внутреннюю сеть compose или на частный адрес, не на все адреса хоста.
|
|
40
|
+
|
|
41
|
+
## HTTP
|
|
42
|
+
|
|
43
|
+
`POST /v1/<команда>`, заголовок `Authorization: Bearer <ключ врача>`, тело — JSON. `GET /health` — без ключа.
|
|
44
|
+
|
|
45
|
+
| Команда | Вход | Успех |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `open` | `{ profile }` | `{ ok }` — запускает профиль |
|
|
48
|
+
| `read`, `back` | `{ profile }` | `{ ok, url, title, snapshot }` |
|
|
49
|
+
| `url` | `{ profile, url }` | то же |
|
|
50
|
+
| `click` | `{ profile, ref, danger }` | то же |
|
|
51
|
+
| `type` | `{ profile, ref, text, submit?, danger }` | то же |
|
|
52
|
+
| `key` | `{ profile, key }` | то же |
|
|
53
|
+
| `scroll` | `{ profile, direction, px? }` | то же |
|
|
54
|
+
| `shot` | `{ profile }` | `{ ok, path }` — JPEG, поля пароля под маской |
|
|
55
|
+
| `logins` | `{ site? }` | `{ ok, logins: [{ id, site, login, profile }] }` |
|
|
56
|
+
| `fill-password` | `{ profile, entryId, ref }` | снимок |
|
|
57
|
+
| `status` | `{ profile }` | `{ ok, state: ready \| busy \| needs_human, reason? }` |
|
|
58
|
+
|
|
59
|
+
После входа паролем сеанс заперт: подресурс вне `allowedOrigins` и `extraOrigins` (счётчик, CDN) не грузится, но действие не роняет. Снимок тогда приходит с `blocked` — по строке на закрытый сайт, не больше 10: что вписать в `extraOrigins`, если сайт нужен приложению. Сайт — домен без поддоменов и порта, числовой адрес — без цифр: имя хоста выбирает скрипт страницы, и пароль в поддомене дальше не идёт. Врач показывает модели только число строк, сами строки — владельцу в отчёте. Попытка страницы унести пароль на самом `fill-password` — отказ `refused_leak`; сообщение говорит, что пароль уже вписан и замок взведён.
|
|
60
|
+
|
|
61
|
+
Отказ — `{ ok: false, code, message }`:
|
|
62
|
+
|
|
63
|
+
| `code` | Когда |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `refused_origin` | сайт вне `allowedOrigins`; элемент во фрейме чужого сайта; вход из хранилища от другого сайта |
|
|
66
|
+
| `refused_danger` | `danger` = `send`, `pay`, `delete`, `publish`, или цель сама похожа на опасное действие |
|
|
67
|
+
| `refused_mutation` | изменяющий запрос вне `allowedMutations` (в `message` — метод и путь) |
|
|
68
|
+
| `refused_leak` | `fill-password`: страница сразу пыталась унести пароль на адрес вне `allowedOrigins` и `extraOrigins` |
|
|
69
|
+
| `refused_field` | `fill-password` не в поле пароля |
|
|
70
|
+
| `refused_profile` | вход из хранилища от другого профиля |
|
|
71
|
+
| `refused_human_control` | управление у владельца |
|
|
72
|
+
| `needs_human` | капча или запрос одноразового кода; пока человек её не прошёл — любая страничная команда, кроме `read` и `shot` |
|
|
73
|
+
| `not_found` | нет профиля, номера элемента или входа |
|
|
74
|
+
| `bad_request` | неверный вход, например нет `danger` |
|
|
75
|
+
| `timeout` | страница не ответила вовремя |
|
|
76
|
+
|
|
77
|
+
HTTP-коды: неверный ключ — 401; больше 10 неверных ключей за минуту с одного адреса — 429 с `code: too_many_attempts`; неизвестная команда — 404; тело не объект — 400; сбой службы — 500 с `code: internal`.
|
|
78
|
+
|
|
79
|
+
## Консоль
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
remontnik-browser read -p remontnik
|
|
83
|
+
remontnik-browser click -p remontnik --ref e12 --danger none
|
|
84
|
+
remontnik-browser logins add --site https://app.example.com --login demo@example.com --profile remontnik --password-stdin
|
|
85
|
+
remontnik-browser logins list
|
|
86
|
+
remontnik-browser logins remove 3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Добавить и удалить вход можно только в консоли на сервере браузера. Пароль читается только из стандартного ввода; `--password` отклоняется.
|
|
90
|
+
|
|
91
|
+
## Живой экран
|
|
92
|
+
|
|
93
|
+
`GET /live?profile=<профиль>` на `REMONTNIK_BROWSER_LIVE_LISTEN`. Вход по ключу владельца, куки `HttpOnly`, `SameSite=Strict`. Не больше 5 неверных ключей в минуту с одного адреса: чужие ошибки из той же сети владельцу вход не закрывают.
|
|
94
|
+
|
|
95
|
+
У куки нет флага `Secure`: живой экран работает по обычному HTTP. Открывайте его только по частному шифрованному каналу (например, Tailscale) или за прокси с TLS. `POST` и поток живой экран принимает только со своей страницы: чужой или пустой (`null`) `Origin` — `403`. За прокси с TLS прокси должен ставить `x-forwarded-proto: https` (и `x-forwarded-host`, если меняет `Host`): тогда своя страница — `https://<хост>`.
|
|
96
|
+
|
|
97
|
+
С первого вписанного пароля и до конца сеанса профиля браузер закрывает любые соединения с адресами вне `allowedOrigins` и `extraOrigins` (`refused_leak`) во всём контексте: и с новых страниц, и из новых окон. Страница могла спрятать пароль в `localStorage`, cookie или `window.name`, поэтому замок не снимается ни новой страницей, ни перезапуском службы: метка лежит в папке профиля. В журнале службы отказ называет сайт (`domain`) и подсказывает вписать его точный адрес в `extraOrigins`, если он нужен приложению. Запись или WebSocket на другой сайт и пароль в адресе закрытого запроса — ещё и событие `leak_attempt`. Решает прокси внутри службы, через который идёт весь трафик Chromium, поэтому скрипт страницы его не обходит: ни фоновыми скриптами (`SharedWorker`), ни WebRTC, ни WebTransport. UDP мимо прокси закрыт флагами Chromium, имена хостов разрешает прокси. Прокси профиля (`proxy`) стоит за ним. Внутренние адреса (петля, частные сети, метаданные облака) вне `allowedOrigins` этот прокси не пускает и без пароля: подресурс оттуда просто не грузится, в журнале службы — `refused_internal`. Сам прокси требует имя и пароль, свои на каждый запуск. Остатки риска — `docs/security.md`, «Пароли браузера».
|
|
98
|
+
|
|
99
|
+
«Взять управление» и «Вернуть» идут тем же потоком, что и ввод, поэтому «Вернуть» не обгоняет набранный текст. Когда владелец нажал «Вернуть», найденная до этого капча или запрос кода считаются пройденными. Кадры идут по `WS /live/stream`, ввод владельца доходит, только пока он взял управление. Пока управление у владельца, врач получает `refused_human_control`. Нет связи с владельцем 2 минуты — управление возвращается врачу. Замки сайтов и запросов действуют и при ручном управлении.
|
|
100
|
+
|
|
101
|
+
## Что служба перепроверяет сама
|
|
102
|
+
|
|
103
|
+
Модель могут сбить текстом страницы, поэтому то, что она заявляет, служба проверяет сама. При сомнении — отказ.
|
|
104
|
+
|
|
105
|
+
- **Опасные действия.** Перед `click`, `type` с `submit` и `key` Enter или Space служба смотрит на саму цель: подпись, текст, адрес ссылки или формы. Для отправки формы — её кнопки отправки, для клавиши — элемент в фокусе. Оплата, удаление, публикация, отправка сообщения людям (по-русски и по-английски) отклоняются, что бы ни было указано в `danger`. Обычное «Отправить» не опасно: так уходит форма или сообщение боту.
|
|
106
|
+
- **Капча и коды.** После `needs_human` врачу доступны только `read` и `shot`, пока владелец не вернёт управление с живого экрана или новый `read` уже не увидит проверки.
|
|
107
|
+
- **Фреймы.** Фрейм тоже грузится только с `allowedOrigins`; пустой фрейм наследует сайт родителя. Фрейм чужого сайта в снимке — только пометка `[фрейм=скрыт: сайт вне allowedOrigins]`, его элементы действий не принимают.
|
|
108
|
+
- **Сайт входа.** `fill-password` вписывает только в точный origin входа, без поддоменов, и только по `https:`. Исключение — `localhost` и `127.0.0.1` для проверок.
|
|
109
|
+
|
|
110
|
+
Чего сетевой замок не ловит: запрос `GET` с побочным действием на сайте и любой запрос, подходящий под слишком широкий шаблон `allowedMutations` (например, `/**`). Шаблоны держите узкими.
|
|
111
|
+
|
|
112
|
+
## Решение T092: номера элементов
|
|
113
|
+
|
|
114
|
+
Закреплённая `playwright-core` 1.64.1 сама даёт снимок с номерами: `page.ariaSnapshot({ mode: 'ai' })` выдаёт `[ref=e12]`, а `page.getByRef('e12')` находит элемент. Номер того же элемента не меняется между снимками. Внутри фреймов номера вида `f1e2`.
|
|
115
|
+
|
|
116
|
+
Берём встроенные номера и дерево (`ariaSnapshotJSON`), а текст снимка собираем сами. Причина: встроенный текст показывает значение поля пароля открыто и не отличает поле пароля от обычного поля. Свой обход DOM с `data-remontnik-ref` не нужен: он менял бы страницу и повторял готовое.
|
|
117
|
+
|
|
118
|
+
Как скрываем пароль:
|
|
119
|
+
|
|
120
|
+
- Поле с `type=password` или `autocomplete=current-password`/`new-password` помечается «[поле пароля]» и остаётся помеченным, даже если страница сменит тип поля (кнопка «глаз»).
|
|
121
|
+
- Поле, куда служба вписала пароль, помечается «[пароль вписан]» навсегда.
|
|
122
|
+
- Поле, значение которого содержит сохранённый пароль, тоже помечается «[пароль вписан]» и закрывается маской. Так скрыто и поле, которое сайт пересоздал после вписывания (перерисовка при «показать пароль»).
|
|
123
|
+
- Значения таких полей в снимке — «[скрыто]», на снимке экрана — под маской.
|
|
124
|
+
- Последняя страховка — фильтр выхода. Он вырезает все сохранённые пароли из ответов и журналов: как есть, со схлопнутыми пробелами, в JSON, в HTML, в `%xx`-кодировании любого регистра, в hex и в base64. Если фильтр сломался, текст не выводится вовсе. От преобразований самой страницей (перевёрнутый пароль, часть пароля, своё шифрование) фильтр не защищает.
|
|
125
|
+
- Пароль короче 8 знаков не сохраняется: его нельзя надёжно вырезать из текста.
|
|
126
|
+
|
|
127
|
+
## Замки паролей
|
|
128
|
+
|
|
129
|
+
1. Каждый вход шифруется AES-256-GCM; сайт, профиль и логин входят в проверку подлинности.
|
|
130
|
+
2. Пароль попадает в хранилище только из стандартного ввода и не короче 8 знаков.
|
|
131
|
+
3. `fill-password` вписывает только в поле пароля и только на точном сайте входа по `https:`, в том числе внутри фрейма.
|
|
132
|
+
4. Команды выполнения своего кода на странице нет.
|
|
133
|
+
5. Снимок скрывает значения.
|
|
134
|
+
6. Снимок экрана закрывает поля маской.
|
|
135
|
+
7. Фильтр выхода вырезает пароли из всего, что уходит наружу.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ControlLock } from '../live/control.js';
|
|
2
|
+
import type { ServiceLog } from '../log.js';
|
|
3
|
+
import type { SitePolicy } from '../policy/index.js';
|
|
4
|
+
import type { Profiles } from '../profiles/index.js';
|
|
5
|
+
import type { LoginVault } from '../vault/index.js';
|
|
6
|
+
export type ActionResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
[field: string]: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
export interface BrowserCommandRunner {
|
|
15
|
+
run(command: string, input: Record<string, unknown>): Promise<ActionResult>;
|
|
16
|
+
}
|
|
17
|
+
export declare const BROWSER_COMMANDS: readonly ["open", "url", "back", "read", "click", "type", "key", "scroll", "shot", "logins", "fill-password", "status"];
|
|
18
|
+
export type BrowserCommand = (typeof BROWSER_COMMANDS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* Закрытые сайты запертого сеанса — по строке на сайт из `siteOf`, для отчёта владельцу: что вписать в extraOrigins,
|
|
21
|
+
* если сайт нужен приложению. Модели врач показывает только их число.
|
|
22
|
+
*/
|
|
23
|
+
export declare function lockedSummary(requests: readonly {
|
|
24
|
+
domain?: string;
|
|
25
|
+
}[]): string[];
|
|
26
|
+
export declare function createBrowserActions(options: {
|
|
27
|
+
profiles: Profiles;
|
|
28
|
+
vault: LoginVault;
|
|
29
|
+
policy: SitePolicy;
|
|
30
|
+
shotsDir: string;
|
|
31
|
+
control: ControlLock;
|
|
32
|
+
log: ServiceLog;
|
|
33
|
+
}): BrowserCommandRunner;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// Команды врача над профилем: открыть, перейти, снимок, клик, ввод, клавиша, прокрутка, снимок экрана,
|
|
2
|
+
// входы и вписывание пароля. Команды одного профиля идут по очереди. Своего кода на странице нет.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { LOCKED_HINT, checkDanger, findHumanCheck, isAllowedOrigin, isFrameAllowed, isLoginOrigin, refuseDangerousTarget } from '../guard/index.js';
|
|
7
|
+
import { refuseIfHumanControl } from '../live/control.js';
|
|
8
|
+
import { takeSnapshot } from '../snapshot/index.js';
|
|
9
|
+
export const BROWSER_COMMANDS = ['open', 'url', 'back', 'read', 'click', 'type', 'key', 'scroll', 'shot', 'logins', 'fill-password', 'status'];
|
|
10
|
+
const fail = (code, message) => ({ ok: false, code, message });
|
|
11
|
+
const LOCKED_SUMMARY_LIMIT = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Закрытые сайты запертого сеанса — по строке на сайт из `siteOf`, для отчёта владельцу: что вписать в extraOrigins,
|
|
14
|
+
* если сайт нужен приложению. Модели врач показывает только их число.
|
|
15
|
+
*/
|
|
16
|
+
export function lockedSummary(requests) {
|
|
17
|
+
const sites = [...new Set(requests.flatMap((request) => (request.domain ? [request.domain] : [])))];
|
|
18
|
+
const lines = sites.slice(0, LOCKED_SUMMARY_LIMIT).map((site) => `${site}: закрыт после входа паролем; ${LOCKED_HINT}`);
|
|
19
|
+
if (sites.length > LOCKED_SUMMARY_LIMIT)
|
|
20
|
+
lines.push(`ещё ${sites.length - LOCKED_SUMMARY_LIMIT} сайтов закрыто`);
|
|
21
|
+
if (requests.some((request) => !request.domain))
|
|
22
|
+
lines.push('другие отказы — в журнале службы');
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
const REF = /^(?:f\d+)?e\d+$/;
|
|
26
|
+
const ACTION_TIMEOUT_MS = 5000;
|
|
27
|
+
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
28
|
+
const SCROLL_DIRECTIONS = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] };
|
|
29
|
+
const PASSWORD_FIELDS = 'input[type="password" i], input[autocomplete~="current-password" i], input[autocomplete~="new-password" i]';
|
|
30
|
+
class Refused extends Error {
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Пока страница ждёт человека (капча, код), врачу можно только смотреть. */
|
|
38
|
+
const ALLOWED_WHILE_HUMAN_CHECK = new Set(['open', 'read', 'shot']);
|
|
39
|
+
const SUBMIT_KEYS = /(?:^|\+)(?:Enter|NumpadEnter|Space| )$/;
|
|
40
|
+
/**
|
|
41
|
+
* Что видит человек у цели действия: подписи, текст, адрес ссылки или формы. Для отправки формы — её кнопки
|
|
42
|
+
* отправки, для клавиши — элемент в фокусе. Выполняется на странице, поэтому без внешних имён.
|
|
43
|
+
*/
|
|
44
|
+
function describeTarget(element, mode) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
const describe = (target) => {
|
|
47
|
+
if (!target)
|
|
48
|
+
return;
|
|
49
|
+
const isField = target instanceof HTMLTextAreaElement || (target instanceof HTMLInputElement && !['submit', 'button', 'reset', 'image'].includes(target.type));
|
|
50
|
+
for (const attribute of ['aria-label', 'title', 'alt', 'id', 'name', 'href', 'formaction', ...(isField ? [] : ['value'])]) {
|
|
51
|
+
const value = target.getAttribute(attribute);
|
|
52
|
+
if (value)
|
|
53
|
+
parts.push(value);
|
|
54
|
+
}
|
|
55
|
+
for (const id of (target.getAttribute('aria-labelledby') ?? '').split(/\s+/))
|
|
56
|
+
if (id)
|
|
57
|
+
parts.push(document.getElementById(id)?.textContent ?? '');
|
|
58
|
+
if (!isField)
|
|
59
|
+
parts.push((target.innerText ?? target.textContent ?? '').slice(0, 300));
|
|
60
|
+
};
|
|
61
|
+
const describeSubmit = (form) => {
|
|
62
|
+
if (!form)
|
|
63
|
+
return;
|
|
64
|
+
parts.push(form.getAttribute('action') ?? '');
|
|
65
|
+
for (const button of Array.from(form.querySelectorAll('button, input[type="submit" i], input[type="image" i]'))) {
|
|
66
|
+
const type = (button.getAttribute('type') ?? 'submit').toLowerCase();
|
|
67
|
+
if (type === 'submit' || type === 'image')
|
|
68
|
+
describe(button);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const CLICKABLE = 'a, button, summary, label, [role="button"], [role="link"], [role="menuitem"], [role="tab"], input[type="submit" i], input[type="button" i], input[type="image" i]';
|
|
72
|
+
const isTextField = element instanceof HTMLInputElement && !['submit', 'button', 'reset', 'image', 'checkbox', 'radio'].includes(element.type);
|
|
73
|
+
if (mode === 'submit' || (mode === 'key' && isTextField)) {
|
|
74
|
+
describeSubmit(element instanceof HTMLInputElement || element instanceof HTMLButtonElement ? element.form : element.closest('form'));
|
|
75
|
+
return parts.join('\n');
|
|
76
|
+
}
|
|
77
|
+
if (mode === 'key' && (element instanceof HTMLTextAreaElement || isTextField))
|
|
78
|
+
return '';
|
|
79
|
+
const clickable = element.closest(CLICKABLE) ?? element;
|
|
80
|
+
describe(clickable);
|
|
81
|
+
if (clickable !== element)
|
|
82
|
+
describe(element);
|
|
83
|
+
const type = (clickable.getAttribute('type') ?? (clickable instanceof HTMLButtonElement ? 'submit' : '')).toLowerCase();
|
|
84
|
+
if ((clickable instanceof HTMLButtonElement || clickable instanceof HTMLInputElement) && (type === 'submit' || type === 'image')) {
|
|
85
|
+
parts.push(clickable.form?.getAttribute('action') ?? '');
|
|
86
|
+
}
|
|
87
|
+
return parts.join('\n');
|
|
88
|
+
}
|
|
89
|
+
/** Ждём, пока страница загрузилась и сеть затихла: без жёстких пауз, с верхним пределом. */
|
|
90
|
+
function trackNetwork(page) {
|
|
91
|
+
let inflight = 0;
|
|
92
|
+
let lastChange = Date.now();
|
|
93
|
+
const change = (delta) => {
|
|
94
|
+
inflight = Math.max(0, inflight + delta);
|
|
95
|
+
lastChange = Date.now();
|
|
96
|
+
};
|
|
97
|
+
page.on('request', () => change(1));
|
|
98
|
+
page.on('requestfinished', () => change(-1));
|
|
99
|
+
page.on('requestfailed', () => change(-1));
|
|
100
|
+
return async function settle() {
|
|
101
|
+
await page.waitForLoadState('domcontentloaded', { timeout: NAVIGATION_TIMEOUT_MS }).catch(() => { });
|
|
102
|
+
const deadline = Date.now() + 3000;
|
|
103
|
+
while (Date.now() < deadline && (inflight > 0 || Date.now() - lastChange < 150)) {
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function createBrowserActions(options) {
|
|
109
|
+
const { profiles, vault, policy, shotsDir, control, log } = options;
|
|
110
|
+
const queues = new Map();
|
|
111
|
+
const busy = new Set();
|
|
112
|
+
const settlers = new WeakMap();
|
|
113
|
+
// Найденная проверка человеком и сколько раз к тому моменту владелец возвращал управление.
|
|
114
|
+
const humanChecks = new Map();
|
|
115
|
+
const snapshotOptions = { allowedOrigins: policy.allowedOrigins, secrets: () => vault.knownPasswords() };
|
|
116
|
+
/** Проверка человеком снята, если владелец после неё сам вернул управление с живого экрана. */
|
|
117
|
+
function pendingHumanCheck(profile) {
|
|
118
|
+
const check = humanChecks.get(profile);
|
|
119
|
+
if (check && control.releases(profile) > check.releases)
|
|
120
|
+
humanChecks.delete(profile);
|
|
121
|
+
return humanChecks.get(profile)?.reason ?? null;
|
|
122
|
+
}
|
|
123
|
+
const settle = (page) => {
|
|
124
|
+
let settler = settlers.get(page);
|
|
125
|
+
if (!settler)
|
|
126
|
+
settlers.set(page, (settler = trackNetwork(page)));
|
|
127
|
+
return settler();
|
|
128
|
+
};
|
|
129
|
+
function inQueue(profile, work) {
|
|
130
|
+
const previous = queues.get(profile) ?? Promise.resolve();
|
|
131
|
+
const next = previous.then(async () => {
|
|
132
|
+
busy.add(profile);
|
|
133
|
+
try {
|
|
134
|
+
return await work();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
busy.delete(profile);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
queues.set(profile, next.catch(() => { }));
|
|
141
|
+
return next;
|
|
142
|
+
}
|
|
143
|
+
async function elementOf(page, ref) {
|
|
144
|
+
if (typeof ref !== 'string' || !REF.test(ref))
|
|
145
|
+
throw new Refused('bad_request', 'ref — номер элемента из снимка, например e12');
|
|
146
|
+
const locator = page.getByRef(ref);
|
|
147
|
+
const handle = await locator.elementHandle({ timeout: 2000 }).catch(() => null);
|
|
148
|
+
if (!handle)
|
|
149
|
+
throw new Refused('not_found', `элемента ${ref} нет: сделайте read заново`);
|
|
150
|
+
const frame = await handle.ownerFrame();
|
|
151
|
+
await handle.dispose();
|
|
152
|
+
if (!frame || !isFrameAllowed(frame, policy.allowedOrigins))
|
|
153
|
+
throw new Refused('refused_origin', `элемент ${ref} во фрейме сайта вне allowedOrigins`);
|
|
154
|
+
return locator;
|
|
155
|
+
}
|
|
156
|
+
async function refuseDangerous(locator, mode) {
|
|
157
|
+
return refuseDangerousTarget(await locator.evaluate(describeTarget, mode, { timeout: ACTION_TIMEOUT_MS }));
|
|
158
|
+
}
|
|
159
|
+
/** Элемент в фокусе на разрешённой части страницы (для клавиш). */
|
|
160
|
+
async function focusedElement(page) {
|
|
161
|
+
for (const frame of page.frames()) {
|
|
162
|
+
if (!isFrameAllowed(frame, policy.allowedOrigins))
|
|
163
|
+
continue;
|
|
164
|
+
const focused = frame.locator('*:focus:not(iframe):not(frame)');
|
|
165
|
+
if ((await focused.count().catch(() => 0)) > 0)
|
|
166
|
+
return focused.first();
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Итог действия: снимок, а также отказ, если сработал замок, страница ушла с сайта или нужен человек.
|
|
172
|
+
* `passwordStep` — действие вписало пароль: попытка страницы унести его роняет действие. На остальных действиях
|
|
173
|
+
* закрытые подресурсы запертого сеанса (счётчик, CDN) действие не роняют — они в `blocked` результата.
|
|
174
|
+
*/
|
|
175
|
+
async function outcome(session, passwordStep = false) {
|
|
176
|
+
await settle(session.page);
|
|
177
|
+
const page = session.page;
|
|
178
|
+
// Пароль уже вписан и сеанс заперт: что бы ни случилось дальше, повторять вход незачем.
|
|
179
|
+
const filled = passwordStep ? 'пароль вписан, замок сеанса взведён — вход не повторяйте; ' : '';
|
|
180
|
+
if (page.url() !== 'about:blank' && !isAllowedOrigin(page.url(), policy.allowedOrigins)) {
|
|
181
|
+
await page.goto('about:blank').catch(() => { });
|
|
182
|
+
session.takeBlocked();
|
|
183
|
+
return fail('refused_origin', `${filled}страница ушла на сайт вне allowedOrigins и закрыта`);
|
|
184
|
+
}
|
|
185
|
+
const snapshot = await takeSnapshot(page, session.fields, snapshotOptions);
|
|
186
|
+
const human = await findHumanCheck(page, snapshot.text);
|
|
187
|
+
if (human.needsHuman)
|
|
188
|
+
humanChecks.set(session.name, { reason: human.reason, releases: control.releases(session.name) });
|
|
189
|
+
else
|
|
190
|
+
humanChecks.delete(session.name);
|
|
191
|
+
// Чужой фрейм и подресурс с внутреннего адреса просто не загружены — само действие на странице прошло.
|
|
192
|
+
const taken = session.takeBlocked().filter((request) => request.code !== 'refused_frame' && request.code !== 'refused_internal');
|
|
193
|
+
const locked = passwordStep ? [] : taken.filter((request) => request.code === 'refused_leak');
|
|
194
|
+
const blocked = taken.filter((request) => !locked.includes(request));
|
|
195
|
+
if (blocked.length > 0) {
|
|
196
|
+
return fail(blocked[0].code, `${filled}заблокировано замком: ${blocked.map((request) => request.reason).join('; ')}`);
|
|
197
|
+
}
|
|
198
|
+
if (human.needsHuman)
|
|
199
|
+
return fail('needs_human', `${filled}${human.reason}: нужен человек, браузер остановился`);
|
|
200
|
+
return { ok: true, url: snapshot.url, title: snapshot.title, snapshot: snapshot.snapshot, ...(locked.length > 0 ? { blocked: lockedSummary(locked) } : {}) };
|
|
201
|
+
}
|
|
202
|
+
async function shot(session) {
|
|
203
|
+
const page = session.page;
|
|
204
|
+
await settle(page);
|
|
205
|
+
const snapshot = await takeSnapshot(page, session.fields, snapshotOptions);
|
|
206
|
+
const mask = [...snapshot.sensitiveRefs.map((ref) => page.getByRef(ref)), ...page.frames().map((frame) => frame.locator(PASSWORD_FIELDS))];
|
|
207
|
+
// Поле, которое сайт пересоздал после вписывания, узнаём по значению: значения сравниваются здесь, не на странице.
|
|
208
|
+
const secrets = vault.knownPasswords();
|
|
209
|
+
for (const frame of page.frames()) {
|
|
210
|
+
const inputs = frame.locator('input, textarea');
|
|
211
|
+
const values = await inputs.evaluateAll((elements) => elements.map((element) => element.value)).catch(() => []);
|
|
212
|
+
values.forEach((value, index) => {
|
|
213
|
+
if (secrets.some((secret) => value.includes(secret)))
|
|
214
|
+
mask.push(inputs.nth(index));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
mkdirSync(shotsDir, { recursive: true });
|
|
218
|
+
const path = join(shotsDir, `${session.name}-${Date.now()}-${randomBytes(3).toString('hex')}.jpg`);
|
|
219
|
+
await page.screenshot({ path, type: 'jpeg', quality: 70, mask, timeout: 15_000 });
|
|
220
|
+
return { ok: true, path };
|
|
221
|
+
}
|
|
222
|
+
async function fillPassword(session, input) {
|
|
223
|
+
const entryId = Number(input.entryId);
|
|
224
|
+
if (!Number.isInteger(entryId) || entryId <= 0)
|
|
225
|
+
return fail('bad_request', 'entryId — номер входа из logins');
|
|
226
|
+
const { entry, password } = vault.passwordFor(entryId);
|
|
227
|
+
if (entry.profile !== session.name)
|
|
228
|
+
return fail('refused_profile', `вход ${entryId} принадлежит другому профилю`);
|
|
229
|
+
const locator = await elementOf(session.page, input.ref);
|
|
230
|
+
const handle = (await locator.elementHandle({ timeout: ACTION_TIMEOUT_MS }));
|
|
231
|
+
const isPasswordField = await handle.evaluate((element) => element instanceof HTMLInputElement && element.type.toLowerCase() === 'password');
|
|
232
|
+
if (!isPasswordField)
|
|
233
|
+
return fail('refused_field', 'элемент — не поле пароля; пароль вписывается только в поле пароля');
|
|
234
|
+
const frame = await handle.ownerFrame();
|
|
235
|
+
const { site } = entry;
|
|
236
|
+
if (!frame || !isFrameAllowed(frame, policy.allowedOrigins) || !isLoginOrigin(frame.url(), site)) {
|
|
237
|
+
return fail('refused_origin', `поле не на сайте входа ${site} (нужен точно этот адрес и https): пароль не вписан`);
|
|
238
|
+
}
|
|
239
|
+
await session.fields.mark(handle, 'filled');
|
|
240
|
+
session.passwordFilled();
|
|
241
|
+
await handle.fill(password, { timeout: ACTION_TIMEOUT_MS });
|
|
242
|
+
return outcome(session, true);
|
|
243
|
+
}
|
|
244
|
+
async function pageCommand(command, session, input) {
|
|
245
|
+
const page = session.page;
|
|
246
|
+
switch (command) {
|
|
247
|
+
case 'open':
|
|
248
|
+
return { ok: true };
|
|
249
|
+
case 'read':
|
|
250
|
+
return outcome(session);
|
|
251
|
+
case 'url': {
|
|
252
|
+
if (typeof input.url !== 'string')
|
|
253
|
+
return fail('bad_request', 'url обязателен');
|
|
254
|
+
if (!isAllowedOrigin(input.url, policy.allowedOrigins))
|
|
255
|
+
return fail('refused_origin', 'сайт вне allowedOrigins');
|
|
256
|
+
session.takeBlocked();
|
|
257
|
+
await page.goto(input.url, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }).catch((error) => {
|
|
258
|
+
if (!/ERR_BLOCKED_BY_CLIENT|ERR_ABORTED/.test(error.message))
|
|
259
|
+
throw error;
|
|
260
|
+
});
|
|
261
|
+
return outcome(session);
|
|
262
|
+
}
|
|
263
|
+
case 'back':
|
|
264
|
+
session.takeBlocked();
|
|
265
|
+
await page.goBack({ waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }).catch(() => null);
|
|
266
|
+
return outcome(session);
|
|
267
|
+
case 'click': {
|
|
268
|
+
const danger = checkDanger(input.danger);
|
|
269
|
+
if (!danger.ok)
|
|
270
|
+
return danger;
|
|
271
|
+
const element = await elementOf(page, input.ref);
|
|
272
|
+
const dangerous = await refuseDangerous(element, 'click');
|
|
273
|
+
if (dangerous)
|
|
274
|
+
return dangerous;
|
|
275
|
+
session.takeBlocked();
|
|
276
|
+
await element.click({ timeout: ACTION_TIMEOUT_MS });
|
|
277
|
+
return outcome(session);
|
|
278
|
+
}
|
|
279
|
+
case 'type': {
|
|
280
|
+
const danger = checkDanger(input.danger);
|
|
281
|
+
if (!danger.ok)
|
|
282
|
+
return danger;
|
|
283
|
+
if (typeof input.text !== 'string')
|
|
284
|
+
return fail('bad_request', 'text обязателен');
|
|
285
|
+
const element = await elementOf(page, input.ref);
|
|
286
|
+
if (input.submit === true) {
|
|
287
|
+
const dangerous = await refuseDangerous(element, 'submit');
|
|
288
|
+
if (dangerous)
|
|
289
|
+
return dangerous;
|
|
290
|
+
}
|
|
291
|
+
session.takeBlocked();
|
|
292
|
+
await element.fill(input.text, { timeout: ACTION_TIMEOUT_MS });
|
|
293
|
+
if (input.submit === true)
|
|
294
|
+
await element.press('Enter', { timeout: ACTION_TIMEOUT_MS });
|
|
295
|
+
return outcome(session);
|
|
296
|
+
}
|
|
297
|
+
case 'key': {
|
|
298
|
+
if (typeof input.key !== 'string' || !/^[\w+-]{1,32}$/.test(input.key))
|
|
299
|
+
return fail('bad_request', 'key — имя клавиши, например Enter или Shift+Tab');
|
|
300
|
+
if (SUBMIT_KEYS.test(input.key)) {
|
|
301
|
+
const focused = await focusedElement(page);
|
|
302
|
+
const dangerous = focused ? await refuseDangerous(focused, 'key') : null;
|
|
303
|
+
if (dangerous)
|
|
304
|
+
return dangerous;
|
|
305
|
+
}
|
|
306
|
+
session.takeBlocked();
|
|
307
|
+
await page.keyboard.press(input.key);
|
|
308
|
+
return outcome(session);
|
|
309
|
+
}
|
|
310
|
+
case 'scroll': {
|
|
311
|
+
const direction = SCROLL_DIRECTIONS[input.direction];
|
|
312
|
+
if (!direction)
|
|
313
|
+
return fail('bad_request', 'direction: up | down | left | right');
|
|
314
|
+
const px = input.px === undefined ? 600 : Number(input.px);
|
|
315
|
+
if (!Number.isFinite(px) || px <= 0 || px > 10_000)
|
|
316
|
+
return fail('bad_request', 'px — от 1 до 10000');
|
|
317
|
+
await page.mouse.wheel(direction[0] * px, direction[1] * px);
|
|
318
|
+
return outcome(session);
|
|
319
|
+
}
|
|
320
|
+
case 'shot':
|
|
321
|
+
return shot(session);
|
|
322
|
+
case 'fill-password':
|
|
323
|
+
return fillPassword(session, input);
|
|
324
|
+
default:
|
|
325
|
+
return fail('bad_request', `команда ${command} не относится к странице`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function statusOf(profile) {
|
|
329
|
+
if (control.holder(profile) === 'owner')
|
|
330
|
+
return { ok: true, state: 'busy', reason: 'управление у владельца' };
|
|
331
|
+
if (busy.has(profile))
|
|
332
|
+
return { ok: true, state: 'busy', reason: 'выполняется команда' };
|
|
333
|
+
return { ok: true, state: 'ready' };
|
|
334
|
+
}
|
|
335
|
+
async function run(command, input) {
|
|
336
|
+
if (!BROWSER_COMMANDS.includes(command))
|
|
337
|
+
return fail('not_found', `нет команды ${command}`);
|
|
338
|
+
if (command === 'logins') {
|
|
339
|
+
const { site } = input;
|
|
340
|
+
return { ok: true, logins: vault.list(typeof site === 'string' ? { site } : {}) };
|
|
341
|
+
}
|
|
342
|
+
const profile = input.profile;
|
|
343
|
+
if (typeof profile !== 'string' || profile === '')
|
|
344
|
+
return fail('bad_request', 'profile обязателен');
|
|
345
|
+
if (!profiles.knows(profile))
|
|
346
|
+
return fail('not_found', `профиль «${profile}» не задан в настройке`);
|
|
347
|
+
if (command === 'status') {
|
|
348
|
+
const status = statusOf(profile);
|
|
349
|
+
const human = pendingHumanCheck(profile);
|
|
350
|
+
if (status.ok && status.state === 'ready' && human)
|
|
351
|
+
return { ok: true, state: 'needs_human', reason: human };
|
|
352
|
+
return status;
|
|
353
|
+
}
|
|
354
|
+
const refused = refuseIfHumanControl(control, profile);
|
|
355
|
+
if (refused)
|
|
356
|
+
return refused;
|
|
357
|
+
const human = pendingHumanCheck(profile);
|
|
358
|
+
if (human && !ALLOWED_WHILE_HUMAN_CHECK.has(command)) {
|
|
359
|
+
return fail('needs_human', `${human}: пока человек не прошёл проверку на живом экране, доступны только read и shot`);
|
|
360
|
+
}
|
|
361
|
+
const started = Date.now();
|
|
362
|
+
const result = await inQueue(profile, async () => {
|
|
363
|
+
const session = await profiles.get(profile);
|
|
364
|
+
try {
|
|
365
|
+
return await pageCommand(command, session, input);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (error instanceof Refused)
|
|
369
|
+
return fail(error.code, error.message);
|
|
370
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
371
|
+
if (error instanceof Error && error.name === 'TimeoutError')
|
|
372
|
+
return fail('timeout', message);
|
|
373
|
+
if (error.code === 'not_found')
|
|
374
|
+
return fail('not_found', message);
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
log({ event: 'command', profile, command, ...(typeof input.ref === 'string' ? { ref: input.ref } : {}), ok: result.ok, ...(result.ok ? {} : { code: result.code }), ms: Date.now() - started });
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
return { run };
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,kGAAkG;AAClG,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AACnJ,OAAO,EAAoB,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAI3E,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AASnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,CAAU,CAAA;AAGvJ,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,OAAe,EAAgB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;AAE5F,MAAM,oBAAoB,GAAG,EAAE,CAAA;AAE/B;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAwC;IACpE,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACnG,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,iCAAiC,WAAW,EAAE,CAAC,CAAA;IACvH,IAAI,KAAK,CAAC,MAAM,GAAG,oBAAoB;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,MAAM,GAAG,oBAAoB,iBAAiB,CAAC,CAAA;IAChH,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;IAC/F,OAAO,KAAK,CAAA;AACd,CAAC;AACD,MAAM,GAAG,GAAG,iBAAiB,CAAA;AAC7B,MAAM,iBAAiB,GAAG,IAAI,CAAA;AAC9B,MAAM,qBAAqB,GAAG,MAAM,CAAA;AACpC,MAAM,iBAAiB,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAW,CAAA;AAC9F,MAAM,eAAe,GAAG,4GAA4G,CAAA;AAEpI,MAAM,OAAQ,SAAQ,KAAK;IAEd;IADX,YACW,IAAY,EACrB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAA;QAHL,SAAI,GAAJ,IAAI,CAAQ;IAIvB,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AACxF,MAAM,WAAW,GAAG,wCAAwC,CAAA;AAI5D;;;GAGG;AACH,SAAS,cAAc,CAAC,OAAgB,EAAE,IAAgB;IACxD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,QAAQ,GAAG,CAAC,MAAsB,EAAE,EAAE;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,OAAO,GAAG,MAAM,YAAY,mBAAmB,IAAI,CAAC,MAAM,YAAY,gBAAgB,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QAC9J,KAAK,MAAM,SAAS,IAAI,CAAC,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1H,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;YAC5C,IAAI,KAAK;gBAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC9B,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,IAAI,EAAE;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,WAAW,IAAI,EAAE,CAAC,CAAA;QAChJ,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,CAAE,MAAsB,CAAC,SAAS,IAAI,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IACzG,CAAC,CAAA;IACD,MAAM,cAAc,GAAG,CAAC,IAA4B,EAAE,EAAE;QACtD,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;QAC7C,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,uDAAuD,CAAC,CAAC,EAAE,CAAC;YAChH,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAA;YACpE,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO;gBAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAA;IACD,MAAM,SAAS,GAAG,mKAAmK,CAAA;IACrL,MAAM,WAAW,GAAG,OAAO,YAAY,gBAAgB,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9I,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,WAAW,CAAC,EAAE,CAAC;QACzD,cAAc,CAAC,OAAO,YAAY,gBAAgB,IAAI,OAAO,YAAY,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QACpI,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC,OAAO,YAAY,mBAAmB,IAAI,WAAW,CAAC;QAAE,OAAO,EAAE,CAAA;IACxF,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAA;IACvD,QAAQ,CAAC,SAAS,CAAC,CAAA;IACnB,IAAI,SAAS,KAAK,OAAO;QAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,YAAY,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IACvH,IAAI,CAAC,SAAS,YAAY,iBAAiB,IAAI,SAAS,YAAY,gBAAgB,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO,CAAC,EAAE,CAAC;QACjI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,4FAA4F;AAC5F,SAAS,YAAY,CAAC,IAAU;IAC9B,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,IAAI,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC3B,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;QAC/B,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAA;QACxC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACzB,CAAC,CAAA;IACD,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IACnC,IAAI,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,OAAO,KAAK,UAAU,MAAM;QAC1B,MAAM,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACnG,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;QAClC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC;YAChF,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;QACzD,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAOpC;IACC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAA;IACnE,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAA;IAClD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA6B,CAAA;IACzD,2FAA2F;IAC3F,MAAM,WAAW,GAAG,IAAI,GAAG,EAAgD,CAAA;IAC3E,MAAM,eAAe,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAA;IAExG,+FAA+F;IAC/F,SAAS,iBAAiB,CAAC,OAAe;QACxC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACtC,IAAI,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ;YAAE,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACpF,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,IAAI,CAAA;IACjD,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,IAAU,EAAE,EAAE;QAC5B,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAChC,IAAI,CAAC,OAAO;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChE,OAAO,OAAO,EAAE,CAAA;IAClB,CAAC,CAAA;IAED,SAAS,OAAO,CAAI,OAAe,EAAE,IAAsB;QACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;QACzD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACpC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjB,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,EAAE,CAAA;YACrB,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YACtB,CAAC;QACH,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CACR,OAAO,EACP,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CACrB,CAAA;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,UAAU,SAAS,CAAC,IAAU,EAAE,GAAY;QAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,OAAO,CAAC,aAAa,EAAE,8CAA8C,CAAC,CAAA;QAC/H,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;QAClC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;QAC/E,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,OAAO,CAAC,WAAW,EAAE,YAAY,GAAG,4BAA4B,CAAC,CAAA;QACxF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;QACvC,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;QACtB,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;YAAE,MAAM,IAAI,OAAO,CAAC,gBAAgB,EAAE,WAAW,GAAG,qCAAqC,CAAC,CAAA;QACrJ,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,UAAU,eAAe,CAAC,OAAgB,EAAE,IAAgB;QAC/D,OAAO,qBAAqB,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAA;IAC5G,CAAC;IAED,mEAAmE;IACnE,KAAK,UAAU,cAAc,CAAC,IAAU;QACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;gBAAE,SAAQ;YAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAA;YAC/D,IAAI,CAAC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;gBAAE,OAAO,OAAO,CAAC,KAAK,EAAE,CAAA;QACxE,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;OAIG;IACH,KAAK,UAAU,OAAO,CAAC,OAAuB,EAAE,YAAY,GAAG,KAAK;QAClE,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC1B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;QACzB,wFAAwF;QACxF,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,4DAA4D,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/F,IAAI,IAAI,CAAC,GAAG,EAAE,KAAK,aAAa,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;YACxF,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAC9C,OAAO,CAAC,WAAW,EAAE,CAAA;YACrB,OAAO,IAAI,CAAC,gBAAgB,EAAE,GAAG,MAAM,oDAAoD,CAAC,CAAA;QAC9F,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;QAC1E,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACvD,IAAI,KAAK,CAAC,UAAU;YAAE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;;YAClH,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACrC,uGAAuG;QACvG,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,eAAe,IAAI,OAAO,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAA;QAChI,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,cAAc,CAAC,CAAA;QAC7F,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QACpE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,GAAG,MAAM,yBAAyB,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACxH,CAAC;QACD,IAAI,KAAK,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC,aAAa,EAAE,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,sCAAsC,CAAC,CAAA;QAChH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;IAC9J,CAAC;IAED,KAAK,UAAU,IAAI,CAAC,OAAuB;QACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;QACzB,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QAClB,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;QAC1E,MAAM,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QAC1I,mHAAmH;QACnH,MAAM,OAAO,GAAG,KAAK,CAAC,cAAc,EAAE,CAAA;QACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YAClC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;YAC/C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAE,OAA4B,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAc,CAAC,CAAA;YACjJ,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC9B,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;YACpF,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAClG,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;QACjF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAC3B,CAAC;IAED,KAAK,UAAU,YAAY,CAAC,OAAuB,EAAE,KAA8B;QACjF,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,aAAa,EAAE,iCAAiC,CAAC,CAAA;QAC7G,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QACtD,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,iBAAiB,EAAE,QAAQ,OAAO,8BAA8B,CAAC,CAAA;QACjH,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;QACxD,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAA+B,CAAA;QAC1G,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,YAAY,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC,CAAA;QAC5I,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC,eAAe,EAAE,mEAAmE,CAAC,CAAA;QACvH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;QACvC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;QACtB,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YACjG,OAAO,IAAI,CAAC,gBAAgB,EAAE,0BAA0B,IAAI,qDAAqD,CAAC,CAAA;QACpH,CAAC;QACD,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QAC3C,OAAO,CAAC,cAAc,EAAE,CAAA;QACxB,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAA;QAC3D,OAAO,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAC/B,CAAC;IAED,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAuB,EAAE,KAA8B;QACzG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;QACzB,QAAQ,OAAO,EAAE,CAAC;YAChB,KAAK,MAAM;gBACT,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;YACrB,KAAK,MAAM;gBACT,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;oBAAE,OAAO,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAA;gBAC/E,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,CAAC;oBAAE,OAAO,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAA;gBAChH,OAAO,CAAC,WAAW,EAAE,CAAA;gBACrB,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;oBACnH,IAAI,CAAC,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;wBAAE,MAAM,KAAK,CAAA;gBAC3E,CAAC,CAAC,CAAA;gBACF,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,KAAK,MAAM;gBACT,OAAO,CAAC,WAAW,EAAE,CAAA;gBACrB,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;gBACtG,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;gBACxC,IAAI,CAAC,MAAM,CAAC,EAAE;oBAAE,OAAO,MAAM,CAAA;gBAC7B,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;gBAChD,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;gBACzD,IAAI,SAAS;oBAAE,OAAO,SAAS,CAAA;gBAC/B,OAAO,CAAC,WAAW,EAAE,CAAA;gBACrB,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAA;gBACnD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;gBACxC,IAAI,CAAC,MAAM,CAAC,EAAE;oBAAE,OAAO,MAAM,CAAA;gBAC7B,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAO,IAAI,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAA;gBACjF,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;gBAChD,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;oBAC1B,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;oBAC1D,IAAI,SAAS;wBAAE,OAAO,SAAS,CAAA;gBACjC,CAAC;gBACD,OAAO,CAAC,WAAW,EAAE,CAAA;gBACrB,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAA;gBAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;oBAAE,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAA;gBACvF,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;oBAAE,OAAO,IAAI,CAAC,aAAa,EAAE,iDAAiD,CAAC,CAAA;gBACrJ,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBAChC,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAA;oBAC1C,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;oBACxE,IAAI,SAAS;wBAAE,OAAO,SAAS,CAAA;gBACjC,CAAC;gBACD,OAAO,CAAC,WAAW,EAAE,CAAA;gBACrB,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACpC,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAA2C,CAAC,CAAA;gBACtF,IAAI,CAAC,SAAS;oBAAE,OAAO,IAAI,CAAC,aAAa,EAAE,qCAAqC,CAAC,CAAA;gBACjF,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;gBAC1D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,MAAM;oBAAE,OAAO,IAAI,CAAC,aAAa,EAAE,oBAAoB,CAAC,CAAA;gBACpG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;gBAC5D,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,KAAK,MAAM;gBACT,OAAO,IAAI,CAAC,OAAO,CAAC,CAAA;YACtB,KAAK,eAAe;gBAClB,OAAO,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;YACrC;gBACE,OAAO,IAAI,CAAC,aAAa,EAAE,WAAW,OAAO,0BAA0B,CAAC,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,SAAS,QAAQ,CAAC,OAAe;QAC/B,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,OAAO;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAA;QAC7G,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAA;QACxF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;IACrC,CAAC;IAED,KAAK,UAAU,GAAG,CAAC,OAAe,EAAE,KAA8B;QAChE,IAAI,CAAE,gBAAsC,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC,WAAW,EAAE,eAAe,OAAO,EAAE,CAAC,CAAA;QAClH,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;YACtB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;QACnF,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;QAC7B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,aAAa,EAAE,oBAAoB,CAAC,CAAA;QACnG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC,WAAW,EAAE,YAAY,OAAO,wBAAwB,CAAC,CAAA;QACnG,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;YACxC,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,KAAK;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;YAC5G,OAAO,MAAM,CAAA;QACf,CAAC;QACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACtD,IAAI,OAAO;YAAE,OAAO,OAAO,CAAA;QAC3B,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;QACxC,IAAI,KAAK,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC,aAAa,EAAE,GAAG,KAAK,gFAAgF,CAAC,CAAA;QACtH,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QAC1B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;YAC/C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC3C,IAAI,CAAC;gBACH,OAAO,MAAM,WAAW,CAAC,OAAyB,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;YACrE,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,OAAO;oBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;gBACpE,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACtF,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;oBAAE,OAAO,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;gBAC5F,IAAK,KAA4B,CAAC,IAAI,KAAK,WAAW;oBAAE,OAAO,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;gBACzF,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC,CAAC,CAAA;QACF,GAAG,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;QAC/L,OAAO,MAAM,CAAA;IACf,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,CAAA;AAChB,CAAC"}
|