@uzum-tech/e-imzo
v1.0.0
Published
Типизированный клиент E-IMZO: подключение по WebSocket, список ключей, подпись PKCS#7
Maintainers
Readme
@uzum-tech/e-imzo
Типизированный клиент E-IMZO для браузера: список ключей (PFX и FTJC), загрузка ключа и подпись PKCS#7 — опционально открепленная и с меткой времени.
Пакет не зависит от фреймворка: ноль импортов Vue/React и ноль рантайм-зависимостей.
Установка
pnpm add @uzum-tech/e-imzoБыстрый старт
Клиент создаётся один раз на приложение и переиспользуется — так проверка версии и установка API-ключей не повторяются на каждый вызов:
import { EImzo } from '@uzum-tech/e-imzo';
export const eImzoClient = new EImzo({
wsUrl: 'wss://127.0.0.1:64443/service/cryptapi',
apiKeys: [
{
domain: 'localhost',
key: 'API-KEY'
}
]
});Подпись одним вызовом — клиент сам возьмёт список ключей, загрузит выбранный и создаст PKCS#7:
const result = await eImzoClient.sign(data);
result.pkcs7;
result.key.TIN;Конфигурация
| Поле | Обязательное | По умолчанию | Описание |
| --- | --- | --- | --- |
| wsUrl | да | — | Адрес локального сервиса E-IMZO |
| apiKeys | да | — | Пары «домен → ключ» |
| minVersion | нет | 336 | Минимальная версия E-IMZO (major * 100 + minor) |
| minVersionExtended | нет | 412 | Версия, начиная с которой ключи с FTJC-токенов не запрашиваются отдельно |
| timeoutMs | нет | 15000 | Таймаут одного запроса к E-IMZO |
API-ключ выдаётся под конкретный домен: без ключа для домена приложения работа
не начнётся — будет api-key-rejected.
timeoutMs не действует на шаги, где E-IMZO открывает нативное окно ввода пароля
или ПИН-кода: отсчитывать сетевой таймаут, пока пользователь печатает, бессмысленно.
Такой запрос ждёт столько, сколько нужно, но не зависает навсегда — при отмене ввода
или закрытии E-IMZO он завершится ошибкой.
Форматы apiKeys
Массив пар:
apiKeys: [
{
domain: 'localhost',
key: 'API-KEY-1'
}
]Плоский список — тот же формат, что у официального e-imzo-client.js, так что
готовый список ключей переносится копипастом:
apiKeys: [
'localhost', 'API-KEY-1',
'cabinet.example.uz', 'API-KEY-2'
]Строка домен=ключ с парами через запятую — чтобы весь список лежал в одной
переменной окружения:
VITE_E_IMZO_API_KEYS="localhost=API-KEY-1,cabinet.example.uz=API-KEY-2"apiKeys: import.meta.env.VITE_E_IMZO_API_KEYSПробелы вокруг домена, ключа и запятых игнорируются, хвостовая запятая допустима.
Пустой список, строка без =, плоский список нечётной длины, пустой домен или
пустой ключ — это invalid-config при первом обращении к E-IMZO.
Подпись
Подписать можно строку, Uint8Array или ArrayBuffer. Строка кодируется как UTF-8,
поэтому кириллица подписывается корректно. А вот файл — PDF, docx, картинку — через
строку прогонять нельзя: любой байт больше 0x7F превратится в UTF-8 пару, и подпись
ляжет на данные, которых в файле нет. Файлы передавайте байтами:
const bytes = new Uint8Array(await file.arrayBuffer());
await eImzoClient.sign(bytes);Если строка уже в base64, скажите об этом — она уйдёт в E-IMZO как есть:
await eImzoClient.sign(base64String, {
base64Encoded: true
});Для байтов флаг игнорируется: кодировать их всё равно нужно клиенту.
Открепленная подпись и метка времени:
await eImzoClient.sign(data, {
detached: true,
timestamper: (signatureHex) => requestTimestampToken(signatureHex)
});Выбор ключа
По умолчанию берётся первый доступный ключ. Чтобы выбрать свой — передайте
selectKey, например по ПИНФЛ владельца:
await eImzoClient.sign(data, {
selectKey: (keys) => keys.find((key) => key.PINFL === pinfl)
});selectKey может быть асинхронным — этого достаточно, чтобы показать пользователю
свой диалог выбора прямо внутри sign:
await eImzoClient.sign(data, {
selectKey: (keys) => showKeyPicker(keys)
});Если ключ выбран раньше — передайте его через key, тогда список запрашиваться
не будет. key имеет приоритет над selectKey:
const keys = await eImzoClient.listKeys();
const key = await showKeyPicker(keys);
await eImzoClient.sign(data, {
key
});Пачка документов
Пароль спрашивается на каждый sign. Чтобы подписать несколько документов одним
вводом пароля, загрузите ключ один раз и вызывайте createPkcs7 напрямую:
const keyId = await eImzoClient.loadKey(key);
for (const document of documents) {
await eImzoClient.createPkcs7(keyId, document);
}Ключи
listKeys() возвращает PFX-сертификаты и, на старых версиях E-IMZO, ключи
с FTJC-токенов. X.500-строка сертификата разбирается в поля CN, TIN, PINFL,
O, T, серийный номер и даты. Ключи, у которых нет ни СТИР, ни ПИНФЛ, в список
не попадают.
validFrom и validTo — обычные Date. Если даты в сертификате записаны не так,
как ожидает парсер, ключ всё равно остаётся в списке, но его Date будет
Invalid Date — скрывать такой ключ от пользователя библиотека не берётся. Прежде
чем показывать или сравнивать срок действия, проверьте разбор:
const expired = !Number.isNaN(key.validTo.getTime())
&& key.validTo < new Date();Методы
| Метод | Что делает |
| --- | --- |
| sign(data, options?) | Подпись целиком: список ключей, загрузка выбранного, PKCS#7 |
| listKeys() | Список доступных ключей |
| loadKey(key, options?) | Загружает ключ и возвращает его идентификатор; { verify: false } пропускает ввод пароля |
| createPkcs7(keyId, data, options?) | PKCS#7 для уже загруженного ключа |
| connect() | Проверяет версию и ставит API-ключи; вызывается автоматически, отдельно нужен, только чтобы проверить связь заранее |
| getVersion() | Диагностический пинг: жив ли сервис и какой версии. Клиент к работе не готовит — подписывать после него ещё нельзя |
| getConfig() | Конфиг с подставленными значениями по умолчанию |
| disconnect() | Забыть состояние соединения: следующий вызов начнёт с проверки версии и API-ключей |
| reload() | То же плюс перечитать конфиг |
Проверка версии и установка API-ключей выполняются один раз на инстанс клиента.
Если E-IMZO перезапустили и он забыл выданные ключи, клиент заметит это сам
и повторит подготовку на следующем вызове — disconnect() руками для этого не нужен.
Ошибки
Все отказы приходят как EImzoError с полями code и reason:
import { EImzoError, ErrorTypes } from '@uzum-tech/e-imzo';
try {
await eImzoClient.sign(data);
} catch (error) {
if (EImzoError.matches(error, ErrorTypes.Code.NOT_INSTALLED)) {
// E-IMZO не запущен
}
}EImzoError.matches(error) без второго аргумента отвечает только на вопрос «это отказ
E-IMZO?» — как error instanceof EImzoError, но без импорта типа в месте проверки.
| Код | Когда |
| --- | --- |
| invalid-config | apiKeys пуст или строка не разобралась в пары «домен → ключ» |
| not-installed | E-IMZO не запущен, недоступен или закрыл соединение раньше ответа |
| timeout | Сервис не ответил за timeoutMs |
| invalid-response | Ответ не разобрался как JSON, оказался не объектом или версия в нём не читается числом |
| unsupported-version | Версия не пришла или ниже minVersion |
| api-key-rejected | Домен не совпал с выданным API-ключом |
| key-not-found | Список ключей пуст или selectKey ничего не вернул |
| key-load-failed | Неверный пароль ключа, ПИН токена или пустой идентификатор ключа в ответе |
| sign-failed | E-IMZO не создал PKCS#7 |
| timestamp-failed | E-IMZO не вернул данные для метки времени или не прикрепил токен |
Окружение
Пакет собран как ESM и рассчитан на браузер: нужны WebSocket, TextEncoder
и btoa. На импорте модуль не трогает window, поэтому его можно импортировать
и в SSR-сборке — там он просто откажет с not-installed при первом запросе.
Типы отдаются вместе с пакетом: ConfigTypes, KeyTypes, SignTypes
и ErrorTypes.
