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.
- package/AI_INTEGRATION.md +43 -0
- package/CHANGELOG.md +9 -1
- package/README.md +196 -143
- package/README.ru.md +233 -0
- package/package.json +11 -9
- package/skills/eimzo-ts/SKILL.md +115 -0
- package/skills/eimzo-ts/references/api.md +74 -0
- package/skills/eimzo-ts/references/backend.md +45 -0
- package/skills/eimzo-ts/references/examples.md +150 -0
- package/skills/eimzo-ts/references/troubleshooting.md +39 -0
|
@@ -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.
|
|
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
|
[](https://www.npmjs.com/package/eimzo-ts)
|
|
4
4
|
[](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
Без зависимостей и без привязки к фреймворку: Angular, React, Vue, Svelte или обычная страница.
|
|
7
|
+
**English** · [Русский](README.ru.md)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
17
|
+
> **Unofficial.** This project is not affiliated with the developers of E-IMZO.
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
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' }, //
|
|
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]);
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
### Sign without a backend
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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'))
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
e-imzo-server, поэтому backend может проксировать запросы:
|
|
149
|
+
### Options
|
|
147
150
|
|
|
148
|
-
|
|
|
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
|
|
151
|
-
| `POST loginUrl`,
|
|
152
|
-
| `POST timestampUrl`,
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
## Compatibility
|
|
157
194
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
173
|
-
@Injectable({ providedIn: 'root' })
|
|
174
|
-
export class EimzoService {
|
|
175
|
-
readonly client = createEimzo({ apiKeys: environment.eimzoApiKeys, backend: createFetchBackend({ /* ... */ }) });
|
|
176
|
-
}
|
|
211
|
+
## Alternatives
|
|
177
212
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
219
|
+
## FAQ
|
|
185
220
|
|
|
186
|
-
**
|
|
221
|
+
**Does it work over HTTPS?** Yes. On https pages it connects to `wss://127.0.0.1:64443`.
|
|
187
222
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
##
|
|
264
|
+
## For AI coding assistants
|
|
215
265
|
|
|
216
|
-
|
|
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 #
|
|
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
|
+
[](https://www.npmjs.com/package/eimzo-ts)
|
|
4
|
+
[](https://github.com/abduazimkhuja/eimzo-ts/actions/workflows/ci.yml)
|
|
5
|
+
[](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.
|
|
4
|
-
"description": "Unofficial
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
50
|
-
"
|
|
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 |
|