# Подключение Chrome-расширения

## Базовый путь без backend

1. Создай проект в Ext Ops Panel: ID опубликованного расширения или черновик. Для черновика заполни листинг и пройди аудит; ID появится после публикации.
2. В «Настройка → Hosted-страницы» возьми точные URL welcome и uninstall этого проекта. Не конструируй код страницы или домен по догадке.
3. В MV3 service worker открой welcome только при `reason === 'install'`, затем вызови `chrome.runtime.setUninstallURL(uninstallUrl)`. Проверь, что разрешения и фоновый скрипт в manifest подходят существующему расширению. Сохраняй текущий install handler, если он уже есть.
4. В панели настрой uninstall-причины, текст возврата и Rate Us. Добавь URL политики конфиденциальности в листинг при необходимости.
5. Запусти «Проверка подключения» и проверь события на тестовой установке. Не делай вывод об успешной интеграции по одному наличию URL в коде.

Пример кода, который нужно адаптировать к существующему service worker:

```js
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') chrome.tabs.create({ url: welcomeUrl });
});
chrome.runtime.setUninstallURL(uninstallUrl);
```

## Дополнительные подключения

- **Feedback на своём сайте:** скрипт `https://extops.dev/widget.js` и публичный `data-extops-widget` из настроек. Для собственной формы используй feedback OpenAPI. Не отправляй реальное сообщение просто ради проверки UI.
  - **Вошедшие пользователи (необязательно):** когда владелец включил в настройках виджета «Принимать данные пользователя со страницы», страница после загрузки `widget.js` вызывает `ExtOpsFeedback.identify({ user_id, email, name, meta, token })`, а при выходе `ExtOpsFeedback.identify(null)`; `open({ topic, message, contact, meta })` прикладывает контекст обращения (до 10 ключей `[a-z0-9_]{1,32}`, значения строка или число до 200 символов, без секретов и данных карт). Без `token` обращение приходит как «не подтверждено».
  - **Подтверждённая личность:** подпись считается только на сервере владельца секретом личности виджета (`wis_…`, создаётся в настройках виджета): `hex(HMAC-SHA256(secret, "v1|" + widget_key + "|" + user_id + "|" + email + "|" + exp))`, `exp` в Unix-секундах не дальше 24 часов вперёд, отсутствующее поле подписывается пустой строкой. Страница получает `{ exp, sig }` от этого серверного эндпоинта и обновляет его до `exp`. Секрет никогда не клади в страницу, код расширения или вывод инструмента.
  - **Переписка в виджете (необязательно, платно):** владелец отвечает людям из панели, а вошедший посетитель читает ответы и пишет в ответ прямо в виджете; почта не отправляется никогда. Работает, только когда выполнено всё сразу: включено «Принимать данные пользователя со страницы», у виджета есть секрет личности, тариф владельца включает CRM обратной связи (Indie, Studio или Fleet) и владелец включил «Переписку в виджете». Тогда `GET /api/v1/feedback/widget/config` отдаёт `"replies": true` (иначе поля нет), а `widget.js` загружает `widget-thread.js` с того же origin, только если у текущего `identify()` есть `token`: бейдж с непрочитанными ответами, вид «Переписка», `ExtOpsFeedback.open({ view: 'thread' })` (без модуля открывается обычная форма). CSP хоста ничего нового не требует. Чтение требует ВАЛИДНОЙ подписи, поэтому для таких виджетов подписывай с `exp` примерно на час вперёд и обновляй, снова вызывая `identify()` со свежим токеном; истёкший токен прячет переписку до этого момента.
  - **Свой диалог или строгий CSP:** `POST /api/v1/feedback/widget/thread` с `{ widget_key, user, identity, mark_seen?, summary? }` возвращает `{ messages: [{ id, from: "you" | "team", text, at }], unread }` (старые первыми, не больше 50; `summary: true` возвращает только `{ unread }`); `POST /api/v1/feedback/widget/thread/messages` с `{ widget_key, user, identity, message, event_id, dwell_ms }` пишет продолжение. `text` это простой текст: выводи только как текст (`textContent`), никогда как HTML. Неверная или истёкшая подпись даёт 401 `identity_invalid`; любой отказ до проверки подписи (переписка выключена, владелец на Free, чужой origin, неизвестный ключ) даёт один и тот же 404. Поля и лимиты сверяй с feedback OpenAPI.
  - **Люди и ответы в панели (CRM обратной связи, платно):** сообщения виджета, в которых есть `user_id` или `email` из `identify()`, группируются в людей внутри одного проекта; вкладка «Обратная связь» проекта переключается между «Лентой» и «Людьми», а в веб-приложении есть раздел «Обращения» с людьми из всех проектов владельца (расширение ведёт туда ссылкой). Подписанная личность и тот же id или e-mail без валидной подписи – два разных человека с отметками «Подтверждено» и «Не подтверждено», они не склеиваются никогда. Сообщения без данных `identify()` остаются только в ленте; свободное поле контакта человека не создаёт никогда. Поэтому, если владельцу нужны люди, передавай `user_id` или `email` в `identify()`, а подписывай, когда нужны подтверждённые люди или переписка в виджете. В ленте человека владелец видит сообщения и ответы по времени, ставит «Открыто» или «Решено» (новое сообщение снова открывает решённого), ведёт личную заметку, которую человек не видит никогда, отвечает простым текстом (до 4000 символов; ответ не редактируется, только удаляется) или жмёт «Ответить из почты», если есть e-mail: откроется почтовая программа владельца с цитатой сообщения, а в ленте запишется, что он ответил; почту мы не отправляем никогда. В списке «Отвечено» стоит, когда последняя запись переписки от владельца. Тариф: entitlement `FeedbackCRM` тарифа аккаунта владельца (Indie, Studio, Fleet); триал его не открывает. На Free лента работает как раньше, «Люди» показывают замок, а привязка сообщений к людям продолжается, поэтому после апгрейда история полная. Хранение: переписка без срока на тарифах с CRM; анонимные сообщения виджета, опрос при удалении, Rate Us и всё на Free хранятся 180 дней; после окончания тарифа с CRM правило 180 дней начинает действовать через 30 дней. Удаление человека («Удалить переписку и данные человека» в меню человека) стирает его сообщения, ответы владельца и заметку на любом тарифе; это путь для запроса на удаление данных. Не создавай тестовых людей и ответы в продуктивном проекте.
