Files
glchat/desktop/README.md
T
grendervill 8814fe7cf4 release: glchat 0.1.1
Версия — в одном месте: файл VERSION в корне, из него её берут Makefile,
deploy/install.sh (IMAGE_TAG) и desktop-обёртка (tauri.conf.json, Cargo.toml).

Публикация обновлений (scripts/desktop-release.sh):
- манифесты прошлых релизов удаляются: оставшийся <старая версия>.json
  перекрывал новый релиз — обёртка спрашивает манифест своей версии и видела
  «обновлений нет»;
- mktemp-каталог раскладки получает 755, каталоги на инстансе
  нормализуются: rsync -a переносил режим 0700 на корень каталога обновлений,
  и приложение не могло читать манифесты (404 при живых файлах);
- --remote работает без TTY и без sudo, если каталог обновлений записываем;
  опции ssh — в GLCHAT_SSH_OPTS;
- предупреждение при публикации версии старше уже опубликованной.

Сервер: HEAD /updates/... (chi не выводит его из GET) — размер и тип артефакта
без скачивания; тест TestUpdatesAnswersHead.

Приёмка на живом канале: 0.1.0 → 0.1.1 на стенде (скачивание, проверка
подписи, установка, перезапуск), /Applications/glchat.app = 0.1.1.
2026-09-26 20:14:25 +03:00

