@islom929/react-eimzo 0.2.0 → 0.3.1

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/README.md ADDED
@@ -0,0 +1,412 @@
1
+ # @islom929/react-eimzo
2
+
3
+ Headless E-IMZO digital signature integration for React. No UI included — bring your own components.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @islom929/react-eimzo
9
+ ```
10
+
11
+ No additional setup required. SDK is bundled and auto-injected.
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Wrap your app with EimzoProvider
16
+
17
+ ```tsx
18
+ import { EimzoProvider } from '@islom929/react-eimzo'
19
+
20
+ function App() {
21
+ return (
22
+ <EimzoProvider>
23
+ <YourApp />
24
+ </EimzoProvider>
25
+ )
26
+ }
27
+ ```
28
+
29
+ ### 2. Use the hook
30
+
31
+ ```tsx
32
+ import { useEimzo } from '@islom929/react-eimzo'
33
+ import type { ICertificate } from '@islom929/react-eimzo'
34
+
35
+ function SignDocument() {
36
+ const { sign, loadKeys, keyList, isInstalled, isLoading } = useEimzo()
37
+
38
+ const handleSign = (cert: ICertificate) => {
39
+ sign({
40
+ keyId: cert,
41
+ data: JSON.stringify({ document: 'content' }),
42
+ onSuccess: (pkcs7) => {
43
+ console.log('Signed:', pkcs7)
44
+ },
45
+ onError: (err) => {
46
+ console.error('Error:', err)
47
+ },
48
+ })
49
+ }
50
+
51
+ return (
52
+ <div>
53
+ <button onClick={loadKeys} disabled={!isInstalled || isLoading}>
54
+ Load keys
55
+ </button>
56
+
57
+ {keyList.map((cert) => (
58
+ <button key={cert.serialNumber} onClick={() => handleSign(cert)}>
59
+ {cert.CN}
60
+ </button>
61
+ ))}
62
+ </div>
63
+ )
64
+ }
65
+ ```
66
+
67
+ ## Usage Examples
68
+
69
+ ### Sign with PFX certificate
70
+
71
+ User selects a certificate from the list. E-IMZO app prompts for password.
72
+
73
+ ```tsx
74
+ import { useEimzo } from '@islom929/react-eimzo'
75
+ import type { ICertificate } from '@islom929/react-eimzo'
76
+
77
+ function PfxSign() {
78
+ const { sign, loadKeys, keyList, isInstalled, isLoading } = useEimzo()
79
+ const [result, setResult] = useState('')
80
+
81
+ useEffect(() => {
82
+ if (isInstalled) loadKeys()
83
+ }, [isInstalled])
84
+
85
+ const handleSign = (cert: ICertificate) => {
86
+ sign({
87
+ keyId: cert,
88
+ data: JSON.stringify({ orderId: 123, amount: 50000 }),
89
+ onSuccess: (pkcs7) => {
90
+ setResult(pkcs7)
91
+ // Send to backend
92
+ fetch('/api/verify', {
93
+ method: 'POST',
94
+ body: JSON.stringify({ pkcs7 }),
95
+ })
96
+ },
97
+ onError: (err) => {
98
+ alert(err) // "Ввод пароля отменен" if user cancels
99
+ },
100
+ })
101
+ }
102
+
103
+ return (
104
+ <div>
105
+ <h3>Select certificate:</h3>
106
+ {keyList.map((cert, i) => (
107
+ <div key={`${cert.serialNumber}-${i}`}>
108
+ <p>{cert.CN} — {cert.O}</p>
109
+ <p>PINFL: {cert.PINFL} | STIR: {cert.TIN}</p>
110
+ <p>Valid until: {new Date(cert.validTo).toLocaleDateString()}</p>
111
+ <button
112
+ onClick={() => handleSign(cert)}
113
+ disabled={cert.expired || isLoading}
114
+ >
115
+ {cert.expired ? 'Expired' : 'Sign'}
116
+ </button>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ )
121
+ }
122
+ ```
123
+
124
+ ### Sign with hardware device
125
+
126
+ No certificate selection needed. Pass device type directly.
127
+
128
+ ```tsx
129
+ function DeviceSign() {
130
+ const { sign, deviceStatus, loadKeys, isInstalled, isLoading } = useEimzo()
131
+
132
+ useEffect(() => {
133
+ if (isInstalled) loadKeys()
134
+ }, [isInstalled])
135
+
136
+ const handleDeviceSign = (device: 'idcard' | 'baikey' | 'ckc') => {
137
+ sign({
138
+ keyId: device,
139
+ data: JSON.stringify({ document: 'content' }),
140
+ onSuccess: (pkcs7) => console.log('Signed:', pkcs7),
141
+ onError: (err) => console.error(err),
142
+ })
143
+ }
144
+
145
+ return (
146
+ <div>
147
+ <button
148
+ onClick={() => handleDeviceSign('idcard')}
149
+ disabled={!deviceStatus.idcard || isLoading}
150
+ >
151
+ ID Card {deviceStatus.idcard ? '(connected)' : '(not connected)'}
152
+ </button>
153
+
154
+ <button
155
+ onClick={() => handleDeviceSign('baikey')}
156
+ disabled={!deviceStatus.baikey || isLoading}
157
+ >
158
+ BAIK Token {deviceStatus.baikey ? '(connected)' : '(not connected)'}
159
+ </button>
160
+
161
+ <button
162
+ onClick={() => handleDeviceSign('ckc')}
163
+ disabled={!deviceStatus.ckc || isLoading}
164
+ >
165
+ CKC {deviceStatus.ckc ? '(connected)' : '(not connected)'}
166
+ </button>
167
+ </div>
168
+ )
169
+ }
170
+ ```
171
+
172
+ ### Form submission with signature
173
+
174
+ Sign form data before submitting to backend.
175
+
176
+ ```tsx
177
+ function ApplicationForm() {
178
+ const { sign, loadKeys, keyList, isInstalled } = useEimzo()
179
+ const [step, setStep] = useState<'form' | 'sign'>('form')
180
+ const [formData, setFormData] = useState({ name: '', amount: 0 })
181
+
182
+ const handleSubmit = (e: React.FormEvent) => {
183
+ e.preventDefault()
184
+ loadKeys()
185
+ setStep('sign')
186
+ }
187
+
188
+ const handleSign = (cert: ICertificate) => {
189
+ sign({
190
+ keyId: cert,
191
+ data: JSON.stringify(formData),
192
+ onSuccess: async (pkcs7) => {
193
+ await fetch('/api/applications', {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({ ...formData, pkcs7 }),
197
+ })
198
+ alert('Application submitted!')
199
+ setStep('form')
200
+ },
201
+ onError: (err) => alert(`Signing failed: ${err}`),
202
+ })
203
+ }
204
+
205
+ if (step === 'form') {
206
+ return (
207
+ <form onSubmit={handleSubmit}>
208
+ <input
209
+ value={formData.name}
210
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
211
+ placeholder="Name"
212
+ />
213
+ <input
214
+ type="number"
215
+ value={formData.amount}
216
+ onChange={(e) => setFormData({ ...formData, amount: +e.target.value })}
217
+ placeholder="Amount"
218
+ />
219
+ <button type="submit" disabled={!isInstalled}>
220
+ Sign & Submit
221
+ </button>
222
+ </form>
223
+ )
224
+ }
225
+
226
+ return (
227
+ <div>
228
+ <h3>Select certificate to sign:</h3>
229
+ {keyList.map((cert, i) => (
230
+ <button
231
+ key={`${cert.serialNumber}-${i}`}
232
+ onClick={() => handleSign(cert)}
233
+ disabled={cert.expired}
234
+ >
235
+ {cert.CN}
236
+ </button>
237
+ ))}
238
+ <button onClick={() => setStep('form')}>Back</button>
239
+ </div>
240
+ )
241
+ }
242
+ ```
243
+
244
+ ### Production API keys
245
+
246
+ Add your domain's API key for production deployment.
247
+
248
+ ```tsx
249
+ <EimzoProvider
250
+ apiKeys={[
251
+ 'yourdomain.uz', 'YOUR_PRODUCTION_API_KEY_HERE',
252
+ 'test.yourdomain.uz', 'YOUR_TEST_API_KEY_HERE',
253
+ ]}
254
+ >
255
+ <App />
256
+ </EimzoProvider>
257
+ ```
258
+
259
+ Default keys for `localhost` and `127.0.0.1` are always included.
260
+
261
+ ### Check E-IMZO installation
262
+
263
+ ```tsx
264
+ function EimzoStatus() {
265
+ const { isInstalled } = useEimzo()
266
+
267
+ if (!isInstalled) {
268
+ return (
269
+ <div>
270
+ <p>E-IMZO is not installed.</p>
271
+ <a href="https://e-imzo.uz/main/downloads/">Download E-IMZO</a>
272
+ </div>
273
+ )
274
+ }
275
+
276
+ return <p>E-IMZO is ready</p>
277
+ }
278
+ ```
279
+
280
+ ### Filter certificates
281
+
282
+ ```tsx
283
+ function FilteredKeys() {
284
+ const { keyList, loadKeys, isInstalled } = useEimzo()
285
+
286
+ useEffect(() => {
287
+ if (isInstalled) loadKeys()
288
+ }, [isInstalled])
289
+
290
+ // Only valid (not expired) certificates
291
+ const validCerts = keyList.filter((cert) => !cert.expired)
292
+
293
+ // Filter by organization
294
+ const orgCerts = keyList.filter((cert) => cert.O === 'MY COMPANY')
295
+
296
+ // Filter by PINFL
297
+ const userCerts = keyList.filter((cert) => cert.PINFL === '12345678901234')
298
+
299
+ return (
300
+ <div>
301
+ <p>All: {keyList.length}</p>
302
+ <p>Valid: {validCerts.length}</p>
303
+ <p>My org: {orgCerts.length}</p>
304
+ </div>
305
+ )
306
+ }
307
+ ```
308
+
309
+ ## API
310
+
311
+ ### EimzoProvider
312
+
313
+ Wraps your app. Initializes E-IMZO SDK automatically.
314
+
315
+ | Prop | Type | Description |
316
+ |------|------|-------------|
317
+ | `apiKeys` | `string[]` | Optional. Additional domain + API key pairs |
318
+ | `children` | `ReactNode` | Required |
319
+
320
+ ### useEimzo()
321
+
322
+ | Property | Type | Description |
323
+ |----------|------|-------------|
324
+ | `isInstalled` | `boolean` | E-IMZO app detected and running |
325
+ | `isLoading` | `boolean` | Operation in progress (loadKeys or sign) |
326
+ | `keyList` | `ICertificate[]` | Available certificates |
327
+ | `deviceStatus` | `IDeviceStatus` | Connected hardware devices |
328
+ | `loadKeys` | `() => Promise<void>` | Load certificates and check devices |
329
+ | `sign` | `(params: ISignParams) => void` | Sign data |
330
+
331
+ ### sign(params)
332
+
333
+ | Param | Type | Description |
334
+ |-------|------|-------------|
335
+ | `keyId` | `ICertificate \| string` | Certificate object or `'idcard'` / `'baikey'` / `'ckc'` |
336
+ | `data` | `string` | Data to sign (usually JSON.stringify) |
337
+ | `onSuccess` | `(pkcs7: string) => void` | Called with base64 PKCS#7 signature |
338
+ | `onError` | `(error: string) => void` | Optional. Called on failure |
339
+
340
+ ## Types
341
+
342
+ ```tsx
343
+ import type {
344
+ ICertificate,
345
+ ISignParams,
346
+ IDeviceStatus,
347
+ IEimzoContext,
348
+ IEimzoProviderProps,
349
+ TKeyType,
350
+ } from '@islom929/react-eimzo'
351
+ ```
352
+
353
+ ### ICertificate
354
+
355
+ | Field | Type | Description |
356
+ |-------|------|-------------|
357
+ | `CN` | `string` | Full name |
358
+ | `PINFL` | `string` | Personal ID number |
359
+ | `TIN` | `string` | Tax ID (STIR) |
360
+ | `O` | `string` | Organization |
361
+ | `T` | `string` | Title/Position |
362
+ | `UID` | `string` | User ID |
363
+ | `serialNumber` | `string` | Certificate serial number |
364
+ | `validFrom` | `Date` | Start of validity |
365
+ | `validTo` | `Date` | End of validity |
366
+ | `type` | `'pfx' \| 'ftjc'` | Certificate type |
367
+ | `expired` | `boolean` | Whether certificate has expired |
368
+
369
+ ### IDeviceStatus
370
+
371
+ | Field | Type | Description |
372
+ |-------|------|-------------|
373
+ | `idcard` | `boolean` | ID card / EIMZO-Token connected |
374
+ | `baikey` | `boolean` | BAIK-Token connected |
375
+ | `ckc` | `boolean` | CKC device connected |
376
+
377
+ ## Supported Key Types
378
+
379
+ | Type | Description | E-IMZO Version |
380
+ |------|-------------|----------------|
381
+ | PFX | Local certificate file (ERI) | v3.36+ |
382
+ | ID-card / EIMZO-Token | Physical smart card | v4.12+ |
383
+ | BAIK-Token | BAIK hardware token | v4.86+ |
384
+ | CKC | CryptKeyContainer (universal) | v4.86+ |
385
+
386
+ ## How It Works
387
+
388
+ ```
389
+ Your React App
390
+ ↓ useEimzo()
391
+ @islom929/react-eimzo
392
+ ↓ WebSocket (wss://127.0.0.1:64443)
393
+ E-IMZO Desktop App
394
+
395
+ PFX files / USB tokens / ID cards
396
+ ```
397
+
398
+ 1. Package injects E-IMZO SDK into the page automatically
399
+ 2. SDK connects to E-IMZO desktop app via WebSocket
400
+ 3. `loadKeys()` fetches available certificates
401
+ 4. `sign()` sends data to E-IMZO app for signing
402
+ 5. E-IMZO app prompts user for password/PIN
403
+ 6. Signed PKCS#7 (base64) returned via `onSuccess`
404
+
405
+ ## Requirements
406
+
407
+ - React 18+
408
+ - [E-IMZO desktop application](https://e-imzo.uz/main/downloads/) installed on user's computer
409
+
410
+ ## License
411
+
412
+ MIT
package/dist/index.cjs CHANGED
@@ -839,7 +839,7 @@ function EimzoProvider({ apiKeys, children }) {
839
839
  }
840
840
  }, []);
841
841
  const sign = (0, import_react.useCallback)(
842
- async ({ keyId, data }) => {
842
+ async ({ keyId, data, onSuccess, onError }) => {
843
843
  setIsLoading(true);
844
844
  try {
845
845
  let id;
@@ -849,7 +849,10 @@ function EimzoProvider({ apiKeys, children }) {
849
849
  id = keyId;
850
850
  }
851
851
  const pkcs7 = await createPkcs7(id, data);
852
- return pkcs7;
852
+ onSuccess(pkcs7);
853
+ } catch (err) {
854
+ const message = typeof err === "string" ? err : "E-IMZO signing failed";
855
+ onError?.(message);
853
856
  } finally {
854
857
  setIsLoading(false);
855
858
  }
package/dist/index.d.cts CHANGED
@@ -26,6 +26,8 @@ interface ICertificate {
26
26
  interface ISignParams {
27
27
  keyId: ICertificate | string;
28
28
  data: string;
29
+ onSuccess: (pkcs7: string) => void;
30
+ onError?: (error: string) => void;
29
31
  }
30
32
  interface IDeviceStatus {
31
33
  idcard: boolean;
@@ -42,7 +44,7 @@ interface IEimzoContext {
42
44
  keyList: ICertificate[];
43
45
  deviceStatus: IDeviceStatus;
44
46
  loadKeys: () => Promise<void>;
45
- sign: (params: ISignParams) => Promise<string>;
47
+ sign: (params: ISignParams) => void;
46
48
  }
47
49
 
48
50
  interface IEimzoProviderProps {
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ interface ICertificate {
26
26
  interface ISignParams {
27
27
  keyId: ICertificate | string;
28
28
  data: string;
29
+ onSuccess: (pkcs7: string) => void;
30
+ onError?: (error: string) => void;
29
31
  }
30
32
  interface IDeviceStatus {
31
33
  idcard: boolean;
@@ -42,7 +44,7 @@ interface IEimzoContext {
42
44
  keyList: ICertificate[];
43
45
  deviceStatus: IDeviceStatus;
44
46
  loadKeys: () => Promise<void>;
45
- sign: (params: ISignParams) => Promise<string>;
47
+ sign: (params: ISignParams) => void;
46
48
  }
47
49
 
48
50
  interface IEimzoProviderProps {
package/dist/index.js CHANGED
@@ -818,7 +818,7 @@ function EimzoProvider({ apiKeys, children }) {
818
818
  }
819
819
  }, []);
820
820
  const sign = useCallback(
821
- async ({ keyId, data }) => {
821
+ async ({ keyId, data, onSuccess, onError }) => {
822
822
  setIsLoading(true);
823
823
  try {
824
824
  let id;
@@ -828,7 +828,10 @@ function EimzoProvider({ apiKeys, children }) {
828
828
  id = keyId;
829
829
  }
830
830
  const pkcs7 = await createPkcs7(id, data);
831
- return pkcs7;
831
+ onSuccess(pkcs7);
832
+ } catch (err) {
833
+ const message = typeof err === "string" ? err : "E-IMZO signing failed";
834
+ onError?.(message);
832
835
  } finally {
833
836
  setIsLoading(false);
834
837
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islom929/react-eimzo",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Headless E-IMZO digital signature integration for React",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",