Перейти к содержимому

Из любой сети

Окно терминала
./scripts/1-node.sh serve --domain node.example.org # or: --tailscale

casty web работает на вашей машине и доступен телефону в той же сети. 1-node.sh serve запускает ту же страницу на хосте ноды, рядом с нодой, за прокси, который завершает HTTPS, так что телефон достаёт до неё и из мобильной сети. Это тот же код casty, собранный на хосте в контейнере из этого клона, и он читает и пишет через ноду по адресу http://snapchain:3381 в собственной Docker-сети хоста.

  1. Читает каталог конфига casty здесь (~/.config/casty или --config): config.json с вашим fid и keys.enc.json с ключом приложения, и спрашивает пароль хранилища ключей. С --read-only ни хранилище ключей, ни пароль не покидают эту машину, а страница читает, но не может писать.
  2. Отправляет исходники клиента (без node_modules, тестов и сборки) в ~/snapchain/casty/src на хосте вместе с Dockerfile: node:22-alpine, зависимости из lock-файла, та же сборка, что и у 2-client.sh --from-source. Образ собирает хост; в первый раз это занимает минуту-две.
  3. Пишет ~/snapchain/docker-compose.casty.yml рядом с compose-файлом ноды и указывает его в COMPOSE_FILE в ~/snapchain/.env, так что каждая команда docker compose там его включает. Сервис casty запускает casty serve --public-host <name>, перезапускается вместе с хостом, хранит свой токен и кэши в ~/snapchain/casty/config (bind-mount) и читает пароль из Docker-секрета, файла ~/snapchain/casty/passphrase, режим 0600.
  4. Ставит перед ним прокси:
    • --domain <name>: сервис caddy публикует 80 и 443 и сам получает сертификат от Let’s Encrypt. Имя должно указывать на хост, а 80 и 443 должны быть доступны из интернета: ./scripts/0-provision.sh --https открывает их на файрволе Hetzner или AWS (повторный запуск сохраняет сервер); на собственном хосте откройте их сами.
    • --tailscale: casty публикуется только на loopback хоста, а tailscale serve --bg 3390 отдаёт его как https://<host>.<tailnet>.ts.net вашему tailnet с сертификатом, который выдаёт Tailscale. Tailscale должен быть установлен на хосте и в нём должен быть выполнен вход, а телефон должен быть в tailnet. Ни один порт не открывается в интернет.
  5. Ждёт, пока сервис запишет свой токен для связывания, затем печатает https://<name>/#token=.... QR-код этой ссылки находится в логах сервиса: ./scripts/1-node.sh logs casty.

Запускайте serve снова после изменения casty: он заново отправляет исходники, пересобирает и перезапускает. ./scripts/1-node.sh down останавливает ноду и casty вместе; на хосте docker compose stop casty (и caddy, с --domain) останавливает только страницу. Чтобы связать заново с нуля, удалите на хосте ~/snapchain/casty/config/web-token и выполните docker compose restart casty.

