RTP DesportoJudo/Mundiais. Miguel Gago já esqueceu Budapeste e quer melhorar em BakuThe Jerusalem PostUK's Greens adopt ‘Zionism is racism’ motion, become Europe’s first mainstream anti-Zionist partyPunchNorthern senators seek military action against Adamawa insurgentsESPNSources: Mayfield out 3-6 weeks, depending on when QB can grip footballESPN DeportesColts y Commanders elevan el nivel de sus ofensivas y se cierra el juegoInquirerWalang pasok: In-person classes suspended Oct. 5 for Nat’l Teachers’ DayZDF heuteEntdecken Sie das ZDF-NachrichtenstudioVarietyZach Bryan Breaks Attendance Record at Robert Kraft’s Gillette Stadium While Wearing ‘Free Palestine’ Shirt for Second Night in a RowSportstarIndian sports wrap, October 4: India U-17 men share spoils with Myanmar in first friendly01netXiaomi 15 : ce n’est plus le roi, mais au prix d’un Redmi il vaut son pesant d’or (-47%)Premium TimesLawal flags off 95km road in Tsafe, says project will improve securityCBS NewsTranscript: Brett McGurk on "Face the Nation with Margaret Brennan," Oct. 4, 2026
The Daily Newsstand · Free, Always
Sunday, October 4, 2026

Как я подавал шахматное расширение в Chrome Web Store: разрешения, данные и отказ

Translate

Вторая часть истории о том, как моё шахматное расширение попадало в Chrome Web Store. Первая была про оплату аккаунта разработчика и мои ошибки, эта про саму подачу: что спрашивает форма, какие объяснения прошли проверку, где я чуть не написал неправду в разделе о данных и как получил отказ по своей же вине.

В конце мая я начал эксперимент для себя: запустить Stockfish прямо в браузере, рядом с доской, а не держать анализ в соседней вкладке. Из него выросло расширение ChessNavio, первая версия вышла 25 июля. Оно читает позицию с доски на Lichess, Chess.com и ещё паре шахматных сайтов и показывает ход рядом с ней. Stockfish 18 и нейросеть Maia лежат внутри пакета и считают на компьютере пользователя. Как это устроено, я рассказывал в первой статье, а как учил его предлагать человеческие ходы вместо идеальных — во второй.

Сразу про очевидный вопрос. Да, подсказки работают и в партии с живым соперником. Многие площадки запрещают внешнюю помощь в рейтинговых играх, расширение об этом напоминает, но решение оставляет игроку. Жёстко я запретил одно: в игре с ботом расширение умеет само сделать ход, а в партии с человеком этого нет.

До магазина расширение раздавалось архивом с GitHub: скачать ZIP, распаковать, включить в Chrome режим разработчика, загрузить папку. Шесть шагов в инструкции, и каждое обновление снова руками. В Chrome Web Store, дальше просто CWS, я хотел ещё в августе, но долго не мог решить вопрос с оплатой аккаунта, про это была предыдущая часть.

Шёл я туда с уверенностью, что люди теряются на установке. Цифры, которые я посмотрел уже после подачи, показали другое: из начавших скачивание до конца инструкции доходят 85%, а больше половины уходят со страницы установки, так и не начав. Похоже, пугает сам вид инструкции, и магазин снимает эту проблему.

Понятно было заранее, к чему будут вопросы на проверке: доступ к чужим сайтам, фоновые задачи и больше восьмидесяти мегабайт движков.

Что придётся заполнить

Как собрать расширение и упаковать архив, хорошо описано в статье KTS, повторять не буду. Архив загружается одной кнопкой, основная работа в полях вокруг него.

Вот что у меня спросили, если не считать описания и картинок:

Раздел

Что там

Единое назначение

одно предложение о том, что делает расширение

Разрешения

отдельное объяснение под каждое разрешение из манифеста

Доступ к хостам

зачем нужен каждый адрес

Удалённый код

да или нет; пояснение если «да»

Данные

какие категории собираются и куда уходят

Статус продавца

торговец вы или нет по определению ЕС

Распространение

видимость, регионы, есть ли покупки

Инструкция проверяющему

