TL;DR: каждый день разработчик открывает пять разных сайтов ради пяти мелких задач — отформатировать JSON, проверить JWT, декодировать base64, посчитать подсеть, сделать QR-код. Мне надоело, что эти сайты медленные, увешаны рекламой и отправляют мои рабочие данные на сервер. Я собрал всё в одном месте, заставил инструменты работать целиком в браузере и по ходу дела наступил на несколько граблей, о которых полезно знать — от потери точности больших чисел при форматировании JSON до того, как CSP может «сломать» счётчик посещений и репутацию сайта в браузере.

Боль

Рабочий день любого бэкендера, фронтендера или админа состоит из мелких операций. В логе пришёл JWT — надо глянуть, что в payload и не истёк ли он. Из тикета прислали base64-строку. Накинуть маску на сеть. Сгенерить QR для инструкции.

Под каждую операцию в интернете есть специализированный сайт. И почти каждый из них устроен одинаково плохо:

  • интерфейс из 2008 года, три баннера и попап «разрешить уведомления»;

  • кнопка «Decode» отправляет ваш запрос на сервер — а внутри JWT и base64 часто лежат токены стендов, имена, email, серийники;

  • русский интерфейс если и есть, то машинный;

  • на телефоне пользоваться невозможно.

Отдельно бесило, что всё это — не какие-то сложные вычисления. Декодировать base64 или посчитать network/broadcast для /24 — это микросекунды на моём же ноутбуке. Зачем для этого сервер? Зачем мне пять закладок, пять интерфейсов, пять политик конфиденциальности?

В какой-то момент я сформулировал требование: рабочие данные не должны покидать браузер. Если инструмент может посчитать результат локально — он обязан делать это локально. И собрал то, чем пользуюсь сам: qrge.ru.

Что внутри

Пять инструментов, каждый — одна страница, RU/EN, светлая и тёмная тема, нормально живёт на 360px:

  • JSON-редактор: текст и дерево вкладками, общая история undo/redo, поиск, форматирование и минификация. Режим JSONC — комментарии и висячие запятые допустимы (форматирование их сохраняет, минификация вычищает). Импорт файла, по URL, из CSV и XLSX, настройки — в IndexedDB.

  • JWT: декодер с подсветкой сегментов, криптографическая проверка подписи (HS/RS/PS/ES через WebCrypto), различимые статусы — «подпись верна, но токен истёк» и «подпись неверна» это разные ситуации. Энкодер для HS.

  • Base64: текст (UTF-8, Base64URL, построчный режим) и файлы в обе стороны, побайтовый round-trip.

  • IP-калькулятор: IPv4 и IPv6, CIDR/dotted/wildcard-маски, корректные /31 (RFC 3021) и /32, разбиение на подсети, IPv6-математика на BigInt без потери точности.

  • QR-генератор: стили модулей и «глаз», цвета, прозрачный фон, логотип, экспорт PNG/SVG.

Дальше — техническая часть. Если вы делаете что-то подобное, эти пять историй сэкономят вам вечер каждый.

1. JSON.parse → JSON.stringify незаметно портит данные

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

Оказалось, что JSON.parse в JS читает числа как double. Для любого целого больше 2^53 вы получаете не то, что было во входе: 12345678901234567890 превращается в 12345678901234567000, а 1e309 — вообще в null. Пользователь открыл JSON со снежинками из микросервисов, нажал «форматировать», получил «успех» — и молча испорченный файл. Бонусом JSON.stringify переставляет местами integer-подобные ключи и схлопывает дубликаты.

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

Тот же принцип я применил к импорту CSV/XLSX: ячейка 9007199254740993 остаётся строкой, потому что преобразование в число необратимо.

2. Приватность — это не обещание, а проверяемое свойство

«Данные обрабатываются локально» — этим фразой грешат все. Я хотел, чтобы это было свойством архитектуры, а не текста на странице:

  • сервер умеет отдавать только HTML, статику и healthcheck; API, принимающего пользовательские данные, физически нет;

  • все вычисления — TextEncoder, WebCrypto, BigInt, canvas — в вкладке;

  • секреты JWT вообще не касаются localStorage/sessionStorage, только state компонента.

Но обещание нужно ещё и протестировать. В e2e-набор добавлены «сентинел-тесты»: в каждый инструмент вводится строка с уникальной меткой, и тест перехватывает все сетевые запросы страницы — метка не имеет права встретиться ни в одном URL и ни в одном POST-теле. Разрешены только запросы к самому сайту. Так утечка через новую зависимость или «аналитику» ловится на уровне CI, а не на уровне репутации.