casty serve — это casty web поверх описанного ниже индекса, с push-уведомлениями, ограничением запросов и, с --public-host, именем прокси:

  • печатаемые им ссылки содержат https://<public host>, и это имя принимается как Host запроса, помимо IP-адреса или localhost;
  • каждый ответ несёт Strict-Transport-Security, так что браузер, который однажды видел страницу, никогда не запрашивает её по обычному HTTP;
  • /api/* ограничен 120 запросами в минуту на адрес клиента, считая до проверки токена, с 429 и Retry-After сверх этого. За прокси адрес клиента — это последний в X-Forwarded-For, тот, который добавил прокси; адрес, который клиент вписал туда сам, не учитывается;
  • без --public-host, на ноутбуке, casty serve — это casty web с индексом и ограничением: те же ссылки, без HSTS, а ограничение считается по адресу сокета, поскольку без прокси в X-Forwarded-For то, что написал клиент. При сборке из исходников casty serve нужен Node 22.13 или новее для его SQLite; однофайловый бинарник несёт свой собственный;
  • пароль хранилища ключей берётся из CASTY_PASSPHRASE_FILE, Docker-секрета, так что сервис запускается без терминала.

Всё остальное — это страница, описанная в В браузере и на телефоне: то же связывание по токену, та же нумерация, общая с терминалом, те же записи через ноду.

casty serve хранит ленту в index.sqlite в своём каталоге конфига (~/snapchain/casty/config/index.sqlite на хосте), с помощью встроенного SQLite в Node. При первом запуске он читает ваши подписки, последние 100 кастов ваших и каждого из них, касты, которые вас упоминают, ответы на ваши последние 25 кастов, а также ваши лайки и рекасты. С этого момента он следует за журналом событий ноды (/v1/events, каждый шард, каждые 1,5 с): каст того, на кого вы подписаны, упоминание вас или ответ вам попадает в индекс; удалённый или вычищенный каст уходит из него; подписка, которую вы делаете, приносит последние касты нового аккаунта, а отписка убирает аккаунт; ваши лайки и рекасты остаются актуальными. Лента, упоминания и состояние лайков на странице тогда берутся из индекса за несколько миллисекунд вместо сотен запросов к ноде, а запись, которую вы делаете на странице, сразу читает события, так что она появляется в следующем списке. Когда первый проход завершён, читаются ещё и имена и приложения ваших подписок, так что первая лента после запуска ничего не спрашивает у ноды. Пока первый проход не завершён, страница читает ноду, как это делает casty web. Профили и треды всегда читают ноду.

Пока вы читаете, страница каждые полминуты и каждый раз, когда снова появляется на экране, спрашивает индекс, не пришли ли касты новее верхнего. Если да, плашка под панелью говорит «3 new casts»; список никогда не сдвигается у вас под руками. Нажатие показывает их.

Нода хранит события примерно за три дня; сервис, который был выключен дольше двух, при запуске снова делает первый проход и продолжает оттуда. Удалите index.sqlite на хосте и перезапустите casty, чтобы начать с нуля.

В разделе о вас «notifications» включает Web Push для этого браузера: страница регистрирует свой service worker (sw.js), подписывается ключом, который casty serve создал при первом запуске (vapid.json в его каталоге конфига, режим 0600), и передаёт подписку сервису, который хранит её в cache/push.json. Когда события приносят каст от кого-то другого, который упоминает вас или отвечает на один из ваших кастов, сервис отправляет каждому подписанному браузеру одно сообщение: «new mention from @name» или «new reply from @name», и ничего больше, не текст каста. Оно зашифровано для собственного ключа браузера (RFC 8291) и подписано ключом сервиса (RFC 8292), так что push-сервис посередине, Apple, Google или Mozilla, передаёт шифротекст, который не может прочитать. Нажатие открывает mentions. Подписка, о которой push-сервис сообщает, что её больше нет, удаляется.

На iPhone Web Push работает только для страницы, добавленной на домашний экран (iOS 16.4 и новее); счётчик рядом с mentions работает везде, как и раньше. Выключение уведомлений удаляет подписку и из браузера, и из сервиса.

С serve на хосте, помимо ноды, хранится:

Что Где Что с этим можно сделать
Ключ приложения, зашифрованный ~/snapchain/casty/config/keys.enc.json, режим 0600 Ничего без пароля
Пароль хранилища ключей ~/snapchain/casty/passphrase, режим 0600, читается контейнером при запуске Расшифровать ключ: вместе с ним писать от вашего имени
Токен страницы ~/snapchain/casty/config/web-token и в каждом связанном браузере Читать и писать от вашего имени, откуда угодно, пока работает сервис
Сертификат и его ключ Том caddy_data у Caddy или у Tailscale Отдавать страницу под этим именем
Индекс вашей ленты ~/snapchain/casty/config/index.sqlite Ничего: в нём публичные касты, ваши подписки и реакции, которые есть и у ноды
Пара ключей для push ~/snapchain/casty/config/vapid.json, режим 0600 Отправлять push подписанным браузерам; новая пара аннулирует каждую подписку
Push-подписки ~/snapchain/casty/config/cache/push.json Отправить push этим браузерам, как эндпоинты push-сервиса, с публичными ключами браузеров

Тот, у кого есть root на хосте, получает ключ приложения, расшифрованный, в памяти сервиса. Это цена страницы, доступной откуда угодно, и вот почему риск остаётся ограниченным: ключ подписывает только то, что разрешают его scopes (по умолчанию касты, реакции, подписки), и истекает без использования после своего TTL; casty key revoke <key> с seed-фразой с любой машины отзывает его сразу; а --read-only оставляет ключ дома для страницы, которая только читает. Seed-фраза никогда никуда не уходит. Безопасность перечисляет каждый секрет.

Usage: casty serve [options]
the page over an index of the feed, with push; on the node's host behind HTTPS,
which 1-node.sh serve installs
Options:
--public-host <domain> the domain the page is reached at through a proxy
that ends HTTPS; it goes into the pairing link
--listen <host:port> where to serve, for the proxy (default:
"0.0.0.0:3390")
--new-token pair again; links printed before stop working
--read-only do not unlock the app key; the page reads but cannot
write
--no-allow-fname-server look @names up with your node only; by default names
it does not know are looked up on
fnames.farcaster.xyz (public, no key)
-h, --help display help for command