как добраться до основной функции

Ещё три мелочи. Аккаунт издателя я завёл отдельный, не личный. Страну платёжного профиля поменять потом непросто: в комментариях к первой части поправили, что можно раз в год, сам я не проверял. Публикацию после одобрения можно сделать автоматической или ручной, я выбрал автоматическую.

Картинки нужны своего размера под каждый слот: 1280×800 для скриншотов, 440×280 и 1400×560 для промо, иконка 128×128.

Единое назначение

Расширение должно делать одну вещь, и её надо описать одним предложением. У меня получилось так:

The single purpose of ChessNavio is to analyse the chess position shown on
the current page and display a suggested move next to the board.

Поле выглядит как формальность, но на нём держится всё остальное: с этим предложением потом сверяют каждое разрешение.

Разрешения

Форма сама разбирает манифест и по каждому разрешению выводит поле до 1000 символов, без примеров. Слева подсказка, ради которой единое назначение и стоило сформулировать точно: «Удалите все разрешения, не связанные с единственным назначением расширения. Если версия расширения запрашивает ненужные разрешения, она будет отклонена». Над полями у меня висело ещё одно предупреждение: из-за доступа к хостам может понадобиться детальная проверка, и публикация задержится.

Обоснования разрешений в кабинете Chrome Web Store

Обоснования разрешений в кабинете Chrome Web Store

Разрешений у меня было семь: storage, offscreen, alarms, activeTab, scripting, webNavigation и identity. Первые варианты я написал по памяти, потом сверил каждый с кодом. Лишних разрешений не нашлось. Зато три объяснения пришлось переписать: два к разрешениям (одно было неверным, другое слишком общим) и объяснение к доступу к хостам, где я забыл адрес.

Из этого я вынес для себя одно: в объяснении должно быть что-то, что проверяющий может сверить за минуту, не запуская расширение. Общие слова, как мне кажется, он и так видит в каждой второй заявке.

storage: неверно по факту

Сначала я написал, что в chrome.storage хранится история партий.

История живёт в IndexedDB, ей разрешение не нужно. Если бы проверяющий стал искать историю в chrome.storage, он бы её не нашёл, и вывод напрашивался бы один из двух: автор не знает своего кода или прячет настоящее назначение.

Как стало:

Stores the extension settings — hint mode, engine strength, interface
language, and the list of chess sites the user has connected — together with
their backups and the first-run state.

Всё перечисленное находится в коде поиском по названию.

scripting: правда, но слабая

Первый вариант был такой: расширение встраивает скрипты на страницы шахматных сайтов. Это правда, но так может написать про себя любое расширение, в том числе то, которое ворует пароли. Проверить в этой фразе нечего.

При этом довод получше у меня был, просто я не сразу о нём вспомнил. В манифесте нет ни одного статического контент-скрипта: всё регистрируется на ходу и только для сайтов, которые человек подключил сам. Переписал так:

The extension ships no static content scripts. Everything is registered at
runtime with registerContentScripts, and only for the chess sites the user
has connected; unregisterContentScripts removes them when a site is
disconnected. executeScript also shows the short explanation when a board
cannot be read on the current page.

Проверяется это быстро: в манифесте content_scripts — пустой массив, а registerContentScripts и unregisterContentScripts находятся в коде за пару секунд.

Доступ к хостам: пропуск, который заметят

В первой версии я перечислил шахматные сайты и забыл api.chess.com. Через него расширение восстанавливает запись партии из публичного архива игрока.

Из трёх случаев этот, по-моему, худший. Если проверяющий откроет сетевые запросы и увидит адрес, которого в объяснении нет, у него сразу появится вопрос, что ещё я не упомянул.

Текст получился длинным, но про каждый адрес в нём сказано, зачем он нужен:

Host permissions cover the supported chess platforms — Lichess, Chess.com,
World Chess, Chess King — where the extension reads the board position and
draws the hint panel; api.chess.com, used to fetch the player's own public
game archive when a game record needs to be recovered; and the ChessNavio
site, used to sign in and check the subscription state. The broad optional
pattern is requested only when the user explicitly clicks the toolbar button
on another chess site to connect it, and never automatically.