- **Кастом-метрики:** реализуй GET-эндпоинт на своём сервисе, возвращающий payload `schema_version: "2.0"`; настрой источник и авторизацию в панели. Сверь `kinds`, `render` и обязательные поля с OpenAPI.
- **Мониторинг расширения:** настрой heartbeat или URL-пинг в проекте. Токен heartbeat хранится на сервере/агенте; POST в `/api/v1/monitoring/report` использует контракт v2 с обязательным статусом `overall`.
- **Мониторинг веб-проекта/окружения:** отдельный POST `/api/v1/heartbeat/report`. Минимальный payload `{ "overall": "ok" }`, допустимые статусы `ok`, `warn`, `crit`; также поддерживается богатый payload метрик v2. Используй токен окружения, показанный при регистрации heartbeat.

## Эндпоинт карточки клиента

**Что это.** Когда владелец открывает подтверждённого человека в «Людях» (вкладка «Обратная связь» проекта или «Обращения» веб-приложения), наш сервер отправляет подписанный `POST` на HTTPS-эндпоинт сервера владельца и показывает ответ рядом с перепиской: план, статус подписки, платежи, счётчики продукта, ссылки в админку владельца. Отвечает сервер владельца, вызывает наш, как у кастом-метрик. Карточку мы не храним (ни в базе, ни в кэше, ни в логах); каждое открытие человека – новый запрос. Действия карточки панель никогда не исполняет. Контракт: https://extops.dev/ru/docs/customer-card, OpenAPI: https://extops.dev/customer-card-openapi.yaml (эндпоинт описан в `webhooks`). В репозитории Ext Ops Panel канон – `docs/customer-card-contract.md`.

**До сборки.**

