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