Последняя фраза — про широкие шаблоны http://*/* и https://*/*. Они лежат в необязательных разрешениях:

host_permissions: [ /* только площадки и наши домены */ ],
optional_host_permissions: ['http://*/*', 'https://*/*'],

При установке браузер о них не спрашивает. Доступ человек даёт сам, когда нажимает кнопку расширения на сайте, которого нет в списке. Стой шаблоны в обычных host_permissions, Chrome при установке показал бы «Чтение и изменение всех ваших данных на всех сайтах».

Остальные четыре

Их я не переписывал: в них с самого начала было что сверить с кодом.

В offscreen написано, что движки работают в отдельном скрытом документе, а не в фоновом скрипте расширения. Иначе тяжёлый расчёт на WebAssembly тормозил бы страницу. В коде это видно сразу: при создании документа указана причина reasons: ['WORKERS'].

В alarms описаны две постоянные фоновые задачи: синхронизация законченных партий, если человек её включил, и восстановление прерванной записи партии. Третья задача — проверка обновлений, и она работает только в копии, поставленной не из CWS. Откуда эта оговорка, расскажу ниже, в разделе про две копии.

В webNavigation перечислены четыре события навигации, на которые подписано расширение. Главное из них onHistoryStateUpdated. Lichess и Chess.com — одностраничные приложения: партия сменилась, адрес в строке тоже, а страница не перезагрузилась. Без этого события панель так и осталась бы на прошлой партии.

activeTab даёт расширению доступ к текущей вкладке, но только в момент, когда человек сам нажал на кнопку расширения, и только до закрытия вкладки. Мне это нужно для сайтов, которых нет в списке: по нажатию расширение пробует найти на странице доску, а если не находит, показывает короткое пояснение. В объяснении я так и написал и добавил в конце, что постоянного доступа к сайту расширение при этом не просит. Такого вопроса в форме нет, но его, скорее всего, задаст себе проверяющий: зачем доступ к произвольной вкладке.

identity: разрешение, которое я убрал зря

С седьмым я ошибся. Не вспомнил, где оно вызывается, поискал chrome.identity и browser.identity, ничего не нашёл и решил, что разрешение осталось от старого замысла. Убрал его и написал в коммите, что оно не используется.

А вызов был, только через локальную переменную:

const identity = (browser as unknown as { identity?: IdentityApi }).identity;
// ...
const result = await identity.launchWebAuthFlow({ url: authorizeUrl, interactive: true });

Искать надо было сам метод: grep -rn launchWebAuthFlow сразу показывает файл. Чем это кончилось, расскажу в разделе «Отказ».

Удалённый код и вес

Вопрос про удалённый код обязательный, с ответом «да» или «нет». Удалённым форма считает любой JavaScript или Wasm, которого нет в пакете, включая строки для eval(). А у меня внутри Stockfish на WebAssembly, ONNX Runtime и семь вызовов importScripts. Прежде чем отвечать «нет», я проверил сборку:

Проверка

Результат

eval, new Function, document.write

ни одного

Сторонние адреса в сборке

нет: свои домены, шахматные площадки, ссылка на релизы

importScripts, 7 вхождений

связующий код Stockfish и ONNX Runtime, грузит файлы пакета

Содержимое архива

52 файла, без карт исходников, тестов и .md

Пояснение к «нет» я заготовил заранее, но вставлять его оказалось некуда: поле для объяснения в форме относится к ответу «да». Так что ответ — одна отметка. Подтверждает её CSP, о котором форма не спрашивает: выполнять чужой код запрещает сам манифест.

content_security_policy: {
  extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
},

wasm-unsafe-eval нужен движкам, всё остальное ограничено 'self'.

Отдельный вопрос — вес. Распакованное расширение занимает 85 МБ, ZIP — 63 МБ:

maia/       58 МБ   модель ONNX, человекоподобные ходы
stockfish/  25 МБ   движок на WebAssembly
opening/   851 КБ   дебютная книга
остальное  1,7 МБ   28 файлов кода, разметки и шрифтов