- Карточка входит в CRM обратной связи тарифа АККАУНТА владельца (Indie, Studio, Fleet); триал её не открывает. После даунгрейда настройки сохраняются, но карточка не запрашивается.
- **Только подтверждённые.** Карточка запрашивается только для человека, чью личность подписал сервер владельца (`identify()` с `token`, подпись v1 секретом `wis_…`, см. «Подтверждённая личность» выше). Для неподтверждённого запроса нет вовсе, а панель пишет «Карточка доступна только для подтверждённых пользователей»: e-mail может ввести кто угодно, и рядом с его сообщением оказались бы данные чужого клиента. Поэтому сначала настрой подпись личности; без неё карточка не появится никогда.
- Мы можем выключить запросы карточки для всех: тогда карточка не запрашивается, карточка и «Проверить» отвечают 404, а настройки владельца сохраняются. Не пытайся это обойти.

**Где настраивается.** Проект → «Настройка» → секция «Виджет обратной связи» → подраздел «Карточка клиента» (только владелец). Вкладка «Обратная связь» только показывает: у человека без карточки есть ссылка «Настроить в настройках проекта», она ведёт в этот подраздел.

1. «Адрес эндпоинта»: `https://…` только на порту 443, публичный хост (не `localhost`, `.local`, `.internal` и не частный IP). Проверяется при сохранении. Редиректы не выполняются.
2. «Выпустить секрет»: `ccs_` + base64url 32 случайных байт. Показывается ОДИН раз; дальше панель показывает только последние 4 символа. Владелец копирует его в конфигурацию своего сервера (переменная окружения или хранилище секретов, например `EXTOPS_CARD_SECRET`).
3. «Заменить секрет»: новый показывается один раз, **прежний действует ещё 24 часа**, и в это окно каждый запрос несёт две подписи. Эндпоинту нужен один секрет за раз: выкатывай новое значение в любой момент в течение 24 часов.
4. «Проверить»: запрос с `test: true` и личностью, введённой в форму (пустые поля отправят `test` / `test@example.com`). Панель покажет «Карточка получена, полей: N», 404 как «клиент не найден, подпись прошла проверку» или HTTP-статус и до 300 байт ответа. Не чаще 10 проверок в час на проект.
5. «Показывать карточку»: включается только с сохранёнными адресом и секретом.

**Запрос нашего сервера.**

```http
POST https://support.example.com/extops/customer-card
Content-Type: application/json
Accept: application/json
User-Agent: ExtOpsPanel-CustomerCard/1.0 (+https://extops.dev/docs/customer-card)
X-ExtOps-Card-Timestamp: 1760371200
X-ExtOps-Card-Signature: v1=035a47842ca90c1fd55619eccaa6aa87b4074f9157c810948d8fba656461c283
X-ExtOps-Request-Id: 6b0d1c9e-2f4a-4c55-9d0e-1f2a3b4c5d6e

{"schema_version":"1.0","project_id":"0c6a5c1e-6f1b-4b8e-9a52-3d1f0e7c2a10","person":{"user_id":"u_42","email":"anna@example.com","verified":true},"locale":"en","test":false}
```

- `project_id` – наш id проекта (один эндпоинт может обслуживать несколько проектов). `person.user_id` (≤ 128) и `person.email` (≤ 254) – то, что подписал сервер владельца, любое из них может быть `null`; `person.verified` всегда `true`. `locale` – язык панели читателя: `en`, `ru`, `de`, `es`, `fr`, `pt`. `test: true` – кнопка «Проверить»: подойдёт демонстрационная карточка.
- Имя, `meta`, текст сообщений и наш внутренний id человека не отправляются никогда. Метод всегда `POST`, поэтому e-mail и id не попадают в адресную строку и логи доступа.
- Таймаут 3 секунды на весь запрос (соединение 2 с), повторов нет; `3xx` считается сбоем. Отвечай из своей базы, без медленных вызовов третьих сервисов.

**Подпись v1.**

```
signed    = "v1|" + timestamp + "|" + METHOD + "|" + path_with_query + "|" + hex(SHA256(raw_body))
signature = hex(HMAC-SHA256(card_secret, signed))
header    = "v1=" + signature
```