3. CSP с nonce — и три дня debugging'а сторонних виджетов

Политика по умолчанию строгая: script-src 'self' 'nonce-…' 'strict-dynamic', никаких unsafe-inline. Это отлично работает, пока вы не подключаете чужой код.

Счётчик посещений (Яндекс.Метрика) сломался трижды, каждый раз по-разному:

  1. скрипт загрузился, но измерительный iframe упёрся в frame-src 'self';

  2. пиксели ушли не только с mc.yandex.ru, но и с yandex.ru — а я разрешил только первый;

  3. после расширения политики мобильный Яндекс.Браузер продолжал помечать сайт «не защищено» при валидном сертификате. Классического mixed content не было — но в CSP оставалось моё же исключение connect-src http://localhost:* «для отладки», и индикатор безопасности браузера трактовал допустимость plain-http как «небезопасные данные».

Итог: connect-src 'self' https: без исключений, директива upgrade-insecure-requests, HSTS — и индикатор погас. Мораль двоякая: подключая сторонний виджет, выписывайте ему полный список суб-ресурсов (script/img/frame/connect) по консоли, а не по главному URL; и не оставляйте в политике «безобидных» http-допущений — они видны не только вам.

Отдельная история — Вебвизор. По умолчанию он маскирует значения полей ввода, но записывает текст страницы. А JWT-декодер, например, показывает вставленный токен и его расшифрованные claims прямо в DOM. Поэтому запись сессий я выключил: счётчик работает, статистика посещений собирается, а содержимое инструментов в записи не попадает.

4. Сетевой импорт, который не может уронить вкладку

«Загрузить JSON по URL» — единственная осознанная сетевая операция на сайте: GET только на адрес, который ввёл сам пользователь. Обратная сторона — это первый инструмент, через который чужой ответ может попасть в ваш браузер.

Правила, которые себя оправдали: схема только http/https; таймаут через AbortController; Content-Length проверяется до чтения тела; само тело читается стримом через getReader() с байтовым лимитом — chunked-ответ без content-length не займёт сотни мегабайт, при превышении лимита запрос отменяется, а не буферизуется; и наконец — закрытие диалога отменяет запрос, иначе поздний ответ молча подменял бы документ, который пользователь уже начал редактировать.

Это всё — вариации одного паттерна: «generation-токен» для асинхронных операций. Он же закрыл гонки в загрузке логотипа QR, файловом режиме base64 и проверке подписи JWT (изменили секрет, пока шла проверка, — результат по старому секрету не должен иметь права появиться).

5. Мелочи, из которых складывается доверие

  • Клавиатура и доступность: tablist-паттерны, aria-live только на статусах (не на всей колонке), focus-visible, prefers-reduced-motion. Иконки-кнопки с tooltip обязаны иметь и aria-label, и title.

  • Тёмная тема без мигания: тема читается из localStorage инлайн-скриптом до первой отрисовки.

  • CSS-модули против hidden: если у панели задан явный display: flex, атрибут hidden перестаёт работать — и «одна активная вкладка» превращается в две видимые. Правило: любой явный display на tabpanel сопровождается правилом [hidden] { display: none }.

  • QR и логотипы: SVG-логотип пользователя — это в первую очередь вектор атаки. Он проходит санитизацию (allowlist тегов/атрибутов) и растеризуется в canvas; в итоговый SVG попадает только валидированный PNG data-URL. Никакой чужой разметки в вашем файле.

Ограничения, о которых говорю честно

Минификация JSONC выдаёт строгий JSON — комментарии удаляются (это обратимая операция в истории). Правки через дерево в документе с комментариями переписывают текст без комментариев — интерфейс об этом предупреждает. Лимиты: 5 МиБ на JSON-текст, 20 МиБ на файл base64, 50 000 строк на XLSX-лист — всё обрабатывается локально, поэтому упираться приходится в память вкладки. Скорость сайта на общем VDS с соседними сервисами — тоже компромисс.

Вместо заключения

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

Если узнали в этом свою пачку закладок — пользуйтесь на здоровье. А если сами когда-нибудь захотите собрать «свой инструмент на одну страничку» — начните с пункта про токены и BigInt, он сэкономит вам самый неожиданный багфикс.