Код самого расширения занимает меньше двух мегабайт, остальное — два движка. Архив на 63 мегабайта сам по себе вызывает вопросы, поэтому мне важно было, чтобы из всех ответов было понятно, откуда этот вес: движки лежат внутри пакета специально, чтобы позиция не уходила с компьютера на сервер.

Данные: где я чуть не написал неправду

В разделе про данные отмечаешь категории и подтверждаешь три пункта: данные не продаются, не используются для посторонних целей и не идут на оценку кредитоспособности.

Форма передачи данных в кабинете Chrome Web Store

Форма передачи данных в кабинете Chrome Web Store

Категорий девять, от медицинских данных до содержимого сайтов. Подсказка слева напоминает, что ответы увидят все на странице расширения, а публикуя его, вы подтверждаете, что они совпадают с политикой конфиденциальности. Расхождение между формой и политикой — тоже ошибка.

Текст для этого раздела я подготовил заранее, и там стояло: позиции и партии на сервер не отправляются. Это неправда. Метод syncCompletedGame отправляет initialFen, finalFen и пару beforeFen/afterFen для каждого хода, то есть расстановку фигур до и после каждого хода.

Оправдывает меня одно: синхронизация по умолчанию выключена. cloudHistoryEnabled: false стоит в трёх местах, и включает её сам пользователь. Но я-то написал не «выключено по умолчанию», а «не отправляется».

Формулировку я переписал, а заодно и политику конфиденциальности: в перечне передаваемых данных там тоже были только «ходы», без позиций.

Отказ на проверке неприятен, но его можно исправить и подать снова. Если расхождение между заявленным и настоящим поведением найдут уже после выхода, расширение могут убрать из каталога.

С категориями я рассуждал так же. «Содержимое сайтов» формально можно было не отмечать: расстановку фигур расширение читает со страницы, но обрабатывает на устройстве, а наружу она уходит только при включённой синхронизации. Я отметил. Заявишь лишнее, и карточка выглядит осторожнее, чем надо. Не заявишь нужное, и расширение могут снять.

Статус продавца

Спрашивают, торговец вы или нет по определению ЕС. Я отметил, что нет. Адрес тогда не публикуется, но в странах ЕЭЗ расширение может быть недоступно. Это я узнал уже после ответа. Меня это устроило: по Метрике за последний месяц 93,7% аудитории из России.

В опубликованной карточке это выглядит так: рядом с версией стоит «Не продавец» и предупреждение покупателям из ЕС, что законы о защите прав потребителей на сделки с этим разработчиком не распространяются. Там же «Есть платный контент» — это ответ на вопрос о покупках из раздела распространения.

Раздел «Подробности» в карточке Chrome Web Store

Раздел «Подробности» в карточке Chrome Web Store

Проверяющему нужен вход

Без входа в аккаунт основная функция не работает, и проверяющий дальше экрана «Sign in to ChessNavio» не пройдёт. Поэтому я завёл ему отдельный аккаунт с активной подпиской и написал короткую инструкцию: войти на сайте, подключить Lichess и начать партию с компьютером. Такой сценарий не требует второго игрока и не касается партий с людьми.

Отказ

Первую версию, 1.3.1, я подал 28 сентября. Через два дня пришёл отказ по пункту «Неверное описание: не работают функции», код нарушения Red Potassium. В описании заявлена возможность, которую проверяющий не смог найти или воспроизвести: «Sign-in or Create account».

Письмо Chrome Web Store с отказом

Письмо Chrome Web Store с отказом

Письмо короткое и по делу. Исправить предлагают двумя способами: сделать так, чтобы функция работала, или удалить из пакета её код. Ниже цитата из правил: если в описании есть ложная или вводящая в заблуждение информация, продукт могут удалить. И путь для апелляции: «Сборка → Статус → Подать апелляцию». Апеллировать я не стал, ошибка была моя.

Причина — то самое убранное identity. Без него Chrome не отдаёт chrome.identity, и кнопка «Войти или зарегистрироваться» молча ничего не делала. Не только у проверяющего, у всех.

Почему не поймали тесты: они проверяют вход с подменённым identity и манифест не читают, так что связь «вызываем API — объявили разрешение» никто не проверял. Теперь для этого есть отдельный тест, он падает, если код вызывает API без разрешения в манифесте:

const PERMISSION_APIS: Record<string, RegExp> = {
  identity: /\b(launchWebAuthFlow|getRedirectURL|getAuthToken|getProfileUserInfo)\b/,
  offscreen: /\b(createDocument|closeDocument|hasDocument)\b/,
  alarms: /\balarms\.(create|onAlarm|clear)\b/,
  scripting: /\bscripting\.(registerContentScripts|executeScript|insertCSS)\b/,
  webNavigation: /\bwebNavigation\.\w+/,
};

for (const [permission, api] of Object.entries(PERMISSION_APIS)) {
  it(`declares "${permission}" while its API is called`, () => {
    if (!api.test(sources)) return;
    expect(declared).toContain(permission);
  });
}

Ограничение у теста одно: список методов для каждого разрешения я веду руками, новый API нужно добавлять самому.

Версию 1.3.2 с возвращённым разрешением и переписанной инструкцией я подал в тот же день. Объяснение для identity в этот раз написал так, чтобы каждое утверждение можно было проверить по пакету:

Signs the user in to their ChessNavio account: launchWebAuthFlow opens the
sign-in page on chessnavio.ru in a Chrome-controlled window and returns a
one-time PKCE code, which is exchanged for a device token at
api.chessnavio.ru/api/v1/extension/token. No Google account data is read —
getAuthToken and getProfileUserInfo do not appear anywhere in the package,
and launchWebAuthFlow with getRedirectURL are the only chrome.identity APIs
it contains.

Во второй половине ответ на вопрос, который при слове identity напрашивается первым: не читает ли расширение данные Google-аккаунта.

28.09   1.3.1 подана
30.09   отказ, Red Potassium
30.09   1.3.2 подана заново
03.10   1.3.2 одобрена

Первая проверка заняла два дня, вторая три.

Две копии одного расширения

Одну вещь пришлось поменять в самом расширении ещё до подачи. Раз в шесть часов оно проверяло, не вышла ли новая версия, и показывало значок со ссылкой на страницу релизов. Копии из CWS это только вредит: обновляет её сам Chrome, а значок звал бы человека обратно к ручной установке.

Отличить установку из CWS можно через management.getSelf(), и для этого не нужно разрешение management:

async function isStoreInstall(): Promise<boolean> {
  try {
    const self = await browser.management?.getSelf?.();
    return self?.installType === 'normal';
  } catch {
    return false;
  }
}

У распакованной копии installType равен development, у установленной из CWS — normal.

Есть и второе следствие. У копии из CWS и копии с GitHub разные ID расширения, а значит и отдельное локальное хранилище. Вход, настройки и история сами не переедут: человек, который ставил расширение с GitHub, получит из CWS новую установку с нуля. Переносить это между расширениями без отдельного защищённого протокола и согласия пользователя я не стал. Вместо этого при переходе я решил прямо говорить, что новая версия ставится отдельно, а старую копию удалять только после входа в новую.

Мой чек-лист перед подачей

  • каждое обоснование содержит то, что проверяется по коду или манифесту за минуту;

  • разрешение считается лишним, только когда найден и проверен код, которому оно нужно; искать метод, а не пространство имён;

  • после любой правки манифеста собранный пакет ставится заново и основной сценарий проходится руками;

  • текст про данные сверен с тем, что расширение реально отправляет, включая выключенные по умолчанию функции;

  • все адреса из сетевых запросов есть в объяснении доступа к хостам;

  • у проверяющего есть аккаунт и пошаговый сценарий до основной функции.

Чем кончилось

В итоге 1.3.2 одобрили. Вот как выглядит раздел о конфиденциальности в карточке:

Раздел «Конфиденциальность» в карточке Chrome Web Store

Раздел «Конфиденциальность» в карточке Chrome Web Store

Две категории: «информация, позволяющая идентифицировать личность» (адрес почты для входа) и «содержимое сайтов».

Если вы подавали расширение с WebAssembly или с доступом к чужим сайтам, напишите, сколько шла проверка и к чему придирались. Мне интересно, насколько два и три дня — это типично.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.