- `timestamp` – заголовок `X-ExtOps-Card-Timestamp` (Unix-секунды); `METHOD` – `POST`; `path_with_query` – путь настроенного адреса вместе с query, как в строке запроса HTTP (`/extops/customer-card`, `/hooks/card?project=a`; пустой путь – `/`). Если обратный прокси переписывает путь, сверяй с путём настроенного адреса.
- `hex` в нижнем регистре; `card_secret` – строка `ccs_…` целиком как байты UTF-8; хэш считается над **сырым телом** до разбора JSON.
- После замены секрета 24 часа заголовок несёт две подписи через запятую: `v1=<новым секретом>,v1=<прежним секретом>`. Принимай запрос, если совпала хотя бы одна.
- Эндпоинт проверяет: оба заголовка есть; `|now − timestamp| ≤ 300` секунд; хотя бы одна подпись совпала, сравнение за постоянное время. Иначе отвечай `401`. Строгая защита от повтора по желанию: хранить `X-ExtOps-Request-Id` 5 минут.

Проверка на Node (Express), тот же текст, что в контракте и в стартовом сниппете настроек панели (версии для Go и Python – на странице контракта):

```js
// Node (Express): needs the raw body, for example express.raw({ type: 'application/json' })
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

export function verifyCard(req, secret) {
  const ts = req.get('X-ExtOps-Card-Timestamp');
  const header = req.get('X-ExtOps-Card-Signature') || '';
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const bodyHash = createHash('sha256').update(req.body).digest('hex');
  const signed = `v1|${ts}|${req.method}|${req.originalUrl}|${bodyHash}`;
  const want = Buffer.from(createHmac('sha256', secret).update(signed).digest('hex'));
  return header.split(',').some((part) => {
    const got = Buffer.from(part.trim().replace(/^v1=/, ''));
    return got.length === want.length && timingSafeEqual(got, want);
  });
}
```

Тест-вектор для юнит-теста (зафиксируй часы, метка времени старая): секрет `ccs_test_vector_secret_not_for_production`, прежний секрет `ccs_previous_test_vector_secret`, timestamp `1760371200`, `POST /extops/customer-card`, тело выше одной строкой без перевода строки в конце. `hex(SHA256(body))` = `dcd0d48f2fb5d26f835b39e51076c79db02fc2bc4b98a364ab5c506959f45146`, подпись = `035a47842ca90c1fd55619eccaa6aa87b4074f9157c810948d8fba656461c283`, прежним секретом = `1f1ecbc40946bd56eefa381a7c65e01c780a28c37f5113b345d500ed3b0b94cd`.

**Статусы.** `200` + JSON карточки: карточка. `404`: такого клиента в системе владельца нет, панель покажет «Клиент не найден в вашей системе». Любой другой (`401`, `5xx`, `3xx`, таймаут, не JSON, нарушены лимиты): «Карточка недоступна», переписка работает как обычно. Тело ответа с ошибкой читается не дальше 300 байт и видно только владельцу через «Проверить».

**Схема карточки** (`Content-Type: application/json`, UTF-8, тело ≤ 64 KiB):

```text
Card    { schema_version: "1.0" (обязательно, мажор 1), title?: ≤ 80, subtitle?: ≤ 160, status?: Status,
          sections: Section[] 1..8 (обязательно), actions?: Action[] ≤ 6 }
Section { id: slug ≤ 40, title?: ≤ 60, fields: Field[] 1..20, actions?: Action[] ≤ 6 }
Field   { id: slug ≤ 40, label: ≤ 60, type, value, ref?: Ref, hint?: ≤ 160 }
  text    value: string ≤ 500
  money   value: { amount_minor: integer, currency: ISO 4217, три буквы }
  date    value: "YYYY-MM-DD" | RFC 3339
  status  value: Status
  link    value: { url: https-URL ≤ 2048, text?: ≤ 80 }
  count   value: integer ≥ 0
Status  { label: ≤ 40, tone: "ok" | "warn" | "bad" | "neutral" }
Ref     { kind: "payment" | "invoice" | "subscription" | "customer", id: ≤ 128, provider?: ≤ 32 }
Action  { type: "link", label: ≤ 40, url: https-URL ≤ 2048 }
      | { type: "copy", label: ≤ 40, value: ≤ 500 }
      | { type: "prompt", label: ≤ 40, prompt: ≤ 4000 }
```

