eimzo-ts 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # Using eimzo-ts with AI coding tools
2
+
3
+ Resources that help AI assistants (Claude Code, Cursor, Codex, Gemini CLI, ChatGPT and others) use this
4
+ library correctly. None of them is discovered automatically unless your tool is configured to read it.
5
+
6
+ | Resource | What it is | Where |
7
+ |---|---|---|
8
+ | `llms.txt` | Index of the documentation in the [llms.txt](https://llmstxt.org/) format | https://abduazimkhuja.github.io/eimzo-ts/llms.txt |
9
+ | `llms-full.txt` | README, backend guide and skill references in one text file | https://abduazimkhuja.github.io/eimzo-ts/llms-full.txt |
10
+ | Agent Skill | Task instructions in the [Agent Skills](https://agentskills.io/specification) format | [`skills/eimzo-ts/`](skills/eimzo-ts/SKILL.md), also inside the npm package |
11
+ | `AGENTS.md` | Rules for agents working on this repository or integrating the library | [`AGENTS.md`](AGENTS.md) |
12
+ | Type declarations | Full TypeScript API with JSDoc (comments in Russian) | `node_modules/eimzo-ts/dist/index.d.ts` |
13
+
14
+ ## Installing the Agent Skill
15
+
16
+ The skill is the folder `skills/eimzo-ts/` (a `SKILL.md` plus `references/`). Copy it into the skills
17
+ directory your tool reads. After `npm install eimzo-ts` it is available at
18
+ `node_modules/eimzo-ts/skills/eimzo-ts/`.
19
+
20
+ Claude Code (project-level):
21
+
22
+ ```bash
23
+ mkdir -p .claude/skills
24
+ cp -r node_modules/eimzo-ts/skills/eimzo-ts .claude/skills/
25
+ ```
26
+
27
+ Claude Code (user-level, all projects): copy to `~/.claude/skills/eimzo-ts/` instead.
28
+
29
+ Other tools that support Agent Skills: copy the same folder into the location described in that tool's
30
+ documentation. The folder name must stay `eimzo-ts` (it must match `name` in `SKILL.md`).
31
+
32
+ Without skill support, point the assistant to the file directly, e.g.
33
+ "Read node_modules/eimzo-ts/skills/eimzo-ts/SKILL.md and follow it."
34
+
35
+ ## Prompts that work well
36
+
37
+ - "Add login with E-IMZO to this app using eimzo-ts. Backend endpoints: …"
38
+ - "Replace e-imzo.js / e-imzo-client.js with eimzo-ts, keep the same UI."
39
+ - "Add a 'Sign' button that signs the order JSON with a timestamp and posts it to /api/orders/:id/sign."
40
+ - "eimzo-ts throws API_KEY_REJECTED on staging — what is wrong?"
41
+
42
+ Ask the assistant to also implement or review the backend: the library creates signatures, but only the
43
+ server (via e-imzo-server) can verify them.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.3]
9
+
10
+ ### Документация
11
+
12
+ - README на английском (русский — `README.ru.md`), сайт документации с примерами, FAQ и разбором ошибок.
13
+ - `llms.txt`, `llms-full.txt`, `AGENTS.md`, Agent Skill `skills/eimzo-ts/` (входит в npm-пакет), `AI_INTEGRATION.md`.
14
+
8
15
  ## [0.1.2]
9
16
 
10
17
  ### Исправлено
@@ -35,7 +42,8 @@
35
42
  - Сборки ESM, CJS и IIFE (`EimzoClient`) с типами.
36
43
  - Демо с режимом без E-IMZO и просмотром содержимого PKCS#7.
37
44
 
38
- [Unreleased]: https://github.com/abduazimkhuja/eimzo-ts/compare/v0.1.2...HEAD
45
+ [Unreleased]: https://github.com/abduazimkhuja/eimzo-ts/compare/v0.1.3...HEAD
46
+ [0.1.3]: https://github.com/abduazimkhuja/eimzo-ts/compare/v0.1.2...v0.1.3
39
47
  [0.1.2]: https://github.com/abduazimkhuja/eimzo-ts/compare/v0.1.1...v0.1.2
40
48
  [0.1.1]: https://github.com/abduazimkhuja/eimzo-ts/compare/v0.1.0...v0.1.1
41
49
  [0.1.0]: https://github.com/abduazimkhuja/eimzo-ts/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,93 +1,100 @@
1
- # eimzo-ts
1
+ # eimzo-ts — TypeScript client for E-IMZO
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/eimzo-ts)](https://www.npmjs.com/package/eimzo-ts)
4
4
  [![CI](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml)
5
5
  [![license](https://img.shields.io/npm/l/eimzo-ts)](LICENSE)
6
6
 
7
- TypeScript-клиент для [E-IMZO](https://e-imzo.uz): список ключей ЭЦП, подпись PKCS#7, вход по ЭЦП.
8
- Без зависимостей и без привязки к фреймворку: Angular, React, Vue, Svelte или обычная страница.
7
+ **English** · [Русский](README.ru.md)
9
8
 
10
- Заменяет `e-imzo.js` и `e-imzo-client.js` из официального примера. Вызовы E-IMZO те же, но
11
- на Promise, с типами, без глобальных переменных и без правок `String.prototype`/`Date.prototype`.
9
+ `eimzo-ts` lets a web page talk to [E-IMZO](https://e-imzo.uz), the desktop application used in
10
+ Uzbekistan for electronic digital signatures (ЭЦП / ERI). With it you can list the user's signing keys,
11
+ create PKCS#7 (CMS) signatures, and log users in with their digital signature.
12
12
 
13
- > **Неофициальная библиотека.** Не связана с разработчиками E-IMZO.
13
+ It replaces `e-imzo.js` and `e-imzo-client.js` from the official E-IMZO example with a typed,
14
+ Promise-based API: no global variables, no changes to `String.prototype` or `Date.prototype`,
15
+ zero runtime dependencies.
14
16
 
15
- **[Демо](https://abduazimkhuja.github.io/eimzo-ts/)** работает и без E-IMZO, на вымышленных ключах.
17
+ > **Unofficial.** This project is not affiliated with the developers of E-IMZO.
16
18
 
17
- Только для ПК: подпись выполняет приложение E-IMZO на компьютере пользователя. Мобильный
18
- вход (ID-CARD E-IMZO MOBILE) пока не поддерживается.
19
+ [Documentation](https://abduazimkhuja.github.io/eimzo-ts/) ·
20
+ [Live demo](https://abduazimkhuja.github.io/eimzo-ts/demo/) (works without E-IMZO, using mock keys) ·
21
+ [npm](https://www.npmjs.com/package/eimzo-ts) ·
22
+ [Changelog](CHANGELOG.md)
19
23
 
20
- ## Установка
24
+ ## Use cases
25
+
26
+ - **Login with a digital signature.** Get a one-time challenge from your backend, sign it with the user's key,
27
+ and send the signature to your backend for verification.
28
+ - **Sign a document or form** (attached or detached PKCS#7), optionally with a trusted timestamp.
29
+ - **Sign a file** picked with `<input type="file">`.
30
+ - **Show the user's keys:** owner name, TIN (INN), PINFL, organization, validity, and an `expired` flag.
31
+ - **Migrate from `e-imzo.js` / `e-imzo-client.js`** callbacks to `async`/`await` ([migration guide](docs/migration.md), in Russian).
32
+
33
+ ## How it works
34
+
35
+ ```
36
+ your page (eimzo-ts) ──WebSocket──► E-IMZO on the user's PC (asks for the key password)
37
+ │
38
+ └── fetch ──► your backend ──► e-imzo-server (verifies the signature)
39
+ ```
40
+
41
+ The library runs in the **browser**. It connects to E-IMZO at `ws://127.0.0.1:64646` (pages served over
42
+ http) or `wss://127.0.0.1:64443` (https). If the `EIMZOEXT` browser extension object is present on the
43
+ page, it is used instead. The password is typed into E-IMZO's own window; your page never sees it.
44
+
45
+ Signatures are **verified on the server** by e-imzo-server, not by this library.
46
+
47
+ ## Installation
21
48
 
22
49
  ```bash
23
50
  npm install eimzo-ts
24
51
  ```
25
52
 
26
- Без сборщика:
53
+ Without a bundler, use the IIFE build. It exposes a global `EimzoClient`:
27
54
 
28
55
  ```html
29
- <script src="https://unpkg.com/eimzo-ts@0.1.2/dist/index.global.js"></script>
56
+ <script src="https://unpkg.com/eimzo-ts@0.1/dist/index.global.js"></script>
30
57
  <script>
31
58
  const eimzo = EimzoClient.createEimzo({ apiKeys: EimzoClient.DEV_API_KEYS });
32
59
  </script>
33
60
  ```
34
61
 
35
- ## Быстрый старт
62
+ The package ships ESM, CommonJS, and IIFE builds with TypeScript declarations. It is side-effect free.
63
+
64
+ ## Quick start
36
65
 
37
66
  ```ts
38
67
  import { createEimzo, createFetchBackend, DEV_API_KEYS } from 'eimzo-ts';
39
68
 
40
69
  const eimzo = createEimzo({
41
- apiKeys: { ...DEV_API_KEYS, 'my-site.uz': 'API-KEY от НИЦ НТ' },
70
+ apiKeys: { ...DEV_API_KEYS, 'my-site.uz': 'API key for my-site.uz' },
42
71
  backend: createFetchBackend({
43
72
  challengeUrl: '/api/eimzo/challenge',
44
73
  loginUrl: '/api/eimzo/login',
45
74
  timestampUrl: '/api/eimzo/timestamp',
46
- init: { credentials: 'include' }, // заголовки, CSRF и т.п.
75
+ init: { credentials: 'include' }, // headers, CSRF token, etc.
47
76
  }),
48
77
  });
49
78
 
50
- const keys = await eimzo.listKeys(); // ключи пользователя
51
- const user = await eimzo.login(keys[0]); // вход: пароль спросит окно E-IMZO
52
- const pkcs7 = await eimzo.signDocument('документ'); // подпись + метка времени
79
+ const keys = await eimzo.listKeys(); // the user's keys
80
+ const user = await eimzo.login(keys[0]); // E-IMZO asks for the password
81
+ const pkcs7 = await eimzo.signDocument('document'); // signature + timestamp
53
82
  ```
54
83
 
55
- Создавайте **один** экземпляр на всё приложение: он хранит `keyId` загруженного ключа.
84
+ Create **one** instance per application. It keeps the `keyId` of the loaded key, and E-IMZO remembers
85
+ the password per `keyId`.
56
86
 
57
- ## API
87
+ ### Sign without a backend
58
88
 
59
- | Метод | Что делает |
60
- |---|---|
61
- | `init()` | Проверяет версию E-IMZO и устанавливает API-ключи. Вызывается сам перед любой операцией. |
62
- | `getVersion()` | Версия установленного E-IMZO. |
63
- | `listKeys()` | Ключи (`EimzoKey`): `CN`, `TIN`, `PINFL`, `O`, `T`, `validFrom`, `validTo`, `expired`. |
64
- | `listDevices()` | Подключены ли ID-карта, BAIK-токен, CKC: `{ idcard, baikey, ckc }`. |
65
- | `loadKey(key \| 'idcard' \| 'baikey' \| 'ckc')` | Загружает ключ, запоминает `keyId`. Предыдущий ключ выгружается. |
66
- | `sign(data, { detached, base64, keyId })` | PKCS#7 в base64. `data` — строка, `Uint8Array` или `ArrayBuffer`. |
67
- | `signFile(file, { detached, maxSize })` | Подпись `File`/`Blob`. По умолчанию detached, не больше 10 МБ. |
68
- | `login(key)` | Вход: challenge → `loadKey` → подпись → `backend.login`. |
69
- | `signDocument(data, options)` | `sign` → `backend.timestamp`. |
70
- | `unloadKey()` | Выгружает ключ из E-IMZO. Вызывайте при выходе пользователя. |
71
- | `call(plugin, name, args)` | Любая функция E-IMZO, см. `http://127.0.0.1:64646/apidoc.html`. |
72
- | `setLocale(locale)`, `t(error)` | Язык сообщений об ошибках. |
73
-
74
- ### Опции `createEimzo`
75
-
76
- | Опция | По умолчанию | |
77
- |---|---|---|
78
- | `apiKeys` | — | `{ домен: ключ }` или `[домен, ключ, ...]`. Для localhost — `DEV_API_KEYS`. |
79
- | `backend` | — | `createFetchBackend(...)` или свой объект, см. ниже. |
80
- | `minVersion` | `'4.86'` | Ниже — ошибка `OUTDATED_VERSION`. |
81
- | `locale` | `'ru'` | `'ru'`, `'uz-Latn'`, `'uz-Cyrl'`, `'kaa'`, `'en'` или свой код. |
82
- | `messages` | — | Свои тексты ошибок. |
83
- | `autoReloadKey` | `true` | Если E-IMZO забыл `keyId`, загрузить ключ заново и повторить подпись. |
84
- | `timeoutMs` | без ограничения | Таймаут вызова E-IMZO. |
85
- | `url`, `WebSocketImpl`, `transport` | | Для тестов и нестандартного окружения. |
86
-
87
- ## Ошибки
89
+ ```ts
90
+ const keys = await eimzo.listKeys();
91
+ await eimzo.loadKey(keys.find((k) => !k.expired)!);
92
+ const pkcs7 = await eimzo.sign('text to sign'); // attached PKCS#7, base64
93
+ const detached = await eimzo.sign(bytes, { detached: true }); // Uint8Array or ArrayBuffer
94
+ const filePkcs7 = await eimzo.signFile(input.files[0]); // detached by default, max 10 MB
95
+ ```
88
96
 
89
- Все ошибки — `EimzoError`. Для логики используйте `code`, для показа — `message`
90
- (уже на языке `locale`) или `eimzo.t(error)`.
97
+ ### Handle errors
91
98
 
92
99
  ```ts
93
100
  import { isEimzoError } from 'eimzo-ts';
@@ -95,127 +102,173 @@ import { isEimzoError } from 'eimzo-ts';
95
102
  try {
96
103
  await eimzo.login(key);
97
104
  } catch (e) {
98
- if (isEimzoError(e, 'CANCELLED')) return; // пользователь закрыл окно
99
- if (isEimzoError(e, 'NOT_INSTALLED')) showDownloadLink();
100
- else showError(eimzo.t(e));
105
+ if (isEimzoError(e, 'CANCELLED')) return; // the user closed the password window
106
+ if (isEimzoError(e, 'NOT_INSTALLED')) showInstallHint();
107
+ else showError(eimzo.t(e)); // localized message
101
108
  }
102
109
  ```
103
110
 
104
- | `code` | Когда |
105
- |---|---|
106
- | `NOT_INSTALLED` | E-IMZO не установлен или не запущен. |
107
- | `OUTDATED_VERSION` | Версия ниже `minVersion`; в `params` — `installed` и `min`. |
108
- | `API_KEY_REJECTED` | Нет API-KEY для домена страницы. |
109
- | `WRONG_PASSWORD` | Неверный пароль ключа. |
110
- | `CANCELLED` | Пользователь закрыл окно пароля. |
111
- | `KEY_NOT_FOUND` | E-IMZO не знает `keyId` (прошло 24 часа, перезапуск E-IMZO). |
112
- | `KEY_NOT_LOADED` | `sign()` до `loadKey()` — ошибка в коде. |
113
- | `FILE_TOO_LARGE` | Файл больше `maxSize`. |
114
- | `BACKEND_NOT_CONFIGURED` | Нет нужного метода в `backend` — ошибка в коде. |
115
- | `BACKEND_ERROR` | Backend ответил ошибкой; `httpStatus`, `params.detail`. |
116
- | `EIMZO_ERROR` | Прочие ошибки E-IMZO; исходный текст в `reason`, код в `status`. |
117
- | `TIMEOUT`, `WEBSOCKET_UNSUPPORTED` | Редкие случаи. |
118
-
119
- ### Языки
111
+ ## Features
120
112
 
121
- ```ts
122
- const eimzo = createEimzo({
123
- locale: 'uz-Latn',
124
- messages: {
125
- 'uz-Latn': { WRONG_PASSWORD: () => 'Parol xato, qayta urinib ko‘ring.' }, // заменить один текст
126
- tg: { NOT_INSTALLED: () => '...' }, // добавить язык
127
- },
128
- });
129
- eimzo.setLocale('en');
130
- ```
113
+ - Typed API for keys, signing, login, and timestamps; Promise-based.
114
+ - PFX keys, ID card, BAIK token, and CKC devices (`listDevices`, `loadKey('idcard' | 'baikey' | 'ckc')`).
115
+ - Typed `EimzoError` with a stable `code`. E-IMZO's raw `reason` and `status` are kept.
116
+ - Error messages in Russian, Uzbek (Latin and Cyrillic), Karakalpak, and English; you can override any text.
117
+ - Concurrent `sign()` calls are queued, so a double click does not open two password windows.
118
+ - If E-IMZO has forgotten the `keyId` (24 hours passed, or E-IMZO restarted), the key is loaded again and
119
+ the signature is retried (`autoReloadKey`).
120
+ - `unloadKey()` on logout removes the key and its remembered password from E-IMZO.
121
+ - `createFetchBackend()` matches e-imzo-server's response format, so your backend can proxy it as-is.
122
+ - Zero runtime dependencies; ESM, CJS, and IIFE builds; `.d.ts` included.
131
123
 
132
- Недостающие тексты берутся из русского. Без экземпляра: `formatEimzoError(error, 'en')`.
124
+ Behaviour for cancel, wrong password, signing, and unloading was checked against E-IMZO 6.3.5.
133
125
 
134
- ## Пароль
126
+ ## API
135
127
 
136
- - Пароль вводится в окне E-IMZO; сайт его не видит.
137
- - Без флажка «Запомнить на 6 час.» E-IMZO помнит пароль 1 минуту, с флажком — 6 часов.
138
- Запоминается для `keyId`, поэтому держите один экземпляр `Eimzo`.
139
- - После перезагрузки страницы `keyId` теряется — снова нужен `loadKey()`.
140
- - `unloadKey()` при выходе из аккаунта стирает запомненный пароль на этом компьютере.
141
- - Несколько вызовов `sign()` подряд (двойной клик) выполняются по очереди, а не открывают два окна.
128
+ | Method | Description |
129
+ |---|---|
130
+ | `createEimzo(options)` | Creates a client. See options below. |
131
+ | `init()` | Checks the E-IMZO version and installs API keys. Called automatically. |
132
+ | `getVersion()` | Installed E-IMZO version, `{ major, minor }`. |
133
+ | `listKeys()` | User keys as `EimzoKey[]`: `CN`, `TIN`, `PINFL`, `O`, `T`, `validFrom`, `validTo`, `expired`. Keys without TIN and PINFL are skipped. |
134
+ | `listDevices()` | `{ idcard, baikey, ckc }` — which hardware devices are connected. |
135
+ | `loadKey(key, { verifyPassword })` | Loads a key (or `'idcard' \| 'baikey' \| 'ckc'`) and stores its `keyId`. Unloads the previous key. |
136
+ | `sign(data, { detached, base64, keyId })` | Returns a base64 PKCS#7. `data`: `string`, `Uint8Array`, or `ArrayBuffer`. |
137
+ | `signFile(blob, { detached, maxSize, keyId })` | Signs a `File` or `Blob`. Detached by default, 10 MB limit. |
138
+ | `login(key)` | Challenge → `loadKey` → sign → `backend.login`. Returns what `backend.login` returns. |
139
+ | `signDocument(data, options)` | `sign` → `backend.timestamp`. |
140
+ | `unloadKey()` | Unloads the key from E-IMZO. Call it on logout. |
141
+ | `call(plugin, name, args)` | Calls any E-IMZO function (see `http://127.0.0.1:64646/apidoc.html`). |
142
+ | `keyId`, `locale`, `setLocale(locale)`, `t(error)` | Current key and language of error messages. |
142
143
 
143
- ## Backend
144
+ Other exports: `createFetchBackend`, `EimzoError`, `isEimzoError`, `formatEimzoError`, `parseX500`,
145
+ `parseCertificateInfo`, `createWebSocketTransport`, `createCapiwsTransport`, `createDefaultTransport`,
146
+ `defaultEimzoUrl`, `DEV_API_KEYS`, `DEFAULT_MAX_FILE_SIZE`, the locale objects (`ru`, `uzLatn`, `uzCyrl`,
147
+ `kaa`, `en`), and all types.
144
148
 
145
- Браузер не обращается к e-imzo-server напрямую. `createFetchBackend` ждёт ответы в формате
146
- e-imzo-server, поэтому backend может проксировать запросы:
149
+ ### Options
147
150
 
148
- | Запрос клиента | Backend | Ответ |
151
+ | Option | Default | Description |
152
+ |---|---|---|
153
+ | `apiKeys` | — | `{ domain: key }` or `[domain, key, ...]`. Use `DEV_API_KEYS` for `localhost` and `127.0.0.1`. If omitted, `apikey` is not called. |
154
+ | `backend` | — | `createFetchBackend(...)` or your own `{ getChallenge, login, timestamp }`. |
155
+ | `minVersion` | `'4.86'` | Older E-IMZO → `OUTDATED_VERSION`. |
156
+ | `locale` | `'ru'` | `'ru'`, `'uz-Latn'`, `'uz-Cyrl'`, `'kaa'`, `'en'`, or your own code with `messages`. |
157
+ | `messages` | — | Override texts: `{ en: { WRONG_PASSWORD: () => '...' } }`. Missing texts fall back to Russian. |
158
+ | `autoReloadKey` | `true` | Reload the key and retry when E-IMZO no longer knows the `keyId`. |
159
+ | `timeoutMs` | none | Timeout of one E-IMZO call. Off by default because signing waits for the user. |
160
+ | `url`, `WebSocketImpl`, `transport` | — | For tests and non-standard environments. |
161
+
162
+ ### Error codes
163
+
164
+ | `code` | Meaning |
165
+ |---|---|
166
+ | `NOT_INSTALLED` | E-IMZO is not installed or not running. |
167
+ | `OUTDATED_VERSION` | E-IMZO is older than `minVersion` (`params.installed`, `params.min`). |
168
+ | `API_KEY_REJECTED` | E-IMZO rejected the API key for this domain. |
169
+ | `WRONG_PASSWORD` | Wrong key password. |
170
+ | `CANCELLED` | The user closed the password window. Usually not worth showing. |
171
+ | `KEY_NOT_FOUND` | E-IMZO does not know this `keyId`. |
172
+ | `KEY_NOT_LOADED` | `sign()` was called before `loadKey()` — a bug in the calling code. |
173
+ | `FILE_TOO_LARGE` | File is larger than `maxSize` (`params.size`, `params.max`). |
174
+ | `BACKEND_NOT_CONFIGURED` | A required `backend` method is missing — a bug in the calling code. |
175
+ | `BACKEND_ERROR` | Your backend returned an error (`httpStatus`, `params.detail`). |
176
+ | `EIMZO_ERROR` | Any other E-IMZO error; see `reason` and `status`. |
177
+ | `TIMEOUT`, `WEBSOCKET_UNSUPPORTED` | Rare. |
178
+
179
+ ## Backend contract
180
+
181
+ The browser never calls e-imzo-server directly. `createFetchBackend` expects e-imzo-server's response
182
+ format, so your backend can proxy these calls:
183
+
184
+ | Client request | Your backend calls | Response |
149
185
  |---|---|---|
150
- | `GET challengeUrl` | `GET /frontend/challenge`, без кеша | `{ "status": 1, "challenge": "..." }` |
151
- | `POST loginUrl`, тело — PKCS#7 | `POST /backend/auth`, поиск пользователя по ПИНФЛ/ИНН, сессия | любой JSON — вернётся из `login()` |
152
- | `POST timestampUrl`, тело — PKCS#7 | `POST /frontend/timestamp/pkcs7` | `{ "status": 1, "pkcs7b64": "..." }` |
186
+ | `GET challengeUrl` | `GET /frontend/challenge` (do not cache) | `{ "status": 1, "challenge": "..." }` |
187
+ | `POST loginUrl`, body: PKCS#7 (`text/plain`) | `POST /backend/auth`, then finds the user by PINFL/TIN and opens a session | any JSON, returned from `login()` |
188
+ | `POST timestampUrl`, body: PKCS#7 (`text/plain`) | `POST /frontend/timestamp/pkcs7` | `{ "status": 1, "pkcs7b64": "..." }` |
153
189
 
154
- Подробно, с примером на Java (Spring Boot): [docs/backend.md](docs/backend.md).
190
+ A full guide with a Spring Boot example, status codes, and security checklist (in Russian):
191
+ [docs/backend.md](docs/backend.md).
155
192
 
156
- Другой формат — свой объект вместо `createFetchBackend`:
193
+ ## Compatibility
157
194
 
158
- ```ts
159
- createEimzo({
160
- backend: {
161
- getChallenge: () => api.get('/challenge').then((r) => r.data.value),
162
- login: (pkcs7) => api.post('/login', { pkcs7 }).then((r) => r.data),
163
- timestamp: (pkcs7) => api.post('/timestamp', { pkcs7 }).then((r) => r.data.pkcs7),
164
- },
165
- });
166
- ```
195
+ - **Browsers:** any modern browser with WebSocket. The build targets ES2020.
196
+ - **TypeScript:** declarations included for ESM and CJS.
197
+ - **Frameworks:** framework-agnostic: plain functions and a class, no framework dependency. It can be used
198
+ from Angular, React, Vue, Svelte, or a plain page. With SSR frameworks (Next.js, Nuxt, Angular SSR), call
199
+ it only on the client: E-IMZO runs on the user's computer, not on your server.
200
+ - **Node.js:** not the intended environment. It can talk to a local E-IMZO from a script if you pass a
201
+ `WebSocketImpl` that sends an `Origin` header (see `scripts/check-real-eimzo.mjs`).
202
+ - **E-IMZO:** 4.86 or newer by default (`minVersion`). Tested with E-IMZO 6.3.5.
167
203
 
168
- ## Фреймворки
204
+ ## When not to use it
169
205
 
170
- **Angular**
206
+ - **To verify signatures.** Verification needs e-imzo-server on your backend; this is a client library.
207
+ - **For mobile signing** (ID-CARD E-IMZO MOBILE, QR code / deep link). Not supported yet.
208
+ - **If you need a ready-made UI** (key picker modal). `eimzo-ts` has no UI components.
209
+ - **If you need another provider** (for example RS-IMZO).
171
210
 
172
- ```ts
173
- @Injectable({ providedIn: 'root' })
174
- export class EimzoService {
175
- readonly client = createEimzo({ apiKeys: environment.eimzoApiKeys, backend: createFetchBackend({ /* ... */ }) });
176
- }
211
+ ## Alternatives
177
212
 
178
- // компонент
179
- keys = signal<EimzoKey[]>([]);
180
- async ngOnInit() { this.keys.set(await this.eimzo.client.listKeys()); }
181
- async login(key: EimzoKey) { await this.eimzo.client.login(key); this.router.navigate(['/cabinet']); }
182
- ```
213
+ - The official `e-imzo.js` + `e-imzo-client.js` example: callback-based, uses globals.
214
+ - Other npm packages for E-IMZO exist. Some are framework-specific (Vue or React wrappers, UI modals),
215
+ some are framework-agnostic clients, and some are server-side clients for e-imzo-server. Search npm for
216
+ [`e-imzo`](https://www.npmjs.com/search?q=e-imzo) and compare their APIs and maintenance with your needs.
217
+ This project has not audited or benchmarked them.
183
218
 
184
- Для `HttpClient` с интерсепторами передайте свой `backend` через `firstValueFrom(this.http.get(...))`.
219
+ ## FAQ
185
220
 
186
- **React**
221
+ **Does it work over HTTPS?** Yes. On https pages it connects to `wss://127.0.0.1:64443`.
187
222
 
188
- ```tsx
189
- export const eimzo = createEimzo({ /* ... */ }); // модуль-синглтон
223
+ **Do I need an API key?** For `localhost` and `127.0.0.1`, use `DEV_API_KEYS` (public keys from the official
224
+ example). For another domain during development, enable "Developer mode" in the E-IMZO tray menu. For
225
+ production, request an API key for each domain and subdomain.
190
226
 
191
- function KeyPicker() {
192
- const [keys, setKeys] = useState<EimzoKey[]>([]);
193
- const [error, setError] = useState<string>();
194
- useEffect(() => { eimzo.listKeys().then(setKeys, (e) => setError(eimzo.t(e))); }, []);
195
- // ...
196
- }
197
- ```
227
+ **Is the password sent to my site?** No. E-IMZO asks for it in its own window. Without "Remember for 6 hours"
228
+ it is remembered for 1 minute per `keyId`.
198
229
 
199
- **Vue**
230
+ **How do I test my code without E-IMZO?** Pass your own `transport` — a function
231
+ `(request) => Promise<response>`:
200
232
 
201
233
  ```ts
202
- export const eimzo = createEimzo({ /* ... */ });
203
-
204
- const keys = ref<EimzoKey[]>([]);
205
- onMounted(async () => { keys.value = await eimzo.listKeys(); });
234
+ const eimzo = createEimzo({
235
+ transport: async (req) => {
236
+ if (req.name === 'version') return { success: true, major: '6', minor: '3' };
237
+ if (req.plugin === 'pfx' && req.name === 'list_all_certificates') return { success: true, certificates: [] };
238
+ return { success: false, reason: `not mocked: ${req.plugin}.${req.name}` };
239
+ },
240
+ });
206
241
  ```
207
242
 
208
- ## API-ключи
243
+ The demo's mock ([`demo/mock-eimzo.js`](demo/mock-eimzo.js)) is a fuller example.
244
+
245
+ **Can I read what is inside a PKCS#7?** The demo has a viewer ([`demo/pkcs7-viewer.js`](demo/pkcs7-viewer.js))
246
+ that shows the document, signer, and signing time. It does not verify the signature.
247
+
248
+ **Is `signingTime` trustworthy?** No. It comes from the user's computer clock. Use a timestamp
249
+ (`signDocument`) and verify it on the server.
209
250
 
210
- - `localhost` и `127.0.0.1` — `DEV_API_KEYS` (публичные ключи из официального примера).
211
- - Другой домен или IP при разработке — «Режим разработчика» в меню E-IMZO в трее.
212
- - Production — API-KEY выдаёт НИЦ НТ, отдельно на каждый домен и поддомен.
251
+ ## Troubleshooting
252
+
253
+ | Symptom | Cause and fix |
254
+ |---|---|
255
+ | `NOT_INSTALLED` | E-IMZO is not running. Start it (tray icon) or install it from [e-imzo.uz](https://e-imzo.uz/main/downloads/). |
256
+ | `API_KEY_REJECTED`, or E-IMZO functions fail on a new domain | No API key for this domain. Use Developer mode for testing; add the production key to `apiKeys`. |
257
+ | `OUTDATED_VERSION` | Update E-IMZO, or lower `minVersion` if you know your users' versions. |
258
+ | `WRONG_PASSWORD` with `reason` "Password is not ASCII" | The password was typed in another keyboard layout. |
259
+ | `listKeys()` returns fewer keys than E-IMZO shows | Keys without TIN and PINFL are skipped, as in the official client. |
260
+ | Password is asked on every signature | A new `Eimzo` instance or `loadKey()` per signature creates a new `keyId`. Keep one instance. |
261
+ | `KEY_NOT_FOUND` with `autoReloadKey: false` | E-IMZO restarted or 24 hours passed. Call `loadKey()` again. |
262
+ | `BACKEND_ERROR` | Check `httpStatus` and `params.detail`; for `/backend/auth` status codes see [docs/backend.md](docs/backend.md). |
213
263
 
214
- ## Переход с e-imzo.js
264
+ ## For AI coding assistants
215
265
 
216
- Соответствие функций `EIMZOClient` и этой библиотеки: [docs/migration.md](docs/migration.md).
266
+ - [`llms.txt`](https://abduazimkhuja.github.io/eimzo-ts/llms.txt) and
267
+ [`llms-full.txt`](https://abduazimkhuja.github.io/eimzo-ts/llms-full.txt) — documentation index and full text.
268
+ - [Agent Skill](skills/eimzo-ts/SKILL.md) for tools that support the Agent Skills format.
269
+ - See [AI_INTEGRATION.md](AI_INTEGRATION.md).
217
270
 
218
- ## Разработка
271
+ ## Development
219
272
 
220
273
  ```bash
221
274
  npm install
@@ -223,9 +276,9 @@ npm test # vitest
223
276
  npm run typecheck
224
277
  npm run build # dist/: ESM, CJS, IIFE + .d.ts
225
278
  npm run demo # http://localhost:3000/demo/
226
- npm run check:eimzo # проверка на установленном E-IMZO; --interactive N — с подписью ключом N
279
+ npm run check:eimzo # checks against a locally installed E-IMZO; --interactive N signs with key N
227
280
  ```
228
281
 
229
- ## Лицензия
282
+ ## License
230
283
 
231
284
  [MIT](LICENSE)
package/README.ru.md ADDED
@@ -0,0 +1,233 @@
1
+ # eimzo-ts
2
+
3
+ [![npm](https://img.shields.io/npm/v/eimzo-ts)](https://www.npmjs.com/package/eimzo-ts)
4
+ [![CI](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/eimzo-ts)](LICENSE)
6
+
7
+ [English](README.md) · **Русский**
8
+
9
+ TypeScript-клиент для [E-IMZO](https://e-imzo.uz): список ключей ЭЦП, подпись PKCS#7, вход по ЭЦП.
10
+ Без зависимостей и без привязки к фреймворку: Angular, React, Vue, Svelte или обычная страница.
11
+
12
+ Заменяет `e-imzo.js` и `e-imzo-client.js` из официального примера. Вызовы E-IMZO те же, но
13
+ на Promise, с типами, без глобальных переменных и без правок `String.prototype`/`Date.prototype`.
14
+
15
+ > **Неофициальная библиотека.** Не связана с разработчиками E-IMZO.
16
+
17
+ **[Документация](https://abduazimkhuja.github.io/eimzo-ts/ru/)** · **[Демо](https://abduazimkhuja.github.io/eimzo-ts/demo/)** работает и без E-IMZO, на вымышленных ключах.
18
+
19
+ Только для ПК: подпись выполняет приложение E-IMZO на компьютере пользователя. Мобильный
20
+ вход (ID-CARD E-IMZO MOBILE) пока не поддерживается.
21
+
22
+ ## Установка
23
+
24
+ ```bash
25
+ npm install eimzo-ts
26
+ ```
27
+
28
+ Без сборщика:
29
+
30
+ ```html
31
+ <script src="https://unpkg.com/eimzo-ts@0.1/dist/index.global.js"></script>
32
+ <script>
33
+ const eimzo = EimzoClient.createEimzo({ apiKeys: EimzoClient.DEV_API_KEYS });
34
+ </script>
35
+ ```
36
+
37
+ ## Быстрый старт
38
+
39
+ ```ts
40
+ import { createEimzo, createFetchBackend, DEV_API_KEYS } from 'eimzo-ts';
41
+
42
+ const eimzo = createEimzo({
43
+ apiKeys: { ...DEV_API_KEYS, 'my-site.uz': 'API-KEY от НИЦ НТ' },
44
+ backend: createFetchBackend({
45
+ challengeUrl: '/api/eimzo/challenge',
46
+ loginUrl: '/api/eimzo/login',
47
+ timestampUrl: '/api/eimzo/timestamp',
48
+ init: { credentials: 'include' }, // заголовки, CSRF и т.п.
49
+ }),
50
+ });
51
+
52
+ const keys = await eimzo.listKeys(); // ключи пользователя
53
+ const user = await eimzo.login(keys[0]); // вход: пароль спросит окно E-IMZO
54
+ const pkcs7 = await eimzo.signDocument('документ'); // подпись + метка времени
55
+ ```
56
+
57
+ Создавайте **один** экземпляр на всё приложение: он хранит `keyId` загруженного ключа.
58
+
59
+ ## API
60
+
61
+ | Метод | Что делает |
62
+ |---|---|
63
+ | `init()` | Проверяет версию E-IMZO и устанавливает API-ключи. Вызывается сам перед любой операцией. |
64
+ | `getVersion()` | Версия установленного E-IMZO. |
65
+ | `listKeys()` | Ключи (`EimzoKey`): `CN`, `TIN`, `PINFL`, `O`, `T`, `validFrom`, `validTo`, `expired`. |
66
+ | `listDevices()` | Подключены ли ID-карта, BAIK-токен, CKC: `{ idcard, baikey, ckc }`. |
67
+ | `loadKey(key \| 'idcard' \| 'baikey' \| 'ckc')` | Загружает ключ, запоминает `keyId`. Предыдущий ключ выгружается. |
68
+ | `sign(data, { detached, base64, keyId })` | PKCS#7 в base64. `data` — строка, `Uint8Array` или `ArrayBuffer`. |
69
+ | `signFile(file, { detached, maxSize })` | Подпись `File`/`Blob`. По умолчанию detached, не больше 10 МБ. |
70
+ | `login(key)` | Вход: challenge → `loadKey` → подпись → `backend.login`. |
71
+ | `signDocument(data, options)` | `sign` → `backend.timestamp`. |
72
+ | `unloadKey()` | Выгружает ключ из E-IMZO. Вызывайте при выходе пользователя. |
73
+ | `call(plugin, name, args)` | Любая функция E-IMZO, см. `http://127.0.0.1:64646/apidoc.html`. |
74
+ | `setLocale(locale)`, `t(error)` | Язык сообщений об ошибках. |
75
+
76
+ ### Опции `createEimzo`
77
+
78
+ | Опция | По умолчанию | |
79
+ |---|---|---|
80
+ | `apiKeys` | — | `{ домен: ключ }` или `[домен, ключ, ...]`. Для localhost — `DEV_API_KEYS`. |
81
+ | `backend` | — | `createFetchBackend(...)` или свой объект, см. ниже. |
82
+ | `minVersion` | `'4.86'` | Ниже — ошибка `OUTDATED_VERSION`. |
83
+ | `locale` | `'ru'` | `'ru'`, `'uz-Latn'`, `'uz-Cyrl'`, `'kaa'`, `'en'` или свой код. |
84
+ | `messages` | — | Свои тексты ошибок. |
85
+ | `autoReloadKey` | `true` | Если E-IMZO забыл `keyId`, загрузить ключ заново и повторить подпись. |
86
+ | `timeoutMs` | без ограничения | Таймаут вызова E-IMZO. |
87
+ | `url`, `WebSocketImpl`, `transport` | | Для тестов и нестандартного окружения. |
88
+
89
+ ## Ошибки
90
+
91
+ Все ошибки — `EimzoError`. Для логики используйте `code`, для показа — `message`
92
+ (уже на языке `locale`) или `eimzo.t(error)`.
93
+
94
+ ```ts
95
+ import { isEimzoError } from 'eimzo-ts';
96
+
97
+ try {
98
+ await eimzo.login(key);
99
+ } catch (e) {
100
+ if (isEimzoError(e, 'CANCELLED')) return; // пользователь закрыл окно
101
+ if (isEimzoError(e, 'NOT_INSTALLED')) showDownloadLink();
102
+ else showError(eimzo.t(e));
103
+ }
104
+ ```
105
+
106
+ | `code` | Когда |
107
+ |---|---|
108
+ | `NOT_INSTALLED` | E-IMZO не установлен или не запущен. |
109
+ | `OUTDATED_VERSION` | Версия ниже `minVersion`; в `params` — `installed` и `min`. |
110
+ | `API_KEY_REJECTED` | Нет API-KEY для домена страницы. |
111
+ | `WRONG_PASSWORD` | Неверный пароль ключа. |
112
+ | `CANCELLED` | Пользователь закрыл окно пароля. |
113
+ | `KEY_NOT_FOUND` | E-IMZO не знает `keyId` (прошло 24 часа, перезапуск E-IMZO). |
114
+ | `KEY_NOT_LOADED` | `sign()` до `loadKey()` — ошибка в коде. |
115
+ | `FILE_TOO_LARGE` | Файл больше `maxSize`. |
116
+ | `BACKEND_NOT_CONFIGURED` | Нет нужного метода в `backend` — ошибка в коде. |
117
+ | `BACKEND_ERROR` | Backend ответил ошибкой; `httpStatus`, `params.detail`. |
118
+ | `EIMZO_ERROR` | Прочие ошибки E-IMZO; исходный текст в `reason`, код в `status`. |
119
+ | `TIMEOUT`, `WEBSOCKET_UNSUPPORTED` | Редкие случаи. |
120
+
121
+ ### Языки
122
+
123
+ ```ts
124
+ const eimzo = createEimzo({
125
+ locale: 'uz-Latn',
126
+ messages: {
127
+ 'uz-Latn': { WRONG_PASSWORD: () => 'Parol xato, qayta urinib ko‘ring.' }, // заменить один текст
128
+ tg: { NOT_INSTALLED: () => '...' }, // добавить язык
129
+ },
130
+ });
131
+ eimzo.setLocale('en');
132
+ ```
133
+
134
+ Недостающие тексты берутся из русского. Без экземпляра: `formatEimzoError(error, 'en')`.
135
+
136
+ ## Пароль
137
+
138
+ - Пароль вводится в окне E-IMZO; сайт его не видит.
139
+ - Без флажка «Запомнить на 6 час.» E-IMZO помнит пароль 1 минуту, с флажком — 6 часов.
140
+ Запоминается для `keyId`, поэтому держите один экземпляр `Eimzo`.
141
+ - После перезагрузки страницы `keyId` теряется — снова нужен `loadKey()`.
142
+ - `unloadKey()` при выходе из аккаунта стирает запомненный пароль на этом компьютере.
143
+ - Несколько вызовов `sign()` подряд (двойной клик) выполняются по очереди, а не открывают два окна.
144
+
145
+ ## Backend
146
+
147
+ Браузер не обращается к e-imzo-server напрямую. `createFetchBackend` ждёт ответы в формате
148
+ e-imzo-server, поэтому backend может проксировать запросы:
149
+
150
+ | Запрос клиента | Backend | Ответ |
151
+ |---|---|---|
152
+ | `GET challengeUrl` | `GET /frontend/challenge`, без кеша | `{ "status": 1, "challenge": "..." }` |
153
+ | `POST loginUrl`, тело — PKCS#7 | `POST /backend/auth`, поиск пользователя по ПИНФЛ/ИНН, сессия | любой JSON — вернётся из `login()` |
154
+ | `POST timestampUrl`, тело — PKCS#7 | `POST /frontend/timestamp/pkcs7` | `{ "status": 1, "pkcs7b64": "..." }` |
155
+
156
+ Подробно, с примером на Java (Spring Boot): [docs/backend.md](docs/backend.md).
157
+
158
+ Другой формат — свой объект вместо `createFetchBackend`:
159
+
160
+ ```ts
161
+ createEimzo({
162
+ backend: {
163
+ getChallenge: () => api.get('/challenge').then((r) => r.data.value),
164
+ login: (pkcs7) => api.post('/login', { pkcs7 }).then((r) => r.data),
165
+ timestamp: (pkcs7) => api.post('/timestamp', { pkcs7 }).then((r) => r.data.pkcs7),
166
+ },
167
+ });
168
+ ```
169
+
170
+ ## Фреймворки
171
+
172
+ **Angular**
173
+
174
+ ```ts
175
+ @Injectable({ providedIn: 'root' })
176
+ export class EimzoService {
177
+ readonly client = createEimzo({ apiKeys: environment.eimzoApiKeys, backend: createFetchBackend({ /* ... */ }) });
178
+ }
179
+
180
+ // компонент
181
+ keys = signal<EimzoKey[]>([]);
182
+ async ngOnInit() { this.keys.set(await this.eimzo.client.listKeys()); }
183
+ async login(key: EimzoKey) { await this.eimzo.client.login(key); this.router.navigate(['/cabinet']); }
184
+ ```
185
+
186
+ Для `HttpClient` с интерсепторами передайте свой `backend` через `firstValueFrom(this.http.get(...))`.
187
+
188
+ **React**
189
+
190
+ ```tsx
191
+ export const eimzo = createEimzo({ /* ... */ }); // модуль-синглтон
192
+
193
+ function KeyPicker() {
194
+ const [keys, setKeys] = useState<EimzoKey[]>([]);
195
+ const [error, setError] = useState<string>();
196
+ useEffect(() => { eimzo.listKeys().then(setKeys, (e) => setError(eimzo.t(e))); }, []);
197
+ // ...
198
+ }
199
+ ```
200
+
201
+ **Vue**
202
+
203
+ ```ts
204
+ export const eimzo = createEimzo({ /* ... */ });
205
+
206
+ const keys = ref<EimzoKey[]>([]);
207
+ onMounted(async () => { keys.value = await eimzo.listKeys(); });
208
+ ```
209
+
210
+ ## API-ключи
211
+
212
+ - `localhost` и `127.0.0.1` — `DEV_API_KEYS` (публичные ключи из официального примера).
213
+ - Другой домен или IP при разработке — «Режим разработчика» в меню E-IMZO в трее.
214
+ - Production — API-KEY выдаёт НИЦ НТ, отдельно на каждый домен и поддомен.
215
+
216
+ ## Переход с e-imzo.js
217
+
218
+ Соответствие функций `EIMZOClient` и этой библиотеки: [docs/migration.md](docs/migration.md).
219
+
220
+ ## Разработка
221
+
222
+ ```bash
223
+ npm install
224
+ npm test # vitest
225
+ npm run typecheck
226
+ npm run build # dist/: ESM, CJS, IIFE + .d.ts
227
+ npm run demo # http://localhost:3000/demo/
228
+ npm run check:eimzo # проверка на установленном E-IMZO; --interactive N — с подписью ключом N
229
+ ```
230
+
231
+ ## Лицензия
232
+
233
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "eimzo-ts",
3
- "version": "0.1.2",
4
- "description": "Unofficial framework-agnostic TypeScript client for E-IMZO (Uzbekistan digital signature): key listing, PKCS#7 signing, login by EDS.",
3
+ "version": "0.1.3",
4
+ "description": "Unofficial TypeScript client for E-IMZO, the Uzbekistan digital signature app: list keys, create PKCS#7 signatures, log in by digital signature. Browser, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Abduazimkhoja",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/abduazimkhuja/eimzo-ts.git"
10
10
  },
11
- "homepage": "https://github.com/abduazimkhuja/eimzo-ts#readme",
11
+ "homepage": "https://abduazimkhuja.github.io/eimzo-ts/",
12
12
  "bugs": {
13
13
  "url": "https://github.com/abduazimkhuja/eimzo-ts/issues"
14
14
  },
@@ -31,24 +31,25 @@
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
- "CHANGELOG.md"
34
+ "skills",
35
+ "CHANGELOG.md",
36
+ "AI_INTEGRATION.md"
35
37
  ],
36
38
  "sideEffects": false,
37
39
  "keywords": [
38
40
  "e-imzo",
39
41
  "eimzo",
40
- "e-imzo.js",
41
42
  "eri",
42
43
  "ecp",
44
+ "eds",
43
45
  "digital-signature",
44
46
  "electronic-signature",
45
47
  "pkcs7",
46
48
  "cms",
47
49
  "uzbekistan",
48
50
  "typescript",
49
- "angular",
50
- "react",
51
- "vue"
51
+ "browser",
52
+ "websocket"
52
53
  ],
53
54
  "scripts": {
54
55
  "build": "tsup",
@@ -56,7 +57,8 @@
56
57
  "typecheck": "tsc --noEmit",
57
58
  "demo": "npm run build && npx --yes serve -l 3000 .",
58
59
  "check:eimzo": "npm run build && node scripts/check-real-eimzo.mjs",
59
- "prepublishOnly": "npm run typecheck && npm test && npm run build"
60
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
61
+ "site": "npm run build && node scripts/build-site.mjs"
60
62
  },
61
63
  "devDependencies": {
62
64
  "@types/node": "^22.20.5",
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: eimzo-ts
3
+ description: Integrates E-IMZO (Uzbekistan electronic digital signature, ЭЦП/ERI) into web apps with the eimzo-ts TypeScript library — listing the user's keys, creating PKCS#7 signatures, login by digital signature, timestamps, and error handling. Use when a browser app must sign data or log users in through the E-IMZO desktop application, when migrating from e-imzo.js / e-imzo-client.js, or when debugging E-IMZO errors such as NOT_INSTALLED, API_KEY_REJECTED or WRONG_PASSWORD.
4
+ license: MIT
5
+ metadata:
6
+ package: eimzo-ts
7
+ repository: https://github.com/abduazimkhuja/eimzo-ts
8
+ ---
9
+
10
+ # eimzo-ts: E-IMZO in web apps
11
+
12
+ ## What it solves
13
+
14
+ E-IMZO is a desktop application on the user's computer that holds digital signature keys and asks for the
15
+ password. A web page reaches it over WebSocket (`ws://127.0.0.1:64646` on http pages,
16
+ `wss://127.0.0.1:64443` on https). `eimzo-ts` wraps that protocol in a typed, Promise-based API with no
17
+ runtime dependencies.
18
+
19
+ ## First, check that it fits
20
+
21
+ Use it when **all** are true:
22
+
23
+ - The code runs in a **browser** on a desktop computer where the user has E-IMZO installed.
24
+ - The task is to list keys, sign data/files, or log in with a digital signature.
25
+
26
+ Do **not** use it (and tell the user why) when:
27
+
28
+ - Signatures must be **verified** — that needs e-imzo-server on the backend; this library only creates them.
29
+ - Signing must happen on a phone (ID-CARD E-IMZO MOBILE) — not supported.
30
+ - The provider is not E-IMZO.
31
+ - The project already uses another working E-IMZO wrapper — do not replace it without being asked.
32
+
33
+ ## Install and set up
34
+
35
+ ```bash
36
+ npm install eimzo-ts
37
+ ```
38
+
39
+ Create **one** shared instance (module singleton, Angular `providedIn: 'root'` service, React module export):
40
+
41
+ ```ts
42
+ import { createEimzo, createFetchBackend, DEV_API_KEYS } from 'eimzo-ts';
43
+
44
+ export const eimzo = createEimzo({
45
+ apiKeys: { ...DEV_API_KEYS }, // localhost/127.0.0.1; add { 'domain.uz': '<API key>' } for production
46
+ locale: 'en', // 'ru' (default) | 'uz-Latn' | 'uz-Cyrl' | 'kaa' | 'en'
47
+ backend: createFetchBackend({
48
+ challengeUrl: '/api/eimzo/challenge',
49
+ loginUrl: '/api/eimzo/login',
50
+ timestampUrl: '/api/eimzo/timestamp',
51
+ }),
52
+ });
53
+ ```
54
+
55
+ Look at the project before choosing URLs: reuse its HTTP client conventions, CSRF header and auth cookies
56
+ (`createFetchBackend({ init: { credentials: 'include', headers: {...} } })`), or pass a custom
57
+ `backend: { getChallenge, login, timestamp }` object that uses the project's own HTTP client.
58
+
59
+ With SSR frameworks call E-IMZO only on the client (`useEffect`, `onMounted`, `afterNextRender`,
60
+ `'use client'` components).
61
+
62
+ ## Choose the API
63
+
64
+ | Goal | Call | Needs backend |
65
+ |---|---|---|
66
+ | Show keys | `listKeys()` → `EimzoKey[]` (`CN`, `TIN`, `PINFL`, `O`, `T`, `validTo`, `expired`) | no |
67
+ | Hardware devices | `listDevices()` → `{ idcard, baikey, ckc }` | no |
68
+ | Login | `login(key)` | `getChallenge`, `login` |
69
+ | Sign text/bytes | `loadKey(key)` then `sign(data, { detached? })` | no |
70
+ | Sign a file | `loadKey(key)` then `signFile(file)` (detached, 10 MB limit by default) | no |
71
+ | Sign with timestamp | `loadKey(key)` then `signDocument(data)` | `timestamp` |
72
+ | Logout | `unloadKey()` | no |
73
+ | Anything else in E-IMZO | `call(plugin, name, args)` | no |
74
+
75
+ All signing methods return a base64 PKCS#7 string. Full reference: [references/api.md](references/api.md).
76
+ Ready-made snippets: [references/examples.md](references/examples.md).
77
+
78
+ ## Handle errors
79
+
80
+ Every error is an `EimzoError` with a `code`. Branch on `code`, display `eimzo.t(error)`:
81
+
82
+ ```ts
83
+ import { isEimzoError } from 'eimzo-ts';
84
+
85
+ try {
86
+ await eimzo.login(key);
87
+ } catch (e) {
88
+ if (isEimzoError(e, 'CANCELLED')) return;
89
+ if (isEimzoError(e, 'NOT_INSTALLED')) return showInstallHint();
90
+ showError(eimzo.t(e));
91
+ }
92
+ ```
93
+
94
+ Diagnosis of each code and of the backend statuses: [references/troubleshooting.md](references/troubleshooting.md).
95
+
96
+ ## Backend
97
+
98
+ The browser never calls e-imzo-server directly. If the project has no backend endpoints yet, implement
99
+ them as described in [references/backend.md](references/backend.md) — signature verification **must** be
100
+ done on the server.
101
+
102
+ ## Verify the integration
103
+
104
+ 1. `npm run build` / the project's typecheck passes; imports come from `'eimzo-ts'` only.
105
+ 2. Only one `createEimzo()` call exists in the app.
106
+ 3. Code that runs during SSR does not call E-IMZO.
107
+ 4. `CANCELLED` is not shown as an error; `NOT_INSTALLED` shows how to install or start E-IMZO.
108
+ 5. Logout calls `unloadKey()`.
109
+ 6. Backend: `/frontend/challenge` is not cached; `/backend/auth` and `/backend/pkcs7/verify/*` are called from
110
+ the server with `X-Real-IP` and `Host`; the signer's PINFL/TIN is matched to the user.
111
+ 7. Manual test on a computer with E-IMZO: list keys → login → cancel the password window → wrong password →
112
+ correct password. Without E-IMZO, the hosted demo at https://abduazimkhuja.github.io/eimzo-ts/demo/
113
+ shows the flow with mock keys.
114
+
115
+ For unit tests, pass `transport: async (request) => response` to `createEimzo` instead of mocking WebSocket.
@@ -0,0 +1,74 @@
1
+ # eimzo-ts API reference
2
+
3
+ ```ts
4
+ import {
5
+ createEimzo, Eimzo, createFetchBackend, DEV_API_KEYS, DEFAULT_MAX_FILE_SIZE,
6
+ EimzoError, isEimzoError, formatEimzoError,
7
+ parseX500, parseCertificateInfo,
8
+ createWebSocketTransport, createCapiwsTransport, createDefaultTransport, defaultEimzoUrl,
9
+ ru, uzLatn, uzCyrl, kaa, en, builtinMessages, DEFAULT_LOCALE,
10
+ } from 'eimzo-ts';
11
+ import type { EimzoKey, PfxKey, TokenKey, EimzoOptions, EimzoBackend, EimzoErrorCode, Transport } from 'eimzo-ts';
12
+ ```
13
+
14
+ Without a bundler: `<script src="https://unpkg.com/eimzo-ts@0.1/dist/index.global.js">` → global `EimzoClient`.
15
+
16
+ ## createEimzo(options?): Eimzo
17
+
18
+ | Option | Type | Default | Notes |
19
+ |---|---|---|---|
20
+ | `apiKeys` | `Record<string, string> \| string[]` | — | Domain → API key. `DEV_API_KEYS` covers `localhost`, `127.0.0.1`. Omitted → `apikey` is not called. |
21
+ | `backend` | `EimzoBackend<TLogin>` | — | `{ getChallenge?(): Promise<string>; login?(pkcs7): Promise<TLogin>; timestamp?(pkcs7): Promise<string> }` |
22
+ | `minVersion` | `string` | `'4.86'` | `'major.minor'` |
23
+ | `locale` | `string` | `'ru'` | `'ru' \| 'uz-Latn' \| 'uz-Cyrl' \| 'kaa' \| 'en'` or custom with `messages` |
24
+ | `messages` | `Record<locale, Partial<Record<code, (params) => string>>>` | — | Missing texts fall back to Russian. |
25
+ | `autoReloadKey` | `boolean` | `true` | On `KEY_NOT_FOUND`, reload the same key and retry `sign` once. Not applied when `keyId` is passed explicitly. |
26
+ | `timeoutMs` | `number` | none | Per call. Signing waits for the user, so keep it unset or large. |
27
+ | `url` | `string` | `defaultEimzoUrl()` | `wss://127.0.0.1:64443/service/cryptapi` on https, `ws://127.0.0.1:64646/service/cryptapi` otherwise |
28
+ | `WebSocketImpl` | `typeof WebSocket` | global | For non-browser environments. |
29
+ | `transport` | `(req: { plugin?, name, arguments? }) => Promise<{ success: boolean, reason?, ... }>` | WebSocket or `EIMZOEXT` | Use for tests. |
30
+
31
+ ## Methods
32
+
33
+ All return Promises and reject with `EimzoError`.
34
+
35
+ - `init(): Promise<{ major, minor }>` — version check + API keys. Cached; retried after a failure. Called by every method.
36
+ - `getVersion(): Promise<{ major, minor }>`
37
+ - `listKeys(): Promise<EimzoKey[]>` — PFX keys (plus `ftjc` tokens on E-IMZO older than 4.12). Keys without TIN and PINFL are skipped.
38
+ `EimzoKey` fields: `type: 'pfx' | 'ftjc'`, `CN`, `TIN` (INN, or UID when no INN), `PINFL`, `UID`, `O`, `T`,
39
+ `serialNumber`, `validFrom: Date | null`, `validTo: Date | null`, `expired: boolean`;
40
+ PFX also has `disk`, `path`, `name`, `alias`.
41
+ - `listDevices(): Promise<{ idcard: boolean, baikey: boolean, ckc: boolean }>`
42
+ - `loadKey(key: EimzoKey | 'idcard' | 'baikey' | 'ckc', { verifyPassword? }): Promise<string>` — returns and stores `keyId`; unloads the previously loaded PFX key. `verifyPassword` asks for the password immediately (no "remember for 6 hours" checkbox in that window).
43
+ - `sign(data: string | Uint8Array | ArrayBuffer, { detached?, base64?, keyId? }): Promise<string>` — strings are UTF-8 encoded unless `base64: true`. Attached by default.
44
+ - `signFile(file: Blob, { detached = true, maxSize = 10 MB, keyId? }): Promise<string>` — `FILE_TOO_LARGE` before reading the file.
45
+ - `login(key): Promise<TLogin>` — `backend.getChallenge()` → `loadKey(key)` → `sign(challenge)` → `backend.login(pkcs7)`.
46
+ - `signDocument(data, signOptions): Promise<string>` — `sign` → `backend.timestamp(pkcs7)`. Needs a loaded key.
47
+ - `unloadKey(): Promise<void>` — `pfx.unload_key`; already-unloaded is not an error. Call on logout.
48
+ - `call(plugin, name, args?): Promise<EimzoResponse>` — raw E-IMZO call; `success: false` → `EimzoError`.
49
+ - `keyId: string | null`, `locale`, `setLocale(locale)`, `t(error, locale?)`.
50
+
51
+ `loadKey`, `unloadKey`, `sign`, `signFile`, `login`, `signDocument` run one at a time (queued).
52
+
53
+ ## createFetchBackend(options)
54
+
55
+ | Option | Method | Request | Expected response |
56
+ |---|---|---|---|
57
+ | `challengeUrl` | `getChallenge` | `GET`, `cache: 'no-store'` | `{ "status": 1, "challenge": "..." }` |
58
+ | `loginUrl` | `login` | `POST`, body = PKCS#7, `Content-Type: text/plain` | any JSON or text, returned as is |
59
+ | `timestampUrl` | `timestamp` | `POST`, body = PKCS#7, `text/plain` | `{ "status": 1, "pkcs7b64": "..." }` |
60
+ | `init` | all | merged into every `fetch` (headers, `credentials`) | |
61
+ | `fetch` | all | custom fetch implementation | |
62
+
63
+ Non-2xx or a JSON body with `status !== 1` → `BACKEND_ERROR` (`httpStatus`, `params.detail` from `message`).
64
+
65
+ ## EimzoError
66
+
67
+ `code`, `params`, `message` (localized), `reason` (raw E-IMZO text), `status` (E-IMZO numeric status),
68
+ `wsCode` (WebSocket close code), `httpStatus` (backend). Codes: `NOT_INSTALLED`, `WEBSOCKET_UNSUPPORTED`,
69
+ `TIMEOUT`, `OUTDATED_VERSION {installed, min}`, `API_KEY_REJECTED`, `WRONG_PASSWORD`, `CANCELLED`,
70
+ `KEY_NOT_FOUND`, `FILE_TOO_LARGE {size, max}`, `KEY_NOT_LOADED`, `BACKEND_NOT_CONFIGURED {methods}`,
71
+ `BACKEND_ERROR {status?, detail?}`, `EIMZO_ERROR {reason?}`.
72
+
73
+ E-IMZO 6.3.5 responses mapped: status `-5000` → `CANCELLED`; `-1010`, `-1014` → `KEY_NOT_FOUND`;
74
+ reason containing `BadPaddingException`, `Sequence tag error`, `getSecretKey failed` → `WRONG_PASSWORD`.
@@ -0,0 +1,45 @@
1
+ # Backend for eimzo-ts
2
+
3
+ The browser signs; the backend verifies through **e-imzo-server** (a Java service distributed to
4
+ integrators, needs a VPN key). Never call `/backend/*` methods of e-imzo-server from the browser.
5
+
6
+ ## Endpoints expected by `createFetchBackend`
7
+
8
+ | App endpoint (any path) | Calls e-imzo-server | Returns to the browser |
9
+ |---|---|---|
10
+ | `GET /api/eimzo/challenge` | `GET /frontend/challenge` | its JSON as is: `{ "status": 1, "challenge": "...", "ttl": 120 }`. Send `Cache-Control: no-store`. |
11
+ | `POST /api/eimzo/login` (body: base64 PKCS#7, `text/plain`) | `POST /backend/auth` | your user/session JSON |
12
+ | `POST /api/eimzo/timestamp` (body: base64 PKCS#7) | `POST /frontend/timestamp/pkcs7` | its JSON as is: `{ "status": 1, "pkcs7b64": "..." }` |
13
+
14
+ On failure return HTTP 4xx/5xx or `{ "status": <not 1>, "message": "..." }`; `message` becomes the
15
+ `BACKEND_ERROR` text, so keep it user-safe.
16
+
17
+ ## Required headers to e-imzo-server
18
+
19
+ - `X-Real-IP`: the user's IP, taken from your trusted reverse proxy, not from a client-supplied header.
20
+ - `Host`: your site's domain (configuration value, not copied from the request).
21
+
22
+ Java pitfall: `java.net.http.HttpClient` and `HttpURLConnection` refuse to set `Host`. Use Apache HttpClient 5,
23
+ or start the JVM with `-Djdk.httpclient.allowRestrictedHeaders=host` (HttpClient) /
24
+ `-Dsun.net.http.allowRestrictedHeaders=true` (HttpURLConnection).
25
+
26
+ ## Login handler
27
+
28
+ 1. Forward the PKCS#7 to `/backend/auth`.
29
+ 2. `status == -1` → 503 (infrastructure problem, often VPN). Other `status != 1` → 401.
30
+ 3. Read `subjectCertificateInfo.subjectName`: PINFL `1.2.860.3.16.1.2`, TIN `1.2.860.3.16.1.1` (or `UID`).
31
+ 4. Find the user; if none, 403.
32
+ 5. Rotate the session ID (or issue a token) and return the user.
33
+
34
+ ## Document signing
35
+
36
+ 1. Verify with `/backend/pkcs7/verify/attached` (or `/detached` with body `documentBase64|pkcs7`).
37
+ 2. For every `pkcs7Info.signers[i]` require `verified`, `certificateVerified`,
38
+ `certificateValidAtSigningTime` = true and no `revokedStatusInfo`.
39
+ 3. Compare `pkcs7Info.documentBase64` with the document you expected to be signed.
40
+ 4. Match the signer's PINFL/TIN with the session user.
41
+ 5. Require a timestamp: `signingTime` is the user's PC clock.
42
+ 6. Reject replays: include a unique ID in the document and store it once.
43
+ 7. Store the PKCS#7 and the verification result as personal data (they contain PINFL).
44
+
45
+ Test keys verify only on a test e-imzo-server (test VPN), production keys only on production.
@@ -0,0 +1,150 @@
1
+ # eimzo-ts examples
2
+
3
+ All snippets assume one shared instance:
4
+
5
+ ```ts
6
+ // eimzo.ts
7
+ import { createEimzo, createFetchBackend, DEV_API_KEYS } from 'eimzo-ts';
8
+
9
+ export const eimzo = createEimzo({
10
+ apiKeys: { ...DEV_API_KEYS },
11
+ backend: createFetchBackend({
12
+ challengeUrl: '/api/eimzo/challenge',
13
+ loginUrl: '/api/eimzo/login',
14
+ timestampUrl: '/api/eimzo/timestamp',
15
+ }),
16
+ });
17
+ ```
18
+
19
+ ## Key picker data
20
+
21
+ ```ts
22
+ const keys = await eimzo.listKeys();
23
+ const options = keys.map((k, i) => ({
24
+ value: i,
25
+ label: `${k.CN} — ${k.TIN || k.PINFL}${k.O ? `, ${k.O}` : ''}`,
26
+ disabled: k.expired,
27
+ }));
28
+ ```
29
+
30
+ ## Login
31
+
32
+ ```ts
33
+ import { isEimzoError, type EimzoKey } from 'eimzo-ts';
34
+
35
+ async function loginWith(key: EimzoKey) {
36
+ try {
37
+ const user = await eimzo.login(key); // whatever your /api/eimzo/login returns
38
+ return user;
39
+ } catch (e) {
40
+ if (isEimzoError(e, 'CANCELLED')) return null;
41
+ throw new Error(eimzo.t(e));
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Sign a document with a timestamp and send it
47
+
48
+ ```ts
49
+ await eimzo.loadKey(key); // once; later signatures reuse the keyId
50
+ const pkcs7 = await eimzo.signDocument(JSON.stringify({ id: docId, nonce: crypto.randomUUID(), body }));
51
+ await fetch(`/api/documents/${docId}/sign`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: pkcs7 });
52
+ ```
53
+
54
+ Put a unique ID into the signed content so the backend can reject a replayed signature.
55
+
56
+ ## Sign a file (detached)
57
+
58
+ ```ts
59
+ const file = input.files![0];
60
+ const pkcs7 = await eimzo.signFile(file, { maxSize: 20 * 1024 * 1024 });
61
+ // send both the file and pkcs7; the backend verifies with /backend/pkcs7/verify/detached
62
+ ```
63
+
64
+ ## Logout
65
+
66
+ ```ts
67
+ await eimzo.unloadKey();
68
+ ```
69
+
70
+ ## Angular service
71
+
72
+ ```ts
73
+ import { Injectable } from '@angular/core';
74
+ import { createEimzo, createFetchBackend, DEV_API_KEYS } from 'eimzo-ts';
75
+
76
+ @Injectable({ providedIn: 'root' })
77
+ export class EimzoService {
78
+ readonly client = createEimzo({
79
+ apiKeys: { ...DEV_API_KEYS },
80
+ backend: createFetchBackend({ challengeUrl: '/api/eimzo/challenge', loginUrl: '/api/eimzo/login' }),
81
+ });
82
+ }
83
+ ```
84
+
85
+ To reuse `HttpClient` interceptors, pass a custom backend:
86
+
87
+ ```ts
88
+ backend: {
89
+ getChallenge: () => firstValueFrom(this.http.get<{ challenge: string }>('/api/eimzo/challenge')).then((r) => r.challenge),
90
+ login: (pkcs7) => firstValueFrom(this.http.post('/api/eimzo/login', pkcs7, { headers: { 'Content-Type': 'text/plain' } })),
91
+ }
92
+ ```
93
+
94
+ ## React
95
+
96
+ ```tsx
97
+ import { useEffect, useState } from 'react';
98
+ import type { EimzoKey } from 'eimzo-ts';
99
+ import { eimzo } from './eimzo';
100
+
101
+ export function KeyPicker({ onPick }: { onPick: (k: EimzoKey) => void }) {
102
+ const [keys, setKeys] = useState<EimzoKey[]>([]);
103
+ const [error, setError] = useState<string>();
104
+ useEffect(() => { eimzo.listKeys().then(setKeys, (e) => setError(eimzo.t(e))); }, []);
105
+ if (error) return <p role="alert">{error}</p>;
106
+ return (
107
+ <ul>
108
+ {keys.map((k) => (
109
+ <li key={k.serialNumber}>
110
+ <button disabled={k.expired} onClick={() => onPick(k)}>{k.CN} — {k.TIN || k.PINFL}</button>
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ );
115
+ }
116
+ ```
117
+
118
+ In Next.js, put this in a `'use client'` component.
119
+
120
+ ## Vue
121
+
122
+ ```ts
123
+ import { onMounted, ref } from 'vue';
124
+ import type { EimzoKey } from 'eimzo-ts';
125
+ import { eimzo } from './eimzo';
126
+
127
+ const keys = ref<EimzoKey[]>([]);
128
+ onMounted(async () => { keys.value = await eimzo.listKeys(); });
129
+ ```
130
+
131
+ ## Unit test with a fake transport
132
+
133
+ ```ts
134
+ import { createEimzo } from 'eimzo-ts';
135
+
136
+ const eimzo = createEimzo({
137
+ transport: async (req) => {
138
+ const name = req.plugin ? `${req.plugin}.${req.name}` : req.name;
139
+ switch (name) {
140
+ case 'version': return { success: true, major: '6', minor: '3' };
141
+ case 'pfx.list_all_certificates': return { success: true, certificates: [] };
142
+ case 'pfx.load_key': return { success: true, keyId: 'K1' };
143
+ case 'pkcs7.create_pkcs7': return { success: true, pkcs7_64: 'MIAG...' };
144
+ default: return { success: false, reason: `not mocked: ${name}` };
145
+ }
146
+ },
147
+ });
148
+ ```
149
+
150
+ Simulate the user cancelling: `{ success: false, status: -5000, reason: 'Ввод пароля отменен' }` → `CANCELLED`.
@@ -0,0 +1,39 @@
1
+ # Troubleshooting eimzo-ts
2
+
3
+ Read `error.code` first, then `error.reason` / `error.status` (raw E-IMZO data) or `error.httpStatus` (backend).
4
+
5
+ | code | Typical cause | Fix |
6
+ |---|---|---|
7
+ | `NOT_INSTALLED` | E-IMZO is not running or not installed; the WebSocket to 127.0.0.1 closed. `wsCode` has the close code. | Ask the user to start E-IMZO or install it from https://e-imzo.uz/main/downloads/. |
8
+ | `WEBSOCKET_UNSUPPORTED` | No `WebSocket` (very old browser, or code ran on the server during SSR). | Run on the client only; in Node pass `WebSocketImpl`. |
9
+ | `TIMEOUT` | `timeoutMs` is set and E-IMZO (or the user) did not answer in time. | Raise or remove `timeoutMs`. |
10
+ | `OUTDATED_VERSION` | Installed E-IMZO is older than `minVersion` (default 4.86). | Ask the user to update E-IMZO. |
11
+ | `API_KEY_REJECTED` | No valid API key for the page's domain. | `DEV_API_KEYS` for localhost; "Developer mode" in the E-IMZO tray for test domains; an issued key per production domain/subdomain. |
12
+ | `WRONG_PASSWORD` | Wrong key password. `reason` "Password is not ASCII" means a non-Latin keyboard layout. | Let the user retry. E-IMZO itself already showed a message. |
13
+ | `CANCELLED` | The user pressed "Cancel" (status -5000). | Do nothing or reset the UI. Do not show an error. |
14
+ | `KEY_NOT_FOUND` | E-IMZO forgot the `keyId` (status -1010/-1014): restart, 24 h, or unloaded. With `autoReloadKey` (default) `sign()` retries once. | Call `loadKey()` again. |
15
+ | `KEY_NOT_LOADED` | `sign`/`signFile`/`signDocument` before `loadKey()`. | Fix the calling code. `login()` loads the key itself. |
16
+ | `FILE_TOO_LARGE` | File larger than `maxSize` (10 MB default). | Raise `maxSize` or reject the file. |
17
+ | `BACKEND_NOT_CONFIGURED` | `login()` without `getChallenge`/`login`, or `signDocument()` without `timestamp`. | Configure `backend`. |
18
+ | `BACKEND_ERROR` | Backend returned non-2xx or `status !== 1`. | Inspect `httpStatus`, `params.detail`; see backend statuses below. |
19
+ | `EIMZO_ERROR` | Anything else from E-IMZO; status 0 with "Функция не найдена" = unknown function or outdated E-IMZO. | Read `reason`. |
20
+
21
+ ## Other symptoms
22
+
23
+ - **Password asked on every signature.** Several `Eimzo` instances, or `loadKey()` before each `sign()`.
24
+ Keep one instance and load the key once. Without "Remember for 6 hours" E-IMZO keeps the password 1 minute.
25
+ - **Fewer keys than in E-IMZO.** Keys without TIN and PINFL are skipped by design.
26
+ - **Works on localhost, fails on the real domain.** Missing API key for that domain, or the page is https and
27
+ E-IMZO's `wss://127.0.0.1:64443` is not reachable.
28
+
29
+ ## e-imzo-server `/backend/auth` statuses
30
+
31
+ | status | Meaning | Action |
32
+ |---|---|---|
33
+ | 1 | OK | Find the user by PINFL (`1.2.860.3.16.1.2`) or TIN (`1.2.860.3.16.1.1`, else `UID`). |
34
+ | -1 | Certificate status could not be checked (often VPN down) | HTTP 503, ask to retry later; do not log in. |
35
+ | -5 | Signing time outside the allowed window | Ask the user to fix the computer clock. |
36
+ | -10 | Invalid signature | 401 |
37
+ | -11, -12 | Certificate invalid (at signing time) | 401 |
38
+ | -20 | Challenge not found or expired (TTL 120 s) | Restart login. |
39
+ | -24, -25 | Certificate policy / CA not allowed | 401 |