463 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# desktop — desktop-обёртка glchat (Tauri 2)
Тот же веб-клиент из `web/`, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного
UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт
платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk,
автозапуск, deep links и автообновление.
```
desktop/
└── src-tauri/ # Rust-проект обёртки
├── src/ # окно, трей, шорткаты, уведомления, deep links, обновления
├── capabilities/ # allowlist IPC: локальная страница настроек и домен инстанса
├── ui/ # локальная страница настроек обёртки (tauri://localhost)
├── icons/ # иконки приложения (сгенерированы из web/public/icons)
└── tauri.conf.json # конфигурация сборки и плагинов
```
## Сборка и запуск
```bash
make desktop-check # cargo fmt --check, clippy, тесты (кэш в .cache/)
make desktop-build # бандлы текущей ОС (macOS: .app и .dmg)
make desktop-build-app # только .app + артефакты обновления, без .dmg
open desktop/src-tauri/target/release/bundle/macos/glchat.app
```
Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через `npx`). Кэши `cargo` и
`npm` держатся в `.cache/` — как у остальных инструментов проекта.
`make desktop-build-app` (`tauri build --bundles app`) нужен там, где `.dmg`
собрать нельзя: `bundle_dmg.sh` монтирует образ через `hdiutil` и управляет
Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он
падает (см. «Что не сделано»).
`open --args` из песочницы агента аргументы не доставляет (проверено на
macOS 27: и на бандле glchat, и на системном TextEdit — процесс запускается без
них; из обычного терминала то же самое работает). Поэтому `make desktop-run
ARGS='--minimized'` и замер свёрнутого окна в `scripts/desktop-perf.py` надо
запускать из обычного терминала, а не из песочницы.
Сборка релиза с артефактами автообновления использует ключ подписи:
```bash
# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
make desktop-build
```
Публичный ключ уже вписан в `src-tauri/tauri.conf.json` и в `UPDATER_PUBKEY`
(`src/updates.rs`). Без локального ключа `make desktop-build` собирает бандл без
артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.
## Аргументы командной строки
| Аргумент | Что делает |
|---|---|
| `--instance <url>` | адрес инстанса вместо сохранённого (`--instance=https://…` тоже работает) |
| `--minimized` | запуск свёрнутым в трей (используется автозапуском) |
| `--settings` | сразу открыть страницу настроек обёртки |
| `--autostart on\|off` | включить или выключить автозапуск при установке скриптом |
| `--update-now` | сразу проверить обновление и поставить его без диалога (тихое обновление) |
## Настройки
Хранятся в `settings.json` каталога конфигурации приложения
(`~/Library/Application Support/su.mhspx.glchat/` на macOS):
| Ключ | Смысл |
|---|---|
| `instanceUrl` | адрес клиента, по умолчанию `https://gl.mhspx.su` |
| `closeToTray` | закрытие окна сворачивает приложение в трей |
| `autostart` | запуск при входе в систему (`--minimized`) |
| `notifications` | показывать системные уведомления |
| `pttShortcut`, `muteShortcut`, `deafenShortcut` | глобальные комбинации (пусто — выключено) |
Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»).
Сессия и токены живут только в cookie webview: обёртка их не хранит.
## Deep links
Схема `glchat://` (регистрируется бандлом):
- `glchat://invite/<code>` → страница приглашения;
- `glchat://guild/<guildId>/channel/<channelId>` → сервер и комната.
Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и
доставляется после `desktop_ready`.
## Автообновление
Обёртка проверяет обновления сама: фоновая проверка через 30 секунд после старта
и вручную — трей → «Проверить обновления…». Адрес манифеста —
`GET {instance}/updates/{target}/{arch}/{current_version}` (target:
`darwin`/`windows`/`linux`, arch: `aarch64`/`x86_64`; см. `src/updates.rs`).
Найденное обновление показывается в интерфейсе, установка спрашивается
отдельным диалогом («во время звонка перезапуск без согласия недопустим»).
Для управляемых машин есть тихий режим — флаг `--update-now`:
```bash
open -a /Applications/glchat.app --args --update-now # из обычного терминала
```
С этим флагом обёртка проверяет обновление сразу при старте (не через 30 секунд)
и ставит найденное без вопросов, после чего перезапускается; диалогов нет вовсе,
а если обновления нет — приложение просто работает дальше. По умолчанию флаг
выключен, поведение обычного запуска не меняется. Подпись артефакта проверяется
в обоих режимах: флаг убирает только вопрос пользователю, но не проверку
(docs/DECISIONS.md, D-077).
В журнале (`~/Library/Logs/su.mhspx.glchat/glchat.log`) видны и версия запущенной
сборки, и ход обновления:
```
обёртка запущена: версия=0.1.0, адрес=https://gl.mhspx.su, окно=видимо, автозапуск=true
тихое обновление по флагу --update-now: проверка сразу при старте
доступно обновление 0.1.1
тихое обновление по флагу: 0.1.0 → 0.1.1
обновление 0.1.1 установлено, перезапуск
обёртка запущена: версия=0.1.1, …
```
Манифест отдаёт сам инстанс (`internal/server/updates.go`), отдельная статика в
Caddy не нужна: и встроенный Caddy профиля, и внешний прокси стенда проксируют
весь домен на приложение, поэтому `/updates/...` доходит до него без правок
чужих конфигов.
Раскладка каталога (`UPDATES_DIR`; на стенде `/opt/glchat/updates`, в контейнере
смонтирован как `/app/updates:ro`):
```
/opt/glchat/updates/
└── darwin/aarch64/
├── glchat.app.tar.gz # артефакт релиза (создаёт tauri build)
├── glchat.app.tar.gz.sig # подпись ed25519 (формат minisign)
├── 0.1.1.json # манифест текущего релиза
└── latest.json # манифест последнего релиза — его и отдаёт инстанс
```
Манифестов прошлых релизов в каталоге быть не должно: обёртка спрашивает
`/updates/{target}/{arch}/{своя версия}`, и оставшийся `<старая версия>.json`
перекрыл бы новый релиз — клиент со старой версией получил бы «обновлений нет».
Публикующий скрипт сам убирает такие файлы (см. ниже).
## Выпуск релиза
Версия живёт в одном месте — файл `VERSION` в корне репозитория; из него её
берут `Makefile` (версия сервера и образов), `deploy/install.sh` (`IMAGE_TAG`)
и desktop-обёртка (`desktop/src-tauri/tauri.conf.json` + `Cargo.toml` — их
значения должны совпадать с `VERSION`).
1. Поднять версию:
```bash
echo 0.1.2 > VERSION # версия сервера и обёртки
# те же значения — в desktop/src-tauri/tauri.conf.json, Cargo.toml, Cargo.lock
make web-build && make build # сервер собирается с новой версией
```
2. Проверки и сборка бандла:
```bash
make go-lint && go test -tags sqlite_fts5 -race -count=1 ./internal/...
cd web && npm run check && npm run lint && npm test && cd ..
make desktop-check # fmt, clippy, cargo test
make desktop-build # .app + .dmg + артефакты обновления
```
3. Зафиксировать и пометить релиз:
```bash
git add VERSION Makefile deploy/install.sh desktop/src-tauri scripts/desktop-release.sh
git commit -m "release: 0.1.2"
git tag -a v0.1.2 -m "glchat 0.1.2" && git push origin main --tags
```
4. Опубликовать обновление на инстансе:
```bash
sudo make desktop-release # локальный инстанс
make desktop-release-upload HOST=user@server # по SSH (sudo спросит пароль)
make desktop-release-upload HOST=user@server \
GLCHAT_SSH_OPTS="-i ~/.ssh/key -o IdentitiesOnly=yes" # свой ключ, без TTY (CI)
make desktop-release ARGS='--dry-run' # посмотреть план
```
5. Проверить канал и живьём обновиться со старой версии — см. следующий раздел.
Скрипт `scripts/desktop-release.sh` берёт версию из `tauri.conf.json`, сам
определяет target/arch по имени артефакта, копирует артефакт и `.sig`, собирает
манифест (`version`, `notes`, `pub_date`, `platforms.<target>-<arch>.signature/url`),
кладёт его и как `<version>.json`, и как `latest.json`, а манифесты прошлых
релизов убирает. Несколько платформ одной версии (macOS + Windows + Linux)
складываются в один манифест: запускайте скрипт по разу на артефакт. Каталог
обновлений на инстансе обычно принадлежит root — тогда rsync идёт через sudo;
если каталог записываем (стенд), sudo не нужен и загрузка работает без
терминала.
`latest.json` нужен потому, что обёртка приходит со **своей установленной**
версией: «новее ли релиз» решает клиент сравнением semver
(`tauri-plugin-updater`), поэтому на запрос любой версии сервер отдаёт
последний манифест, а если есть манифест конкретной версии — его. Без этого
обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия.
Владельцу прокси добавлять ничего не нужно: `/updates/...` уходит в
catch-all `handle` и доходит до приложения (так же, как статика клиента).
Вариант «раздавать обновления статикой мимо приложения» требует правила
`rewrite` на `latest.json` с подстановкой `target`/`arch` в Caddy — лишняя
работа без выгоды, поэтому не рекомендуется.
Проверка (стенд):
```bash
curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq . # манифест для старой версии
curl -sI https://gl.mhspx.su/updates/darwin/aarch64/glchat.app.tar.gz | head -3 # 200 + размер
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»
```
Порядок обновления: проверка → уведомление в интерфейсе → скачивание →
подтверждение в диалоге (во время звонка перезапуск без согласия не делается) →
подпись проверяется до установки, downgrade отклоняется.
### Приёмка релиза на живом инстансе (26.09.2026, 0.1.0 → 0.1.1)
Проверено на боевом канале стенда, без локальных заглушек:
- манифест опубликован `scripts/desktop-release.sh --remote`, артефакт
`glchat.app.tar.gz` (3 204 953 Б) и подпись лежат в
`/opt/glchat/updates/darwin/aarch64/`;
- обёртка 0.1.0, запущенная с `--update-now`, в журнале (UTC) показала:
`доступно обновление 0.1.1` → `тихое обновление по флагу: 0.1.0 → 0.1.1` →
`обновление 0.1.1 установлено, перезапуск` → `обёртка запущена: версия=0.1.1` →
`обновлений нет` (17:08:23–17:08:25Z);
- запрос старой версии `GET /updates/darwin/aarch64/0.1.0` отдаёт манифест
**0.1.1** — это и есть та точка, где раньше мешал оставшийся `0.1.0.json`;
- `/Applications/glchat.app` после установки — 0.1.1, ярлык на рабочем столе
(`~/Desktop/glchat.app`) указывает на него.
**Важно про место установки.** `tauri-plugin-updater` распаковывает обновление
во временный каталог и переносит бандл `rename(2)`. Если приложение лежит на
другом томе, чем временный каталог (например, на внешнем диске
`/Volumes/…`, а `TMPDIR` — на загрузочном), установка падает с
`установка обновления не удалась: Cross-device link (os error 18)`, хотя
скачивание и проверка подписи проходят. Поэтому приложение для автообновления
надо держать на загрузочном томе — так делает `.dmg` (ставит в `/Applications`);
ярлык на рабочем столе лучше вести на `/Applications/glchat.app`, а не на
сборку в репозитории.
### Локальная проверка «поверх старой версии» (без стенда)
Установка обновления проверена локально на настоящем сервере инстанса: сборка
0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка → перезапуск
на 0.1.1. Порядок действий (все данные — в каталоге репозитория, стенд не
затрагивается):
```bash
# 1. артефакт текущей версии и приложение 0.1.0 (с ключом подписи)
make desktop-build-app
# 2. версия 0.1.1 в desktop/src-tauri/tauri.conf.json и Cargo.toml, затем:
make desktop-build-app
# 3. раскладка манифеста в свой каталог, ссылки — на локальный сервер
bash scripts/desktop-release.sh --artifact \
desktop/src-tauri/target/release/bundle/macos/glchat.app.tar.gz \
--version 0.1.1 --base-url http://127.0.0.1:8100 --dir /tmp/updates
# 4. настоящий сервер инстанса со своим каталогом обновлений и клиентом
LISTEN_ADDR=127.0.0.1:8099 DATA_DIR=/tmp/glchat-e2e/data WEB_ROOT=$PWD/web/dist \
UPDATES_DIR=/tmp/updates DOMAIN=127.0.0.1:8099 TLS_ENABLED=false \
APP_VERSION=0.1.0-local SESSION_PEPPER=… MASTER_KEY=… TOTP_ENCRYPTION_KEY=… \
./build/glchat
# 5. запуск 0.1.0 с тихим обновлением (адрес инстанса — локальный)
/Applications/glchat.app/Contents/MacOS/glchat-desktop \
--instance http://127.0.0.1:8099 --update-now
```
Что нужно знать, повторяя это:
- **локальный стенд — http, а `tauri-plugin-updater` по умолчанию требует
`https`** («The configured updater endpoint must use a secure protocol»), для
проверки сборку делают с временным флагом
`tauri build --config '{"plugins":{"updater":{"dangerousInsecureTransportProtocol":true}}}'`.
В релизной сборке этого флага быть не должно: боевой адрес — `https://`;
- **приложение должно лежать на пути без символических ссылок**: macOS-проверка
`tauri-utils` отвергает `current_exe()` с symlink-компонентом, поэтому
`/tmp/glchat-e2e/…` не годится (`/tmp` — ссылка на `/private/tmp`), а
`/private/tmp/glchat-e2e/…` годится. Тот же смысл у требования ставить
приложение в `/Applications`, а не запускать через ссылку;
- **`--update-now` обязателен**: без него установку спрашивают диалогом, а
кликнуть в headless-прогоне некому;
- диагностика в логе: `GET /updates/darwin/aarch64/0.1.0` в журнале сервера,
затем `доступно обновление 0.1.1` → `тихое обновление по флагу: 0.1.0 → 0.1.1`
→ `обновление 0.1.1 установлено, перезапуск` → строка запуска с
`версия=0.1.1` и запрос манифеста уже для `0.1.1`;
- проверка подписи проверяется и «от обратного»: если испортить байт в
артефакте, установка обязана упасть с `The signature verification failed`,
а версия на диске — остаться прежней.
## Уведомления и переход в канал
Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные
сообщения, окно неактивно или комната не открыта), обёртка показывает системное
уведомление и обновляет счётчик.
Клик по уведомлению: у `tauri-plugin-notification` на desktop нет колбэка
действия, поэтому реализовано лучшее доступное поведение — уведомление,
показанное при неактивном окне, запоминается, и когда приложение
активируется в течение 10 секунд (окно получило фокус или macOS прислала
`Reopen`), окно поднимается и открывается комната из уведомления
(`glchat://guild/<id>/channel/<id>`, для личной беседы `glchat://dm/<id>`).
Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по
уведомлению, поэтому переход сработает и при обычном возврате в приложение в
течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает
окно. Полноценный колбэк требует своего моста к `UNUserNotificationCenter`
(`UNUserNotificationCenterDelegate`) — отдельная задача.
## Ресурсы и холодный старт
Замеры собраны скриптом `scripts/desktop-perf.py` (macOS arm64, Mac на M-серии,
инстанс `gl.mhspx.su` через внешний прокси; подробности — `docs/PERF.md`):
| Метрика | Значение | Бюджет (docs/client-tauri.md §1) |
|---|---|---|
| Холодный старт (`open` → процесс → строка «загружена страница») | 773–898 мс (три прогона) | ≤ 2 с |
| Из них: запуск процесса | 74–78 мс | — |
| Из них: загрузка страницы клиента | 696–821 мс | — |
| RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) | 254 МБ (117 + 137) | ≤ 350 МБ |
| RAM, окно свёрнуто в трей (`--minimized`) | 256 МБ | ≤ 350 МБ |
| Физический след (Activity Monitor) | 106 МБ | — |
Наблюдения:
- свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом
продолжает жить (так работают WKWebView и трей-режим) — экономия была бы
только при выгрузке страницы, а это потеря мгновенного возврата в клиент;
- первый запуск сразу после сборки бандла (холодный кэш файловой системы и
страницы) дал 1,26 с — тоже в бюджете;
- почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ.
Повторить замер:
```bash
make desktop-build-app # свежий .app (+ артефакты обновления)
python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта + простой в трее
```
## Что не сделано (открытые пункты Фазы 6)
- **Автообновление проверено локально «поверх старой версии»** (2026-09-26):
0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка →
перезапуск на 0.1.1, подробности и команды — «Локальная проверка» выше.
Открытым остаётся публикация реального релиза на стенде (номер версии
поднимает оператор) и то, что локальная проверка шла по http с временным
флагом `dangerousInsecureTransportProtocol` — на стенде канал https.
- **Аватары в уведомлениях** (thumb/буква) и группировка — сейчас иконка приложения.
- **Захват экрана и микрофон**: разрешения macOS выдаёт webview, отдельных
экранов-инструкций у обёртки нет.
- **Developer ID и нотаризация** macOS, EV/Trusted Signing для Windows — только
в CI с платными сертификатами (ниже — точные команды).
- **`.dmg`**: собирается из обычного терминала (Finder/AppleScript для раскладки
окна); в песочнице агента `hdiutil create` и AppleScript запрещены, но образ
**без оформления окна** собирается флагом `--sandbox-safe` к `bundle_dmg.sh`
(проверено 2026-09-26, см. ниже).
- **Windows/Linux-бандлы**: конфигурация кросс-платформенная, но собирались и
проверялись только macOS-arm64.
## Что осталось оператору (точные команды)
### macOS: `.dmg`
`bundle_dmg.sh` создаёт образ через `hdiutil create` и раскладывает иконки через
AppleScript/Finder. Из песочницы агента и из headless-сессии обе операции
запрещены (`hdiutil create` → «Операция не разрешена», AppleScript → -10004).
Из обычного терминала на macOS:
```bash
cd /Volumes/Samsung/projects/glchat
make desktop-build # .app + .dmg + артефакты обновления
ls -lh desktop/src-tauri/target/release/bundle/dmg/
```
Если терминала с графической сессией нет (CI, headless), тот же образ
собирается без оформления окна — тем же скриптом Tauri с флагом
`--sandbox-safe` (он пропускает AppleScript и `hdiutil create`):
```bash
cd /Volumes/Samsung/projects/glchat
make desktop-build-app # .app и артефакты обновления
rm -rf /tmp/dmg-stage && mkdir -p /tmp/dmg-stage
cp -R desktop/src-tauri/target/release/bundle/macos/glchat.app /tmp/dmg-stage/
cd desktop/src-tauri/target/release/bundle/dmg
bash bundle_dmg.sh --volname glchat --volicon icon.icns \
--icon glchat.app 180 170 --app-drop-link 480 170 --hide-extension glchat.app \
--sandbox-safe glchat_0.1.0_aarch64.dmg /tmp/dmg-stage
hdiutil verify glchat_0.1.0_aarch64.dmg
```
Такой `.dmg` монтируется и содержит `glchat.app` и ссылку `Applications`, но без
красивой раскладки иконок — для локальных проверок и CI этого достаточно, для
публикации лучше собрать обычным `make desktop-build`.
Артефакты без `.dmg` (для автообновления) собираются где угодно:
`make desktop-build-app`.
### macOS: подпись Developer ID и нотаризация
Нужны сертификат Developer ID Application в связке ключей и пароль приложения
(appleid.apple.com → App-Specific Passwords):
```bash
security find-identity -v -p codesigning # проверить наличие сертификата
export APPLE_SIGNING_IDENTITY="Developer ID Application: … (TEAMID)"
export APPLE_ID="…@example.com"
export APPLE_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="TEAMID"
make desktop-build # Tauri подпишет и отправит на нотаризацию
xcrun stapler validate desktop/src-tauri/target/release/bundle/macos/glchat.app
spctl --assess --type execute --verbose=4 desktop/src-tauri/target/release/bundle/macos/glchat.app
```
Переменные читает сам Tauri (`APPLE_*`), в репозиторий они не попадают.
Проверка «не подписано»: `codesign -dv --verbose=4 <app>` покажет `Signature=adhoc`.
### Windows (x64): MSI/NSIS + подпись
Сборка идёт на Windows-машине или в CI (`windows-latest`), Rust + Node 22:
```powershell
npx @tauri-apps/cli@^2 build --bundles nsis,msi
# подпись (EV/Trusted Signing или signtool с сертификатом):
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
/f cert.pfx /p $env:CERT_PASSWORD `
desktop\src-tauri\target\release\bundle\nsis\glchat_0.1.0_x64-setup.exe
```
Артефакты обновления (`*.nsis.zip`/`*.msi` + `.sig`) публикуются тем же
скриптом: `bash scripts/desktop-release.sh --artifact <файл> --target windows --arch x86_64`.
### Linux (AppImage + deb)
Сборка на Ubuntu 24.04/26.04 (нужны `libwebkit2gtk-4.1-dev`, `librsvg2-dev`,
`patchelf`, `libssl-dev`, `build-essential`):
```bash
sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf libssl-dev build-essential
npx @tauri-apps/cli@^2 build --bundles appimage,deb
bash scripts/desktop-release.sh --artifact desktop/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \
--target linux --arch x86_64
```
На Wayland нативная тряска окна при несохранённых изменениях не работает
(композитор запрещает `set_position`) — остаётся CSS-тряска контента; это
ожидаемое поведение, а не дефект.
### Ключ подписи обновлений
Приватный ключ — только в CI-секретах (`TAURI_SIGNING_PRIVATE_KEY`,
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`); локально — `build/desktop-keys/`
(каталог в `.gitignore`). Публичный ключ уже вшит в сборку и меняется только
вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.