- Во всей карточке не больше 40 полей и 12 действий. `slug` – `[a-z0-9_-]`, от 1 до 40 символов; длины считаются в символах Unicode. `amount_minor` – в минимальных единицах (центах). `ref` – машинная подсказка, панель её пока не показывает; неизвестный `kind` отбрасывает `ref`, а не поле.
- Наш сервер разбирает ответ, браузер сырого тела не получает. Неизвестный тип поля или действия, неизвестный `tone`: элемент отбрасывается. Нарушен лимит у элемента: элемент отбрасывается целиком, а не обрезается (необязательная строка сверх лимита, например `title`, `subtitle`, заголовок секции или `hint`, отбрасывается одна). Лишние секции, поля или действия отбрасываются с конца. Секция без единого валидного поля отбрасывается. Нет `schema_version` с мажором 1, нет ни одной валидной секции, больше 64 KiB или не JSON: вся карточка «недоступна». Ссылки только `https:`: поле `link` с другим адресом показывается текстом, действие `link` отбрасывается. Неизвестные ключи игнорируются.

**Действия.** `link` открывает `https:`-адрес в новой вкладке; `copy` копирует `value`; `prompt` показывает текст и копирует его для собственного ИИ-агента поддержки владельца. Панель ничего не исполняет и не отправляет. Никогда не подставляй в prompt текст сообщений посетителя (это prompt injection в агента, у которого могут быть права на деньги) и пиши в prompt, что любое действие с деньгами подтверждает человек.

**Что не класть в карточку.** Всё, что в карточке, уходит в браузер владельца. Держи минимум: план, статус подписки, суммы и даты платежей, счётчики продукта, ссылки в админку владельца. Никогда: данные карт (номера, CVC), пароли, токены, API-ключи, почтовые адреса, телефоны, данные других людей. E-mail и имя у владельца уже есть из обращения. Эндпоинт должен читать из собственной базы владельца; не давай ему ключ платёжного провайдера с полными правами.

**Наши лимиты.** Не больше 60 запросов карточки в минуту на проект и 8 одновременных на весь наш сервер (сверх – сразу «недоступна»); «Проверить» – 10 раз в час на проект.

**Чек-лист агента.**

1. Убедись, что люди проекта подтверждены (подписанный `identify()`) и тариф владельца включает CRM обратной связи.
2. Добавь `POST <путь>` на backend владельца: прочитай сырое тело, проверь подпись, разбери JSON, найди клиента по `person.user_id`, затем по `person.email`; нет такого – `404`; иначе собери карточку и ответь `200`. На запросы с `test: true` отвечай демонстрационной карточкой или настоящим поиском введённой личности.
3. Читай секрет из конфигурации сервера. Никогда не логируй тело запроса, e-mail, id и подписи.
4. Добавь юнит-тест на тест-вектор выше и тест на карточку, которая проходит схему.
5. Попроси владельца сохранить адрес, выпустить секрет, самому положить его в конфигурацию сервера и нажать «Проверить». Интеграция работает, только когда «Проверить» показывает «Карточка получена, полей: N». Не создавай тестовых людей в продуктивном проекте ради проверки.

## Баннер Ext Ops Panel и знак подтверждённого автора

**Что это.** У каждого Chrome-расширения и плагина ИИ-инструментов из публичного каталога extops.dev есть баннер, который любой может поставить на сайт или в README без аккаунта: картинка с extops.dev внутри ссылки на карточку продукта, с несколькими живыми публичными фактами каталога. На нём написано «Listed on Ext Ops Panel». Когда подтверждённый автор продукта включает знак, тот же адрес говорит «Verified author on Ext Ops Panel»; сниппет не меняется. Продукт, которого каталог не знает или чей листинг ушёл из магазина или с витрины, получает нейтральную плашку (только наш логотип, тот же размер и тема). Ни скрипта, ни cookie на странице, где он стоит. Все стили, форматы и темы с живыми примерами: https://extops.dev/ru/docs/author-banners. Что значит «подтверждённый»: https://extops.dev/ru/extensions/methodology#verified-authors.

