npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@itd-api/crypto

v0.1.0

Published

Скрытые сообщения в постах и профилях итд.com: плагин шифрования для itd-api

Downloads

333

Readme

@itd-api/crypto

Скрытые сообщения в постах, комментариях и профилях итд.com — плагин к itd-api.

Руководство · API из TSDoc

Текст прячется в невидимых Unicode-символах внутри обычного поста либо записывается видимым шифротекстом. Читатель видит только обложку, а тот, у кого подключён этот пакет, получает исходное сообщение отдельным полем.

Отдельным пакетом — потому что клиенту API незачем знать про шифры, а формат скрытых сообщений живёт своей жизнью: алгоритмы добавляются, не трогая itd-api.

Установка

npm i itd-api @itd-api/crypto

Поддерживается itd-api >=0.5.0 <1.0.0: crypto использует namespace RequestOptions.extensions и стабильный operationId запроса.

Использование

import { ItdClient } from 'itd-api';
import { crypt } from '@itd-api/crypto';

const itd = new ItdClient({ auth: process.env.ITD_TOKEN });
itd.use(crypt());

// отправка: текст прогоняется через шифр, обложка остаётся видимой
const created = await itd.posts.create(
  { content: 'секретный текст' },
  { extensions: { crypto: { encrypt: { cipher: 'invisible', cover: 'обычный пост' } } } },
);

// чтение: content не меняется, расшифровка приезжает рядом
const post = await itd.posts.get(created.id);
post.content;       // 'обычный пост' и невидимая нагрузка следом
post.secret?.text;  // 'секретный текст'

Без обложки видимого текста не будет вовсе — получится «пустой» пост, в котором на самом деле есть сообщение:

await itd.posts.create(
  { content: 'только для своих' },
  { extensions: { crypto: { encrypt: 'invisible' } } },
);

Где работает

Шифрование включается опцией extensions.crypto.encrypt у методов, принимающих текст:

Поддерживаемое действие определяется по стабильному operationId, а не по HTTP-пути. Поэтому низкоуровневому itd.request() с encrypt требуется корректный явный ID; запрос raw, даже на совпадающий URL, намеренно не маскируется под встроенный resource.

| Метод | Что шифруется | |---|---| | itd.posts.create | content | | itd.posts.update | content | | itd.posts.repost | content | | itd.posts.comment | content | | itd.comments.reply | content | | itd.comments.update | content | | itd.users.updateMe | displayName, bio | | itd.users.createProfile | displayName |

Расшифровка работает всюду и сама: ответ просматривается целиком, поэтому находки появляются и у постов ленты, и у исходного поста репоста, и у комментариев внутри поста, и у авторов — везде, где есть content, bio или displayName.

for await (const post of itd.posts.iterate({ tab: 'following' })) {
  if (post.secret) console.log(`@${post.author.username} спрятал: ${post.secret.text}`);
}

У профиля полей два, и шифруются они независимо — все находки лежат в secrets:

await itd.users.updateMe(
  { bio: 'скрытая подпись' },
  { extensions: { crypto: { encrypt: { fields: ['bio'], cover: 'то, что видят все' } } } },
);

const profile = await itd.users.get('username');
profile.secrets;  // [{ cipher: 'invisible', field: 'bio', text: 'скрытая подпись' }]

Шифры

| Имя | Как выглядит | Обложка | |---|---|---| | invisible | невидимые символы U+206AU+206F | да | | beecrypt | видимый текст из букв жъЖЪ | нет |

Подключены оба, шифрует по умолчанию первый — invisible. Расшифровка перебирает все: какой прочитал текст, тот и попадёт в secret.cipher.

await itd.posts.create(
  { content: 'секрет' },
  { extensions: { crypto: { encrypt: 'beecrypt' } } },
);
// content уходит как «ЖъЪжЖъЖЪжъ…» — сообщение не спрятано, а записано другими буквами

const post = await itd.posts.get(id);
post.secret;  // { cipher: 'beecrypt', field: 'content', text: 'секрет' }

У beecrypt шифротекст виден целиком, поэтому обложки у него нет: переданная cover не игнорируется, а отвергается ошибкой — молча потерять видимый текст хуже.

Настройки

import { crypt, invisible } from '@itd-api/crypto';

