eimzo-sign-core 0.1.0
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 +62 -0
- package/dist/index.cjs +1340 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +247 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +1330 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/cert-utils.ts +53 -0
- package/src/client.ts +456 -0
- package/src/errors.ts +73 -0
- package/src/index.ts +45 -0
- package/src/raw-types.ts +65 -0
- package/src/types.ts +112 -0
- package/src/util.ts +69 -0
- package/src/vendor/VENDOR.md +59 -0
- package/src/vendor/e-imzo-client.js +477 -0
- package/src/vendor/e-imzo.js +483 -0
- package/src/vendor/index.ts +46 -0
- package/src/vendor/vendor.d.ts +15 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { EimzoError, toEimzoError } from './errors'
|
|
2
|
+
import type {
|
|
3
|
+
AuditEvent,
|
|
4
|
+
BatchSignItem,
|
|
5
|
+
Cert,
|
|
6
|
+
CertType,
|
|
7
|
+
EimzoClientOptions,
|
|
8
|
+
EimzoVersion,
|
|
9
|
+
SignBatchOptions,
|
|
10
|
+
SignDoc,
|
|
11
|
+
} from './types'
|
|
12
|
+
import { nowIso, normalizeSerial, throwIfAborted, withTimeout } from './util'
|
|
13
|
+
import { getBase64, getCapiws, rawClient } from './vendor'
|
|
14
|
+
import type { RawCapiResponse, RawCert } from './raw-types'
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 30_000
|
|
17
|
+
const DEFAULT_KEY_LOAD_TIMEOUT_MS = 120_000
|
|
18
|
+
const DEFAULT_MIN_VERSION: EimzoVersion = { major: 3, minor: 37 }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* E-IMZO bilan ishlashning framework-agnostik yadrosi.
|
|
22
|
+
*
|
|
23
|
+
* Kafolatlar:
|
|
24
|
+
* - har chaqiruvda timeout bor (osilib qolmaydi);
|
|
25
|
+
* - `signBatch` kalitni BIR MARTA yuklaydi (parol bir marta so'raladi);
|
|
26
|
+
* - har imzodan keyin seriya raqami tanlangan sertifikatga tekshiriladi;
|
|
27
|
+
* - sessiya tugagach yuklangan kalit holati JS'da saqlanmaydi;
|
|
28
|
+
* - `console` ga hech narsa yozilmaydi — hammasi `onAudit` orqali.
|
|
29
|
+
*/
|
|
30
|
+
export class EimzoClient {
|
|
31
|
+
private readonly apiKeys: ReadonlyArray<readonly [string, string]>
|
|
32
|
+
private readonly timeoutMs: number
|
|
33
|
+
private readonly keyLoadTimeoutMs: number
|
|
34
|
+
private readonly minVersion: EimzoVersion
|
|
35
|
+
private readonly onAudit?: (event: AuditEvent) => void
|
|
36
|
+
|
|
37
|
+
/** Faqat joriy sessiya davomida to'ldiriladi; `signBatch` oxirida tozalanadi. */
|
|
38
|
+
private activeKeySerial: string | null = null
|
|
39
|
+
|
|
40
|
+
constructor(options: EimzoClientOptions) {
|
|
41
|
+
if (!Array.isArray(options?.apiKeys) || options.apiKeys.length === 0) {
|
|
42
|
+
throw new EimzoError(
|
|
43
|
+
'E_MISCONFIGURED',
|
|
44
|
+
"EimzoClient: 'apiKeys' berilishi shart ([[domen, kalit], ...]).",
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
this.apiKeys = options.apiKeys
|
|
48
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
49
|
+
this.keyLoadTimeoutMs = options.keyLoadTimeoutMs ?? DEFAULT_KEY_LOAD_TIMEOUT_MS
|
|
50
|
+
this.minVersion = options.minVersion ?? DEFAULT_MIN_VERSION
|
|
51
|
+
this.onAudit = options.onAudit
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Commanda: o'rnatish ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Versiyani tekshiradi va domen API kalitlarini o'rnatadi.
|
|
58
|
+
* Har imzolash seansidan oldin (yoki ilova yuklanganda bir marta) chaqiring.
|
|
59
|
+
*/
|
|
60
|
+
async install(): Promise<EimzoVersion> {
|
|
61
|
+
const version = await this.checkVersion()
|
|
62
|
+
|
|
63
|
+
if (!isVersionAtLeast(version, this.minVersion)) {
|
|
64
|
+
throw new EimzoError(
|
|
65
|
+
'E_VERSION_LOW',
|
|
66
|
+
`E-IMZO ${version.major}.${version.minor} — kamida ` +
|
|
67
|
+
`${this.minVersion.major}.${this.minVersion.minor} talab qilinadi.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rawClient.API_KEYS = this.apiKeys.flatMap(([domain, key]) => [domain, key])
|
|
72
|
+
await this.installApiKeys()
|
|
73
|
+
return version
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
checkVersion(): Promise<EimzoVersion> {
|
|
77
|
+
return withTimeout<EimzoVersion>(
|
|
78
|
+
this.timeoutMs,
|
|
79
|
+
() =>
|
|
80
|
+
new EimzoError(
|
|
81
|
+
'E_NOT_INSTALLED',
|
|
82
|
+
"E-IMZO dasturi javob bermadi (versiya so'rovi). Dastur ishga tushganini tekshiring.",
|
|
83
|
+
),
|
|
84
|
+
(resolve, reject) => {
|
|
85
|
+
rawClient.checkVersion(
|
|
86
|
+
(major, minor) => {
|
|
87
|
+
this.audit({ type: 'version_checked', ts: nowIso(), major, minor })
|
|
88
|
+
resolve({ major, minor })
|
|
89
|
+
},
|
|
90
|
+
(error) =>
|
|
91
|
+
reject(
|
|
92
|
+
new EimzoError('E_NOT_INSTALLED', 'E-IMZO dasturi topilmadi.', {
|
|
93
|
+
cause: error,
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private installApiKeys(): Promise<void> {
|
|
102
|
+
return withTimeout<void>(
|
|
103
|
+
this.timeoutMs,
|
|
104
|
+
() => new EimzoError('E_TIMEOUT', "API kalitlari o'rnatilmadi (javob yo'q)."),
|
|
105
|
+
(resolve, reject) => {
|
|
106
|
+
rawClient.installApiKeys(
|
|
107
|
+
() => resolve(),
|
|
108
|
+
(error) =>
|
|
109
|
+
reject(
|
|
110
|
+
new EimzoError(
|
|
111
|
+
'E_API_KEY_REJECTED',
|
|
112
|
+
"Domen API kaliti rad etildi. Domen va kalit juftligini tekshiring.",
|
|
113
|
+
{ cause: error },
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── So'rov: sertifikatlar ───────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/** Foydalanuvchining barcha kalitlari (disk + token). ID-karta bunga kirmaydi. */
|
|
124
|
+
listCerts(): Promise<Cert[]> {
|
|
125
|
+
return withTimeout<Cert[]>(
|
|
126
|
+
this.timeoutMs,
|
|
127
|
+
() => new EimzoError('E_TIMEOUT', "Sertifikatlar ro'yxati javob bermadi."),
|
|
128
|
+
(resolve, reject) => {
|
|
129
|
+
rawClient.listAllUserKeys(
|
|
130
|
+
(cert, index) => `${cert.serialNumber}-${index}`,
|
|
131
|
+
(_id, cert) => cert,
|
|
132
|
+
(items) => {
|
|
133
|
+
const certs = (items as RawCert[]).map((raw) => normalizeCert(raw))
|
|
134
|
+
this.audit({ type: 'certs_listed', ts: nowIso(), count: certs.length })
|
|
135
|
+
resolve(certs)
|
|
136
|
+
},
|
|
137
|
+
(error) =>
|
|
138
|
+
reject(
|
|
139
|
+
new EimzoError('E_NO_CERTS', "Sertifikatlarni o'qib bo'lmadi.", {
|
|
140
|
+
cause: error,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** ID-karta o'quvchiga ulanganmi. Timeout'da `false` qaytaradi (xato tashlamaydi). */
|
|
149
|
+
isIdCardPlugged(): Promise<boolean> {
|
|
150
|
+
return new Promise<boolean>((resolve) => {
|
|
151
|
+
let settled = false
|
|
152
|
+
const finish = (value: boolean): void => {
|
|
153
|
+
if (settled) return
|
|
154
|
+
settled = true
|
|
155
|
+
clearTimeout(timer)
|
|
156
|
+
resolve(value)
|
|
157
|
+
}
|
|
158
|
+
const timer = setTimeout(() => finish(false), this.timeoutMs)
|
|
159
|
+
try {
|
|
160
|
+
rawClient.idCardIsPLuggedIn(
|
|
161
|
+
(plugged) => finish(Boolean(plugged)),
|
|
162
|
+
() => finish(false),
|
|
163
|
+
)
|
|
164
|
+
} catch {
|
|
165
|
+
finish(false)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** ID-karta uchun "sertifikat" o'rnini bosuvchi belgili obyekt. */
|
|
171
|
+
static idCardCert(): Cert {
|
|
172
|
+
const far = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000)
|
|
173
|
+
return {
|
|
174
|
+
serialNumber: 'IDCARD',
|
|
175
|
+
CN: 'ID card',
|
|
176
|
+
TIN: '',
|
|
177
|
+
UID: '',
|
|
178
|
+
O: '',
|
|
179
|
+
T: '',
|
|
180
|
+
validFrom: new Date(0),
|
|
181
|
+
validTo: far,
|
|
182
|
+
type: 'idcard',
|
|
183
|
+
raw: null,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Commanda: partiyali imzolash ───────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Bitta sertifikat bilan bir nechta hujjatni imzolaydi.
|
|
191
|
+
*
|
|
192
|
+
* Oqim:
|
|
193
|
+
* 1. Kalitni bir marta yuklaydi (`idcard` bo'lsa — yuklamaydi).
|
|
194
|
+
* 2. Har hujjat uchun `create_pkcs7` ni KETMA-KET chaqiradi (parallel EMAS —
|
|
195
|
+
* CAPIWS bitta soket).
|
|
196
|
+
* 3. Har natijada `signer_serial_number` ni tekshiradi.
|
|
197
|
+
* 4. Yakunda kalit holatini tozalaydi.
|
|
198
|
+
*
|
|
199
|
+
* Bitta hujjat xato bersa — massivda `ok:false` bilan qoladi, qolganlari davom etadi.
|
|
200
|
+
* Foydalanuvchi bekor qilsa (`signal`) — `E_CANCELLED` tashlanadi.
|
|
201
|
+
*/
|
|
202
|
+
async signBatch(
|
|
203
|
+
cert: Cert,
|
|
204
|
+
docs: readonly SignDoc[],
|
|
205
|
+
options: SignBatchOptions = {},
|
|
206
|
+
): Promise<BatchSignItem[]> {
|
|
207
|
+
if (docs.length === 0) {
|
|
208
|
+
throw new EimzoError('E_MISCONFIGURED', "Imzolash uchun hujjat berilmadi.")
|
|
209
|
+
}
|
|
210
|
+
assertUniqueIds(docs)
|
|
211
|
+
const { signal, onProgress } = options
|
|
212
|
+
throwIfAborted(signal)
|
|
213
|
+
|
|
214
|
+
const base64 = getBase64()
|
|
215
|
+
const keyId = await this.acquireKey(cert, signal)
|
|
216
|
+
|
|
217
|
+
const results: BatchSignItem[] = []
|
|
218
|
+
let signed = 0
|
|
219
|
+
let failed = 0
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
for (let i = 0; i < docs.length; i += 1) {
|
|
223
|
+
throwIfAborted(signal)
|
|
224
|
+
const doc = docs[i]!
|
|
225
|
+
onProgress?.({ done: i, total: docs.length, currentId: doc.id })
|
|
226
|
+
this.audit({
|
|
227
|
+
type: 'sign_started',
|
|
228
|
+
ts: nowIso(),
|
|
229
|
+
serial: cert.serialNumber,
|
|
230
|
+
docId: doc.id,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const raw = await this.createPkcs7(keyId, base64.encode(doc.content), signal)
|
|
235
|
+
const signerSerial = String(raw['signer_serial_number'] ?? '')
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
cert.type !== 'idcard' &&
|
|
239
|
+
normalizeSerial(signerSerial) !== normalizeSerial(cert.serialNumber)
|
|
240
|
+
) {
|
|
241
|
+
throw new EimzoError(
|
|
242
|
+
'E_SERIAL_MISMATCH',
|
|
243
|
+
'Imzolangan sertifikat seriyasi tanlangandan farq qiladi.',
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
results.push({
|
|
248
|
+
id: doc.id,
|
|
249
|
+
ok: true,
|
|
250
|
+
pkcs7_64: String(raw['pkcs7_64'] ?? ''),
|
|
251
|
+
signature_hex: String(raw['signature_hex'] ?? ''),
|
|
252
|
+
signer_serial_number: signerSerial,
|
|
253
|
+
})
|
|
254
|
+
signed += 1
|
|
255
|
+
this.audit({
|
|
256
|
+
type: 'sign_ok',
|
|
257
|
+
ts: nowIso(),
|
|
258
|
+
serial: cert.serialNumber,
|
|
259
|
+
docId: doc.id,
|
|
260
|
+
})
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Bekor qilish — butun partiyani to'xtatadi.
|
|
263
|
+
if (EimzoError.is(error, 'E_CANCELLED')) throw error
|
|
264
|
+
|
|
265
|
+
const err = toEimzoError(error, 'E_SIGN_FAILED', 'Hujjatni imzolab bo\'lmadi.')
|
|
266
|
+
results.push({ id: doc.id, ok: false, error: err })
|
|
267
|
+
failed += 1
|
|
268
|
+
this.audit({
|
|
269
|
+
type: 'sign_failed',
|
|
270
|
+
ts: nowIso(),
|
|
271
|
+
docId: doc.id,
|
|
272
|
+
code: err.code,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
onProgress?.({ done: docs.length, total: docs.length })
|
|
278
|
+
return results
|
|
279
|
+
} finally {
|
|
280
|
+
// Kalitni JS holatida ushlab turmaymiz. E-IMZO dasturi kalitni o'z
|
|
281
|
+
// ichida qisqa TTL bilan eslaydi; biz unga tayanmaymiz.
|
|
282
|
+
this.activeKeySerial = null
|
|
283
|
+
this.audit({
|
|
284
|
+
type: 'session_ended',
|
|
285
|
+
ts: nowIso(),
|
|
286
|
+
serial: cert.serialNumber,
|
|
287
|
+
signed,
|
|
288
|
+
failed,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Yagona hujjatni imzolash — `signBatch` ustidagi qulaylik. */
|
|
294
|
+
async sign(
|
|
295
|
+
cert: Cert,
|
|
296
|
+
content: string,
|
|
297
|
+
options: SignBatchOptions = {},
|
|
298
|
+
): Promise<BatchSignItem> {
|
|
299
|
+
const [item] = await this.signBatch(cert, [{ id: 'single', content }], options)
|
|
300
|
+
return item!
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Ichki ─────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
private async acquireKey(cert: Cert, signal?: AbortSignal): Promise<string> {
|
|
306
|
+
if (cert.type === 'idcard') return 'idcard'
|
|
307
|
+
try {
|
|
308
|
+
return await this.loadKey(cert, signal)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw toEimzoError(
|
|
311
|
+
error,
|
|
312
|
+
'E_KEY_LOAD_CANCELLED',
|
|
313
|
+
'Kalit yuklanmadi yoki bekor qilindi.',
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private loadKey(cert: Cert, signal?: AbortSignal): Promise<string> {
|
|
319
|
+
return withTimeout<string>(
|
|
320
|
+
this.keyLoadTimeoutMs,
|
|
321
|
+
() =>
|
|
322
|
+
new EimzoError(
|
|
323
|
+
'E_KEY_LOAD_CANCELLED',
|
|
324
|
+
"Kalit yuklash javob bermadi (parol oynasi ochilmadi yoki yopilmadi).",
|
|
325
|
+
),
|
|
326
|
+
(resolve, reject) => {
|
|
327
|
+
rawClient.loadKey(
|
|
328
|
+
cert.raw as RawCert,
|
|
329
|
+
(id) => {
|
|
330
|
+
this.activeKeySerial = cert.serialNumber
|
|
331
|
+
this.audit({
|
|
332
|
+
type: 'key_loaded',
|
|
333
|
+
ts: nowIso(),
|
|
334
|
+
serial: cert.serialNumber,
|
|
335
|
+
})
|
|
336
|
+
resolve(id)
|
|
337
|
+
},
|
|
338
|
+
(error) =>
|
|
339
|
+
reject(
|
|
340
|
+
new EimzoError('E_KEY_LOAD_CANCELLED', 'Kalit yuklanmadi.', {
|
|
341
|
+
cause: error,
|
|
342
|
+
}),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
signal,
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private createPkcs7(
|
|
351
|
+
keyId: string,
|
|
352
|
+
contentBase64: string,
|
|
353
|
+
signal?: AbortSignal,
|
|
354
|
+
): Promise<RawCapiResponse> {
|
|
355
|
+
return withTimeout<RawCapiResponse>(
|
|
356
|
+
this.timeoutMs,
|
|
357
|
+
() => new EimzoError('E_TIMEOUT', "Imzolash javob bermadi (create_pkcs7)."),
|
|
358
|
+
(resolve, reject) => {
|
|
359
|
+
let capiws
|
|
360
|
+
try {
|
|
361
|
+
capiws = getCapiws()
|
|
362
|
+
} catch (error) {
|
|
363
|
+
reject(error)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
capiws.callFunction(
|
|
367
|
+
{
|
|
368
|
+
plugin: 'pkcs7',
|
|
369
|
+
name: 'create_pkcs7',
|
|
370
|
+
arguments: [contentBase64, keyId, 'no'],
|
|
371
|
+
},
|
|
372
|
+
(_event, data) => {
|
|
373
|
+
if (data && data.success) {
|
|
374
|
+
resolve(data)
|
|
375
|
+
} else {
|
|
376
|
+
reject(
|
|
377
|
+
new EimzoError(
|
|
378
|
+
'E_SIGN_FAILED',
|
|
379
|
+
reasonOf(data) || 'E-IMZO imzolashni rad etdi.',
|
|
380
|
+
{ cause: data },
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
(error) =>
|
|
386
|
+
reject(
|
|
387
|
+
new EimzoError('E_TRANSPORT', "E-IMZO bilan aloqa uzildi.", {
|
|
388
|
+
cause: error,
|
|
389
|
+
}),
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
},
|
|
393
|
+
signal,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private audit(event: AuditEvent): void {
|
|
398
|
+
if (!this.onAudit) return
|
|
399
|
+
try {
|
|
400
|
+
this.onAudit(event)
|
|
401
|
+
} catch {
|
|
402
|
+
// Audit hook consumer kodida yashaydi — uning xatosi kutubxonani buzmasin.
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Modul yordamchilari ──────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
function isVersionAtLeast(actual: EimzoVersion, min: EimzoVersion): boolean {
|
|
410
|
+
if (actual.major !== min.major) return actual.major > min.major
|
|
411
|
+
return actual.minor >= min.minor
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function reasonOf(data: RawCapiResponse | undefined | null): string {
|
|
415
|
+
return data && typeof data.reason === 'string' ? data.reason : ''
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const KNOWN_TYPES: ReadonlySet<CertType> = new Set(['pfx', 'ftjc', 'idcard'])
|
|
419
|
+
|
|
420
|
+
function normalizeCert(raw: RawCert): Cert {
|
|
421
|
+
return {
|
|
422
|
+
serialNumber: normalizeSerial(raw.serialNumber),
|
|
423
|
+
CN: str(raw.CN),
|
|
424
|
+
TIN: str(raw.TIN),
|
|
425
|
+
UID: str(raw.UID),
|
|
426
|
+
O: str(raw.O),
|
|
427
|
+
T: str(raw.T),
|
|
428
|
+
validFrom: toDate(raw.validFrom),
|
|
429
|
+
validTo: toDate(raw.validTo),
|
|
430
|
+
type: KNOWN_TYPES.has(raw.type as CertType) ? (raw.type as CertType) : 'pfx',
|
|
431
|
+
raw,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function str(value: unknown): string {
|
|
436
|
+
return value == null ? '' : String(value)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function toDate(value: string | Date | undefined): Date {
|
|
440
|
+
if (value instanceof Date) return value
|
|
441
|
+
const d = new Date(String(value ?? ''))
|
|
442
|
+
return Number.isNaN(d.getTime()) ? new Date(0) : d
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function assertUniqueIds(docs: readonly SignDoc[]): void {
|
|
446
|
+
const seen = new Set<string>()
|
|
447
|
+
for (const doc of docs) {
|
|
448
|
+
if (!doc.id) {
|
|
449
|
+
throw new EimzoError('E_MISCONFIGURED', 'Har bir hujjatda id bo\'lishi shart.')
|
|
450
|
+
}
|
|
451
|
+
if (seen.has(doc.id)) {
|
|
452
|
+
throw new EimzoError('E_MISCONFIGURED', `Hujjat id'si takrorlangan: ${doc.id}`)
|
|
453
|
+
}
|
|
454
|
+
seen.add(doc.id)
|
|
455
|
+
}
|
|
456
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E-IMZO xatolarining barqaror kontrakti.
|
|
3
|
+
*
|
|
4
|
+
* Chaqiruvchi kod HAR DOIM `error.code` bo'yicha shox tanlaydi — xabar matnini
|
|
5
|
+
* hech qachon parslamaydi (matn i18n bilan o'zgaradi).
|
|
6
|
+
*/
|
|
7
|
+
export type EimzoErrorCode =
|
|
8
|
+
/** E-IMZO dasturi ishga tushmagan yoki javob bermayapti. */
|
|
9
|
+
| 'E_NOT_INSTALLED'
|
|
10
|
+
/** E-IMZO versiyasi talab qilinganidan past. */
|
|
11
|
+
| 'E_VERSION_LOW'
|
|
12
|
+
/** Domen uchun API kaliti rad etildi (noto'g'ri kalit yoki domen). */
|
|
13
|
+
| 'E_API_KEY_REJECTED'
|
|
14
|
+
/** Foydalanuvchida sertifikat/kalit topilmadi yoki ro'yxatni o'qib bo'lmadi. */
|
|
15
|
+
| 'E_NO_CERTS'
|
|
16
|
+
/** Kalit yuklanmadi — ko'pincha foydalanuvchi parol oynasini yopgan. */
|
|
17
|
+
| 'E_KEY_LOAD_CANCELLED'
|
|
18
|
+
/** Imzolangan sertifikatning seriya raqami tanlangandan farq qiladi. XAVFSIZLIK. */
|
|
19
|
+
| 'E_SERIAL_MISMATCH'
|
|
20
|
+
/** `create_pkcs7` muvaffaqiyatsiz tugadi. */
|
|
21
|
+
| 'E_SIGN_FAILED'
|
|
22
|
+
/** E-IMZO belgilangan vaqt ichida javob bermadi. */
|
|
23
|
+
| 'E_TIMEOUT'
|
|
24
|
+
/** Foydalanuvchi yoki idle-taymer amalni bekor qildi. */
|
|
25
|
+
| 'E_CANCELLED'
|
|
26
|
+
/** Lokal soket / transport xatosi. */
|
|
27
|
+
| 'E_TRANSPORT'
|
|
28
|
+
/** Kutubxona noto'g'ri sozlangan (masalan, apiKeys berilmagan). */
|
|
29
|
+
| 'E_MISCONFIGURED'
|
|
30
|
+
|
|
31
|
+
export interface EimzoErrorOptions {
|
|
32
|
+
cause?: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class EimzoError extends Error {
|
|
36
|
+
readonly code: EimzoErrorCode
|
|
37
|
+
/** Asl sabab (vendor xatosi, CAPIWS javobi va h.k.). ES2022 `Error.cause` bilan bir xil nom. */
|
|
38
|
+
readonly cause?: unknown
|
|
39
|
+
|
|
40
|
+
constructor(code: EimzoErrorCode, message: string, options?: EimzoErrorOptions) {
|
|
41
|
+
super(message)
|
|
42
|
+
this.name = 'EimzoError'
|
|
43
|
+
this.code = code
|
|
44
|
+
this.cause = options?.cause
|
|
45
|
+
// TS -> eski target'da instanceof ishlashi uchun
|
|
46
|
+
Object.setPrototypeOf(this, EimzoError.prototype)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `EimzoError.is(e)` yoki `EimzoError.is(e, 'E_TIMEOUT')`. */
|
|
50
|
+
static is(error: unknown, code?: EimzoErrorCode): error is EimzoError {
|
|
51
|
+
return (
|
|
52
|
+
error instanceof EimzoError && (code === undefined || error.code === code)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Bekor qilingan (foydalanuvchi voz kechgan) — odatda jimgina o'tiladi. */
|
|
57
|
+
static isCancelled(error: unknown): boolean {
|
|
58
|
+
return (
|
|
59
|
+
EimzoError.is(error, 'E_CANCELLED') ||
|
|
60
|
+
EimzoError.is(error, 'E_KEY_LOAD_CANCELLED')
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Noma'lum qiymatni EimzoError'ga keltiradi (allaqachon EimzoError bo'lsa — o'zini qaytaradi). */
|
|
66
|
+
export function toEimzoError(
|
|
67
|
+
error: unknown,
|
|
68
|
+
fallbackCode: EimzoErrorCode,
|
|
69
|
+
fallbackMessage: string,
|
|
70
|
+
): EimzoError {
|
|
71
|
+
if (EimzoError.is(error)) return error
|
|
72
|
+
return new EimzoError(fallbackCode, fallbackMessage, { cause: error })
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eimzo-sign-core — E-IMZO imzolashning framework-agnostik yadrosi.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { EimzoClient } from 'eimzo-sign-core'
|
|
7
|
+
*
|
|
8
|
+
* const eimzo = new EimzoClient({
|
|
9
|
+
* apiKeys: [['example.uz', '<E-IMZO tomonidan berilgan kalit>']],
|
|
10
|
+
* onAudit: (e) => auditLog.write(e),
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* await eimzo.install()
|
|
14
|
+
* const certs = await eimzo.listCerts()
|
|
15
|
+
* const results = await eimzo.signBatch(certs[0], [
|
|
16
|
+
* { id: 'doc-1', content: xml1 },
|
|
17
|
+
* { id: 'doc-2', content: xml2 },
|
|
18
|
+
* ])
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { EimzoClient } from './client'
|
|
22
|
+
export { EimzoError, toEimzoError } from './errors'
|
|
23
|
+
export type { EimzoErrorCode, EimzoErrorOptions } from './errors'
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
AuditEvent,
|
|
27
|
+
BatchProgress,
|
|
28
|
+
BatchSignItem,
|
|
29
|
+
Cert,
|
|
30
|
+
CertType,
|
|
31
|
+
EimzoClientOptions,
|
|
32
|
+
EimzoVersion,
|
|
33
|
+
SignBatchOptions,
|
|
34
|
+
SignDoc,
|
|
35
|
+
SignResult,
|
|
36
|
+
} from './types'
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
certExpiresSoon,
|
|
40
|
+
certIsExpired,
|
|
41
|
+
certIsNotYetValid,
|
|
42
|
+
certIsUsable,
|
|
43
|
+
dedupeCerts,
|
|
44
|
+
sortCerts,
|
|
45
|
+
} from './cert-utils'
|
package/src/raw-types.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored E-IMZO SDK'ning XOM (raw) API shakllari.
|
|
3
|
+
* Bu tiplar faqat `client.ts` ichida ishlatiladi; tashqariga chiqmaydi.
|
|
4
|
+
* Tashqi API uchun `types.ts` ga qarang.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RawCapiResponse {
|
|
8
|
+
success: boolean
|
|
9
|
+
reason?: string
|
|
10
|
+
[key: string]: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RawCapiws {
|
|
14
|
+
URL?: string
|
|
15
|
+
callFunction(
|
|
16
|
+
funcDef: { plugin?: string; name: string; arguments: unknown[] },
|
|
17
|
+
callback: (event: unknown, data: RawCapiResponse) => void,
|
|
18
|
+
error: (e: unknown) => void,
|
|
19
|
+
): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RawCert {
|
|
23
|
+
disk?: string
|
|
24
|
+
path?: string
|
|
25
|
+
name?: string
|
|
26
|
+
alias?: string
|
|
27
|
+
serialNumber: string
|
|
28
|
+
validFrom: string | Date
|
|
29
|
+
validTo: string | Date
|
|
30
|
+
CN?: string
|
|
31
|
+
TIN?: string
|
|
32
|
+
UID?: string
|
|
33
|
+
O?: string
|
|
34
|
+
T?: string
|
|
35
|
+
type?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RawEimzoClient {
|
|
39
|
+
API_KEYS: string[]
|
|
40
|
+
|
|
41
|
+
checkVersion(
|
|
42
|
+
success: (major: number, minor: number) => void,
|
|
43
|
+
fail: (error: unknown, message?: unknown) => void,
|
|
44
|
+
): void
|
|
45
|
+
|
|
46
|
+
installApiKeys(success: () => void, fail: (error: unknown) => void): void
|
|
47
|
+
|
|
48
|
+
idCardIsPLuggedIn(
|
|
49
|
+
success: (plugged: boolean) => void,
|
|
50
|
+
fail: (error: unknown) => void,
|
|
51
|
+
): void
|
|
52
|
+
|
|
53
|
+
listAllUserKeys(
|
|
54
|
+
itemIdGen: (cert: RawCert, index: number) => string,
|
|
55
|
+
itemUiGen: (id: string, cert: RawCert) => unknown,
|
|
56
|
+
success: (items: unknown[], firstId: string | null) => void,
|
|
57
|
+
fail: (error: unknown, reason?: unknown) => void,
|
|
58
|
+
): void
|
|
59
|
+
|
|
60
|
+
loadKey(
|
|
61
|
+
cert: RawCert,
|
|
62
|
+
success: (id: string) => void,
|
|
63
|
+
fail: (error: unknown) => void,
|
|
64
|
+
): void
|
|
65
|
+
}
|