Files
enchanced-twitch-panel/RESEARCH.md
T
2026-09-05 20:39:44 +05:00

328 lines
29 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.
# Исследование: Twitch Panel Extension «мультидокументная панель»
> Задача: расширение размером в одну Twitch-панель (в блоке «О канале»), внутри которого
> листается несколько документов на основе Markdown (+ заголовочные картинки).
> Управление контентом — через удобную админ-панель; желательно обновление через git.
> Фронтенд — Svelte 5.
---
## 1. Вердикт
**Полностью реализуемо, без бэкенда, бесплатно.** Платформа Twitch Extensions жива
(20252026, Developer Rig устарел, но сами Extensions и ревью работают).
Ключевой трюк: код приложения (зашит в билд расширения) почти не меняется, а контент
(MD + картинки) тянется в рантайме из git-репозитория через CORS-дружелюбный CDN
(jsDelivr / raw.githubusercontent.com — оба отдают `Access-Control-Allow-Origin: *`,
проверено curl-ом). Обновление контента = `git push`, без пересборки и без ревью Twitch.
---
## 2. Ключевые факты о панельных расширениях (проверено по докам)
| Факт | Значение |
|---|---|
| Тип расширения | **Panel** — «коробка» в блоке «О канале», под плеером |
| Размер | **318 × 496 px**, жёстко (чтобы не было скролла iframe); скроллим внутренний контент |
| Активность | Панель видна **даже когда канал офлайн** — идеально для био/справки |
| Pop-out | Зритель может открыть расширение в отдельном окне побольше |
| Хостинг кода | zip-загрузка на CDN Twitch (`*.ext-twitch.tv`) ИЛИ свой HTTPS-хостинг (Base URI) |
| Config Service | 3 сегмента (developer / broadcaster / global), **5 КБ на сегмент**, публично читаемо |
| Helper-библиотека | `<script src="https://extension-files.twitch.tv/helper/v1/twitch-ext.min.js">` — обязательна |
| CSP | Настраивается в консоли: домены для `connect-src` и `img-src` надо внести в allowlist |
| Sandbox | нет `alert/confirm/prompt`, нет вложенных iframe, нет top-navigation, storage API ограничен |
| Мобилки | Панели показываются в мобильном приложении, если включить mobile-ready; бюджет первой загрузки **1 МБ / 3 сек** |
| Ревью | Нужно только для публичного релиза. **На свой канал ставится без ревью** (Local Test → Activate) |
| Жизненный цикл | Local Test → Hosted Test → Review → Released (Released не редактируется, только новая версия) |
Важно: в Config Service можно хранить **только метаданные** (список документов, порядок,
URL'ы), не сами документы — 5 КБ мало, и сегмент публичен (никаких секретов).
Контент живёт в git-репозитории.
---
## 3. Архитектура (рекомендуемый вариант, без бэкенда)
```
GitHub-репозиторий (public) Twitch
├─ docs/
│ ├─ index.json ← манифест контента ┌─────────────────────────────┐
│ ├─ rules.md ← сами документы │ Extension (Panel 318×496) │
│ ├─ faq.md │ Svelte 5 + Vite │
│ └─ assets/ │ ┌───────────────────────┐ │
│ ├─ rules-header.png │ │ табы/пейджер доков │ │
│ └─ ... │ │ MD → HTML (sanitized) │ │
└─ (git push = обновление контента) │ │ картинки из CDN git'а │ │
│ └───────────────────────┘ │
Config Service (5 КБ) │ Config view (в дашборде): │
broadcaster segment ← JSON: список доков, │ CRUD документов, порядок, │
порядок, заголовки, URL md/картинок │ сохранение в конфиг-сегмент│
└─────────────────────────────┘
```
**Поток данных во вьювер-вью:**
1. При старте Helper отдаёт `configuration.broadcaster` (JSON со списком документов).
2. Приложение рендерит табы/карусель; по выбору таба лениво фетчит MD с jsDelivr
(`https://cdn.jsdelivr.net/gh/<user>/<repo>@<branch|sha>/docs/faq.md`).
3. Markdown → HTML (`marked`/`markdown-it`) → `DOMPurify.sanitize``{@html}` в Svelte.
4. Относительные пути картинок переписываются на абсолютные URL CDN до рендера.
5. Кэширование: jsDelivr держит кэш ветки ~12ч; для мгновенных апдейтов — либо
`@sha`-пиннинг (SHA коммита кладётся в index.json/конфиг), либо cache-buster query.
**Конфиг-вью (панель управления):** отдельная страница того же Svelte-приложения
(в консоли на неё указывает Panel Config Path; режим можно бранчить по query `?mode=config`,
которую Helper/Twitch сам подставляет). UI: список документов, добавить/удалить/скрыть,
переупорядочить (drag&drop), заголовок, ссылка на .md, URL заголовочной картинки,
кнопка «Сохранить» → `Twitch.ext.configuration.set('broadcaster', '1', JSON.stringify(...))`.
**Картинки:** заголовочные и inline-картинки кладутся в `docs/assets/` в том же репо
(«загрузка картинки» = файл в репо; при желании конфиг-вью может коммитить их сам через
GitHub REST API — см. открытый вопрос про PAT).
### Альтернативы
- **С EBS (свой мини-бэкенд, CF Worker/Node):** синк репо по webhook, прокси приватного
репо, серверное хранение PAT, аплоад картинок из UI коммитом в репо. Плюсы: приватность,
секреты, мгновенная инвалидация. Минусы: инфраструктура, которую мы хотели избежать.
- **Без git вообще (Twitch-only):** нежизнеспособно — Config Service 5 КБ, файловый хост
Twitch меняется только вместе с билдом версии. Именно поэтому внешний CDN — правильный путь.
---
## 4. Git-обновления: что именно работает
- `raw.githubusercontent.com``ACAO: *`, кэш 5 мин.
- `cdn.jsdelivr.net/gh/...``ACAO: *`, кэш браузера 7 дней / CDN 12 ч; поддерживает
пиннинг по ветке, тегу и SHA; есть минификация для `gh`.
- GitHub Pages — тоже `ACAO: *` (можно собирать статический сайт/бандлы из репо).
Схема: ветка `main` = черновик, тег/отдельная ветка `published` = то, что показывает панель;
в конфиг-сегменте храним ссылку с `@published` + SHA. Обновить контент = push + (при
SHA-пиннинге) одно нажатие в конфиг-вью. Никакого пересборочного пайплайна не нужно,
если MD рендерится на клиенте; при желании можно рендерить на CI (GitHub Actions →
JSON-бандл в `gh-pages`), но это опция.
---
## 5. Плюсы и минусы
### Плюсы
- Нативное место на канале, виден офлайн — ровно то, что нужно для био/справки.
- Одна панель слота — сколько угодно документов (табы), лимит только UX.
- Хостинг кода на CDN Twitch бесплатно; контент — на CDN GitHub бесплатно. Ноль затрат, ноль серверов.
- Обновление контента через `git push` — без пересборки расширения и без ревью (ревью нужно только коду при публичном релизе).
- Config Service бесплатно решает per-channel настройки (у каждого стримера свой список документов).
- Svelte 5 здесь уместен: маленький бандл (бюджет мобильной первой загрузки 1 МБ), рунеты ($state/$derived) закрывают весь стейт, Vite мульти-HTML сборка из коробки.
- Версионирование контента бесплатно из коробки — это git.
### Минусы / ограничения
- 318×496 px — тесно для длинных документов; обязателен внутренний скролл и мобильный дизайн; pop-out спасает частично.
- CSP-allowlist: каждый внешний домен (jsDelivr, GitHub) надо руками вносить в консоли на версию — забыл = контент молча не грузится.
- Config Service публичен и мал (5 КБ) — секреты (PAT) там хранить нельзя.
- Нет серверного аплоада картинок в CDN Twitch в рантайме — картинки только через репо/внешний хост.
- Песочница: нет alert/confirm, нет вложенных iframe, storage API ограничен (localStorage под вопросом — проверить рано).
- Platform risk: Twitch вправе менять правила (Rig уже убили); расширение не переносится на другие платформы.
- Мобильная поддержка = отдельная mobile-ready версия вьюера + жёсткий бюджет 1 МБ/3 c.
- Для публичного релиза — ревью с гайдом/скриншотами; Released-версию нельзя править, только новая версия.
---
## 6. Библиотеки
| Назначение | Пакет | Комментарий |
|---|---|---|
| Фреймворк | `svelte@5`, `vite`, `@sveltejs/vite-plugin-svelte` | два entry: viewer.html / config.html |
| Markdown | `marked` | крошечный, GFM из коробки; либо `markdown-it` если нужны плагины |
| Санитайзер | `dompurify` | **обязателен** — контент внешний |
| Заголовки-анкоры | `marked-gfm-heading-id` или `github-slugger` | для оглавления в панели |
| Подсветка кода | `highlight.js` (опц.) | shiki тяжёлый для 1 МБ бюджета |
| Формулы | `katex` (опц.) | если нужны |
| Git API из UI | `octokit` (опц.) | коммит картинок из конфиг-вью |
| Local dev | `mkcert` + Twitch CLI / community DevRig | Rig от Twitch устарел; HTTPS обязателен даже локально |
| Типы Helper | своя `.d.ts` для `window.Twitch.ext` | npm-пакета с офиц. типами нет надёжного |
CSS-фреймворк не нужен — свои стили на 318px проще.
---
## 7. Что надо сделать (по шагам)
1. **Консоль** dev.twitch.tv → Extensions → создать (тип Panel), получить client id/secret.
2. **Скелет**: Vite + Svelte 5, два entry (`viewer.html`, `config.html`), скрипт Helper
в обоих, `mkcert` для localhost:8080, в консоли Base URI + пути вьюера/конфига.
3. **Вьювер**: чтение `configuration.broadcaster` (fallback — дефолтный demo-документ),
табы/пейджер, ленивый фетч MD, marked+DOMPurify, рерайт путей картинок, скролл.
4. **Конфиг-вью**: CRUD списка документов + порядок + заголовочные картинки, сохранение
сегмента через `configuration.set`. (В конфиг-вью JWT имеет роль broadcaster.)
5. **Репозиторий контента**: `docs/*.md`, `docs/assets/`, `index.json`; CSP-домены в консоли.
6. **Тест на своём канале**: Local Test → «Activate» в Extension Manager — ревью не нужно.
Потом Hosted Test (загрузка zip) — проверить кэши CDN Twitch.
7. **(Опц.) Публичный релиз**: walkthrough guide, change log, скриншоты, EULA/privacy →
Review → Released или Invite Only (allowlist стримеров).
Оценка трудоёмкости MVP (вьювер + конфиг + репо + тест на своём канале): 2–4 вечера.
---
## 8. Открытые вопросы (решить в процессе)
1. **localStorage в sandbox** — работает ли (кэш доков между сессиями)? Проверить первым делом.
2. **Точный лимит zip-билда** в консоли (официальной цифры в доках нет, форум даёт ориентиры ~несколько МБ) — некритично, билд маленький.
3. **PAT для коммита картинок из конфиг-вью**: в Config Service хранить нельзя (публично).
Варианты: вводить PAT сессией (in-memory), хранить в EBS, или отказаться от аплоада из UI —
картинки просто коммитятся в репо руками.
4. **Приватный контент?** Если да — только через EBS-прокси (или приватный репо + Worker).
5. **Мобильная версия**: делать ли mobile-ready сразу; как табы ведут себя в мобильной панели.
6. **Инвалидация кэша**: ветка@main (до 12ч задержка jsDelivr) vs SHA-пиннинг (мгновенно, но
нужно обновлять ссылку) — выбрать схему; компромисс: index.json на @main, он дешёвый.
7. **Pop-out у панелей** — заявлен в доках дизайна; проверить фактическое поведение и размеры.
8. **Формат index.json** и схема конфиг-сегмента (поля: id, title, headerImage, src, sha?, hidden, order) — зафиксировать до начала UI.
---
## 9. Источники
- Extensions overview / hosting / CSP: https://dev.twitch.tv/docs/extensions/
- Building (Helper, Config Service 5 КБ, PubSub): https://dev.twitch.tv/docs/extensions/building/
- Designing (панель 318×496, pop-out): https://dev.twitch.tv/docs/extensions/designing/
- Life cycle (Local Test, ревью, Released): https://dev.twitch.tv/docs/extensions/life-cycle/
- Guidelines & Policies (мобайл 1 МБ): https://dev.twitch.tv/docs/extensions/guidelines-and-policies/
- Mobile-видимость панелей: https://help.twitch.tv/s/article/how-to-use-extensions
- Статус платформы / deprecation Rig: https://dev.twitch.tv/docs/product-lifecycle/ и https://barrycarlyon.co.uk/wordpress/2023/04/04/the-twitch-extensions-developer-rig-is-dead/
- CORS jsDelivr/GitHub: проверено curl-ом (ACAO: * на raw.githubusercontent.com, cdn.jsdelivr.net, pages)
- Pop-out как элемент UI Twitch (отключить нельзя): RFC 0008 https://discuss.dev.twitch.com/t/rfc-0008-pop-out-support-for-extensions/16920
- vite-plugin-singlefile (один вход на билд, multi-entry не поддерживается): https://github.com/richardtallent/vite-plugin-singlefile
---
## 10. Итерация 2 — принятые решения
Уточнения от автора: подсветка кода и формулы не нужны; localStorage не нужен;
приватность не нужна; интерактив только — кнопки листания и ссылки; попапы/pop-out не нужны;
нужен билдер контента (кроп картинок, валидация MD, чистка лишнего); главный приоритет —
надёжная загрузка контента.
### 10.1 Билд фронтенда → два самодостаточных HTML
- Vite/Svelte по умолчанию НЕ дробит на мелкие чанки: код-сплиттинг возникает только из
`import()` и нескольких entry с общими модулями. Правило: **не использовать `import()` вообще**
(ленивость не нужна — контент лениво грузится по сети).
- Схема: `viewer.html` и `config.html`, каждый собирается отдельным билдом через
`vite-plugin-singlefile` → один HTML без внешних js/css (плагин официально не умеет
multi-entry, поэтому два конфига: `vite build -c vite.viewer.config.ts` + `vite build -c vite.config.config.ts`).
В консоли: Panel Viewer Path = `/viewer.html`, Panel Config Path = `/config.html`.
- Размеры: вьювер без marked/DOMPurify (см. 10.2) ≈ 1525 КБ, конфиг-вью ≈ 40–60 КБ —
на порядок ниже мобильного бюджета 1 МБ и лимитов zip.
- Альтернатива (проще): обычный multi-entry Vite без плагина → 2 html + 34 hashed-ассета.
Тоже корректно; single-file выигрывает надёжностью (меньше файлов, меньше шансов ошибиться в путях).
- CSP в консоли: `connect-src`/`img-src` = cdn.jsdelivr.net + raw.githubusercontent.com.
Шрифтов и GA нет — системный стек.
### 10.2 Пре-рендер MD в билдере + чистка на стороне зрителя
Рендер Markdown происходит **на этапе сборки (в CI)**, но решение «убрать санитайзер
из рантайма» пересмотрено (итерация 3): всё, что приходит с jsDelivr, считается
**недоверенными данными** (репо/CDN могут быть подменены), а доверенный корень — это
код вьювера (CDN Twitch) и конфиг-сегмент (Twitch). Чистка результата выполняется
у зрителя, до вставки в DOM — два варианта в 10.7.
Пайплайн: `git push → GitHub Actions → builder → ветка published → jsDelivr`.
### 10.3 Билдер контента (Node-скрипт, `builder/`)
Стек: `unified` + `remark-parse` + `remark-gfm` (AST + валидация по белому списку),
`gray-matter` (front-matter: title/order/hidden), `sharp` (картинки). Сериализация:
основной вариант — компактный JSON-AST (своя функция ~50 строк поверх mdast, см. 10.7);
запасной — `remark-rehype` + `rehype-external-links` + `rehype-stringify` → HTML
(тогда во вьювере DOMPurify).
- **Валидация MD по белому списку узлов AST**: heading, paragraph, emphasis/strong,
list/listItem, link, image, blockquote, hr, break, text. Всё прочее (html, code, table,
footnote…) = ошибка сборки с файлом и позицией. Белый список — один конфиг-объект.
- **Картинки (sharp)**: заголовочные — кроп 636×340 (318×170 @2x, fit:cover), webp;
inline — max width 640, webp q≈80; метаданные стирать; битые пути = ошибка билда;
лимит на размер исходника (напр. 10 МБ) и на суммарный вес документа.
- **Чистка лишнего (кандидаты, расширяемо)**: сырой HTML в MD; запрещённые конструкции;
битые относительные ссылки/картинки; нормализация уровней заголовков (в панели не выше h4);
трим пустых строк/пробелов; слишком большие документы → warning с размером.
- **Выход**: `published/index.json`, `published/docs/*.json` (или `*.html` в варианте A),
`published/img/*`; для каждого файла — SHA-256 в index.json (см. 10.7). Относительные
пути картинок работают на jsDelivr как есть (всё под одним `@sha`-префиксом) — рерайт не нужен.
### 10.4 Схема доставки (надёжность важнее автомагии)
- **Индекс читается вьювером напрямую из CDN**: `https://raw.githubusercontent.com/<u>/<r>/published/index.json`
(TTL raw — 5 минут, файл крошечный). Каждый entry содержит абсолютные jsDelivr-URL,
**запиннованные на SHA коммита** (`.../gh/<u>/<r>@<sha>/published/...`) — контент иммутабелен,
нет расхождения TTL: индекс ссылается только на документы, которые уже существуют.
- **Обновление контента полностью автоматическое**: push → CI собрал published →
через ≤5 минут зрители видят новое. Никаких кнопок и заходов в панель.
- **broadcaster-сегмент — только per-channel исключения** (опционально):
`{v:1, hidden:[id], order:[id]}`; пустой сегмент = порядок из index.json.
- Fallback: index недоступен → developer-сегмент → вшитый дефолтный документ.
- localStorage не используется.
### 10.5 Pop-out и интерактив
- Pop-out отключить нельзя — это кнопка UI Twitch (RFC 0008), не наша. Приложение просто
остаётся нейтральным к размеру окна.
- Интерактив внутри: только кнопки «‹ ›» и ссылки. Ссылки `target=_blank` разрешены
песочницей (allow-popups), top-navigation заблокирован самим Twitch — «увести» зрителя
со страницы нельзя.
### 10.6 Итоговый стек (после упрощений)
- Приложение: `svelte@5`, `vite`, `@sveltejs/vite-plugin-svelte`, `vite-plugin-singlefile`. Всё.
- Билдер: `unified`/`remark-parse`/`remark-gfm`, `gray-matter`, `sharp` (+rehype-стек в варианте A).
- Убраны: marked, highlight.js, katex. DOMPurify — по варианту 10.7 (A: в рантайме ~8 КБ gzip; B: не нужен вовсе).
### 10.7 Санитизация на стороне зрителя (итерация 3)
Модель доверия: доверенный корень = билд расширения (CDN Twitch) + конфиг-сегмент
(хранит Twitch, правит только аккаунт стримера). Всё, что приходит с jsDelivr (даже
запиннованное на SHA) — недоверенные данные: компрометация репо/CDN/GitHub-аккаунта не
должна приводить к исполнению чего-либо у зрителя. Важно: билдер живёт в том же репо и
тоже подменяем — поэтому граница доверия обязана находиться в коде, который раздаёт Twitch.
**Вариант B — рекомендуемый: JSON-AST вместо HTML, никакого `{@html}`**
- билдер сериализует проверенный mdast в компактное мини-дерево:
`["h2","Заголовок"]`, `["p","текст ",["strong","жирный"]]`, `["ul",[["li","пункт"]]]`,
`["a","https://…","текст"]`, `["img","https://…","alt"]`, `["blockquote",[…]]`, `["hr"]`;
- вьювер — один рекурсивный Svelte-компонент (~80–120 строк): строка = текст
(авто-эскейп Svelte), неизвестный узел = пропустить с warning в консоль,
URL проверяется по схеме (img: только https на cdn.jsdelivr.net / raw.githubusercontent.com;
a: только http/https), атрибуты кроме href/src/alt/title не мапятся вообще;
- санитайзер не нужен: в DOM физически попадают только узлы, которые отрисовал наш код.
Zero зависимостей — это и есть «попроще»: нечего чистить.
**Вариант A — запасной: HTML в выдаче + DOMPurify во вьювере**
~8–9 КБ gzip, одна строка вызова. Tight-конфиг: `ALLOWED_TAGS` по белому списку,
`ALLOWED_ATTR = ['href','src','alt','title']`, `ADD_ATTR: ['target']`. Стандарт индустрии,
mXSS закрыты, поддерживается активнее всех.
**Дополнительный слой — SHA-256 пиннинг (напрямую закрывает «подменят файлы»)**
- билдер считает SHA-256 каждого файла выдачи → index.json → конфиг-сегмент;
- вьювер: `const buf = await (await fetch(url)).arrayBuffer();`
`const digest = await crypto.subtle.digest('SHA-256', buf);` → сравнить со значением из
сегмента; не совпало → экран «контент изменился, обновите список в панели управления».
~10 строк, ноль зависимостей, работает во всех браузерах (secure context).
- согласуется с @sha-пиннингом: старый контент остаётся доступен по старому SHA, поэтому
push без нажатия «Обновить список» ничего не ломает — зритель видит прежнюю версию.
- картинки через `<img>` так не проверить; для них достаточно host-allowlist в рендерере;
максимум паранойи — тот же fetch+hash+objectURL.
Остаточный риск (честно): компрометация Twitch-аккаунта = компрометация доверенного корня
(атакующий правит сегмент и хеши). Это граница любой схемы без EBS.
### 10.8 Итерация 4 — финальные решения
- **SHA-256 слой отклонён**: лишний обязательный шаг («обновила файл — зайди в панель и
нажми кнопку») перевешивает пользу. Подмена ловится схемой B + host-allowlist.
- **Принят вариант B**: JSON-AST, рекурсивный рендерер, никакого `{@html}` и санитайзера.
- **Доставка упрощена** (см. 10.4): index.json вьювер берёт прямо с raw@published
контентные пуши доезжают до зрителей сами за ≤5 минут; сегмент — только исключения.
- Реализован скелет: два single-file билда (viewer/config), вьювер с пейджером,
конфиг-панель (скрыть/порядок), билдер (валидация + JSON-AST + sharp), sample-контент, CI.