itd.use(
  crypt({
    ciphers: [invisible],  // по умолчанию — все встроенные, BUILT_IN_CIPHERS
    decrypt: true,         // искать ли скрытое в ответах; по умолчанию да
  }),
);

// расшифровку можно выключить или включить у отдельного вызова
await itd.posts.list({}, { extensions: { crypto: { decrypt: false } } });

Читать secret можно и без дополнений типов — помощниками secretOf и secretsOf:

import { secretOf } from '@itd-api/crypto';

const text = secretOf(post)?.text;

Алгоритм invisible

Алфавит — шесть невидимых символов U+206AU+206F, основание системы счисления 6. Каждый байт UTF-8 записывается четырьмя символами (6⁴ = 1296 ≥ 256); фиксированная ширина заменяет разделитель, которым мог быть только пробел, а пробелы сервер схлопывает. Нагрузка крепится к обложке без маркеров — извлечение сводится к фильтрации строки по алфавиту.

Алфавит именно такой, потому что сервер итд.com нормализует текст поста при сохранении:

| Символы | Что происходит | |---|---| | U+2000, U+2001, U+2002, U+200A, U+202F | заменяются пробелом, соседние схлопываются | | U+200B, U+200C | удаляются полностью | | U+200F, U+206AU+206F | проходят без изменений |

Из последней строки исключён U+200F (RLM): он выживает, но разворачивает направление текста и ломает вид поста.

Алгоритм доступен и напрямую, без клиента:

import { encodeInvisible, decodeInvisible, stripInvisible } from '@itd-api/crypto';

const content = `обычный текст${encodeInvisible('секрет')}`;
decodeInvisible(content);  // 'секрет'
stripInvisible(content);   // 'обычный текст'

Алгоритм beecrypt

Текст переводится в UTF-8, потом в base64, а каждая пара битов символов base64 заменяется буквой: 00ж, 01ъ, 10Ж, 11Ъ.

import { encodeBeeCrypt, decodeBeeCrypt } from '@itd-api/crypto';

encodeBeeCrypt('A');  // 'ъъжъъъжъжЪЪъжЪЪъ'

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

Длина растёт примерно в 5–6 раз от исходного текста — больше, чем у invisible для латиницы, но меньше для кириллицы.

Свой шифр

Контракт из двух методов — ни о запросах, ни о моделях шифр не знает:

import { crypt, type Cipher } from '@itd-api/crypto';

const base64: Cipher = {
  name: 'base64',
  encode: (text) => `[${btoa(unescape(encodeURIComponent(text)))}]`,
  decode: (text) => {
    const match = /^\[(.+)]$/.exec(text);
    return match ? decodeURIComponent(escape(atob(match[1]))) : null;
  },
};

itd.use(crypt({ ciphers: [base64] }));
await itd.posts.create(
  { content: 'секрет' },
  { extensions: { crypto: { encrypt: 'base64' } } },
);

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

Имена встроенных шифров собраны в CipherName — замороженном объекте, как перечисления в самом itd-api. Своя строка остаётся валидной, а Object.values(CipherName) даёт список известных:

import { CipherName } from '@itd-api/crypto';

await itd.posts.create(
  { content: 'секрет' },
  { extensions: { crypto: { encrypt: CipherName.Invisible } } },
);

Что нужно знать

  • Это обфускация, а не шифрование. Кто знает алфавит — прочитает сообщение. Для секретности комбинируйте с настоящим шифром: сначала зашифруйте текст сами, потом спрячьте результат.
  • Длина. Четыре невидимых символа на каждый байт UTF-8: ×4 к длине для латиницы, ×8 для кириллицы. Лимит длины поста на сервере считается по ним же.
  • Разметка. spans задаются по обложке, а не по секретному тексту: видимым остаётся только она, а нагрузка крепится в конец и смещений не сдвигает. Поэтому spans вместе с encrypt принимаются, лишь когда обложка задана и вмещает каждый фрагмент; в остальных случаях — CryptError, чтобы на сервер не уехали смещения, указывающие в пустоту.
  • Уведомления не расшифровываются. Библиотека пересобирает их в единую форму уже после плагина, и находка до вызывающего кода не доедет. Берите текст поста через itd.posts.get().
  • Поток realtime не покрыт: плагины работают в HTTP-транспорте, а поток событий идёт мимо него.

Лицензия

MIT