eimzo-ts 0.1.1 → 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 +17 -1
- package/README.md +196 -143
- package/README.ru.md +233 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +2 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- 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,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 |
|