**Просто баннер (любой продукт из каталога).** Переходи к шагу 3: собери адрес из адреса карточки продукта и форму сниппета ниже. Шаги 1 и 2 нужны только автору, который хочет знак «Verified author».

**1. Подтвердить авторство (нужен аккаунт, любой тариф, включая Free).**

- *Chrome-расширение.* Расширение есть в проекте аккаунта пользователя, и он отметил себя автором («Я автор»). В блоке проекта «Подтверждённый автор» он выбирает способ и получает значение:
  - «Домен сайта»: сайт, указанный в карточке магазина. Значение публикуется DNS-записью TXT на `_extops.<регистрируемый домен>` или строкой в `https://<хост>/.well-known/extops-challenge.txt`. Достаточно одного из двух. Перепроверка раз в 30 дней: запись оставить на месте; 3 неудачные проверки подряд заканчивают подтверждение.
  - «Подтверждённый домен»: домен, который аккаунт уже подтвердил для веб-сервиса, если он покрывает сайт листинга. Публиковать ничего не нужно.
  - «Строка в описании»: `extops-verify=<значение>` в любом месте описания листинга, для листинга без своего сайта или с сайтом на общей платформе (хостинг кода, магазин, соцсеть). Описание правится в кабинете разработчика Chrome Web Store; проверять после того, как магазин опубликует обновление. Разовое доказательство: после подтверждения строку можно убрать.
- *Плагин ИИ-инструментов.* Плагин добавлен в проект («Добавить плагин ИИ») и подтверждён одним из способов: `extops-verify.txt` со значением в корне его GitHub-репозитория (ветка по умолчанию), `/.well-known/extops-challenge.txt` на домене издателя или на хосте удалённого endpoint, DNS TXT на `_extops.<домен>` или домен, уже подтверждённый для веб-сервиса. Зеркала и общие хостинги ничего не доказывают.
- Значение действует 14 дней; в первые сутки мы ищем его каждый час, потом раз в 6 часов, а «Проверить сейчас» проверяет сразу. Подтверждённый автор у продукта один: более позднее валидное доказательство другого аккаунта забирает подтверждение. У расширения подтверждение заканчивается и при смене издателя листинга, а при доказательстве доменом ещё и при смене сайта.
- Помоги пользователю опубликовать значение: добавить TXT-запись в DNS, закоммитить файл в репозиторий или на сайт, или точно сказать, куда вставить строку. Значение не выдумывай: оно берётся из панели.

**2. Согласие.** Само подтверждение публично ничего не меняет. Переключатель «Публиковать меня как автора» в том же блоке (у плагина в блоке плагина проекта) включает для этого одного продукта знак на публичной карточке и на всех баннерах, а у плагина ещё доступность за 30 дней по его собственному мониторингу. По умолчанию выключен. Включает его только сам пользователь: не делай этого за него через API. Выключение или окончание подтверждения сразу возвращает баннеры к «Listed on Ext Ops Panel»; закэшированные копии обновятся в течение 6 часов.

**3. Выбрать и поставить баннер.** Подтверждённому автору раздел «Баннер» проекта показывает превью и собирает сниппеты с названием и адресом продукта; остальные собирают их по форме ниже. Адрес картинки задаётся путём без параметров:

```
https://extops.dev/extensions/<extension-id>/banner/<style>/<format>-<theme>.<svg|png>
https://extops.dev/plugins/<family>/<key>/banner/<style>/<format>-<theme>.<svg|png>
```

- `<style>`: `pulse-run` (по умолчанию), `amber-slab`, `terminal-pro`, `insignia`.
- `<format>`: `badge` (высота 28 px, ширина по тексту; ряд значков README, строка подвала), `pill` (250×54; шапка или подвал сайта, ряд «As seen on»), `card` (400×120; README под заголовком), `wide` (728×90; шапка или подвал во всю ширину, полоса README), `tile` (300×250; сайдбар).
- `<theme>`: `light`, `dark`, `brand` (амбер), `auto` (только SVG; светлая или тёмная по цветовой схеме читателя, хороша для README на GitHub).
- SVG везде, где принимают; PNG (рисуется в 2×) только там, где платформа берёт растровые картинки, и тогда `<picture>`: `-dark.png` для `prefers-color-scheme: dark`, иначе `-light.png`.
- `<family>/<key>`: собственный адрес карточки плагина (например, `mcp/dev.pdf2md/pdf-to-markdown`); `<extension-id>`: идентификатор в Chrome Web Store.

Форма сниппетов (настоящие бери из раздела «Баннер»):

```html
<a href="https://extops.dev/extensions/<extension-id>" title="<name> on Ext Ops Panel">
  <img src="https://extops.dev/extensions/<extension-id>/banner/pulse-run/pill-light.svg" alt="<name>: verified author on Ext Ops Panel" width="250" height="54">
</a>
```

```md
[![<name>: verified author on Ext Ops Panel](https://extops.dev/plugins/<family>/<key>/banner/pulse-run/badge-auto.svg)](https://extops.dev/plugins/<family>/<key>)
```

- В Markdown нет ширины, поэтому он для `badge` и `pill`; `card`, `wide` и `tile` вставляй в README HTML-сниппетом (GitHub его принимает).
- В `alt` пиши `<name>: listed on Ext Ops Panel`, а после включения знака автором `<name>: verified author on Ext Ops Panel`; ничего больше. `width` и `height` оставляй. Ссылка: канонический адрес карточки без параметров; опций только три: «открывать в новой вкладке» (`target="_blank" rel="noopener"`), `rel="nofollow"` и язык карточки (`/de/`, `/es/`, `/fr/`, `/pt/`, `/ru/` перед `/extensions` или `/plugins`).

**4. Проверить.** Открой адрес картинки: он показывает «Listed on Ext Ops Panel» или «Verified author», когда знак включён; нейтральная плашка значит, что продукта нет в каталоге или его листинг ушёл (это не ошибка, вёрстка страницы не ломается). Подтверждённому автору раздел «Баннер» говорит, почему баннер ещё не показывает знак. Баннер кэшируется на 6 часов (плашка на 1 час); факты обновляются сами, сниппет править не нужно.

**Что показывает баннер.** Только публичные факты карточки с датой прохода каталога и выше порогов (правило гордости): позиция только из публичного хаба (`/extensions/trending`, хаб категории, `/plugins/trending`); пользователи и только положительный прирост за 7 дней; рейтинг от 4.0 и от 3 оценок; «No site access»; значки магазина Featured / Established publisher текстом; версия, обновлённая не позже 90 дней назад; Manifest V3. У плагинов: доступность от 99.0% (собственный мониторинг автора, только с согласием подтверждённого автора); «Works with N AI clients»; звёзды GitHub или загрузки npm/PyPI за неделю от 10; витрины; версия. Неопубликованное число не выводится и никогда не показывается нулём. Текст на баннере только английский.

**Чего не делать.**

- Не обещай «Verified author» для продукта, автор которого его не подтвердил и не включил знак: баннер скажет «Listed on Ext Ops Panel». Не выдавай баннер «Listed» за одобрение автора продукта.
- Не добавляй UTM, `ref` и другие параметры к ссылке на карточку или к адресу картинки (карточка перенаправляет любой параметр на чистый адрес, картинка query игнорирует).
- Не скачивай картинку к себе, не перекрашивай её и не переписывай `alt`: копия теряет живые факты и сохраняет старую надпись, когда знак меняется.
- Не используй баннер «Listed» или бейдж листинга `/extensions/<id>/badge.svg` как знак подтверждения: их может вставить кто угодно.
- Не помещай контакты разработчиков, значение подтверждения и данные аккаунта в баннер, сниппет или текст README.
- Не выдавай знак за проверку безопасности или рейтинг: он говорит только, что автор доказал контроль и решил это показать.

Не смешивай first-party события hosted-страниц, публичные числа Chrome Web Store и кастомные метрики: у них разные источники и задержки обновления.
