@gerege-systems/eid-mongolia-sdk 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/dist/index.js ADDED
@@ -0,0 +1,693 @@
1
+ // src/errors.ts
2
+ var EidError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var AuthenticationError = class extends EidError {
9
+ };
10
+ var ForbiddenError = class extends EidError {
11
+ };
12
+ var ApiError = class extends EidError {
13
+ constructor(status, body) {
14
+ super(`RP-API ${status}: ${body}`);
15
+ this.status = status;
16
+ this.body = body;
17
+ }
18
+ status;
19
+ body;
20
+ };
21
+ var NetworkError = class extends EidError {
22
+ };
23
+ var SessionFailedError = class extends EidError {
24
+ constructor(endResult) {
25
+ super(`Session \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${endResult}`);
26
+ this.endResult = endResult;
27
+ }
28
+ endResult;
29
+ };
30
+ var ValidationError = class extends EidError {
31
+ };
32
+
33
+ // src/http.ts
34
+ var DEFAULT_TIMEOUT_MS = 15e3;
35
+ var Http = class {
36
+ constructor(cfg) {
37
+ this.cfg = cfg;
38
+ }
39
+ cfg;
40
+ post(path, body, timeoutMs) {
41
+ return this.request("POST", path, body, timeoutMs);
42
+ }
43
+ get(path, timeoutMs) {
44
+ return this.request("GET", path, void 0, timeoutMs);
45
+ }
46
+ async request(method, path, body, timeoutMs) {
47
+ const f = this.cfg.fetchImpl ?? fetch;
48
+ const url = this.cfg.baseUrl + path;
49
+ const init = {
50
+ method,
51
+ headers: {
52
+ Authorization: `Bearer ${this.cfg.apiSecret}`,
53
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
54
+ },
55
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
56
+ signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
57
+ };
58
+ if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
59
+ let res;
60
+ try {
61
+ res = await f(url, init);
62
+ } catch (e) {
63
+ const err = e;
64
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
65
+ throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${method} ${path}`);
66
+ }
67
+ throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${method} ${path}): ${err.message}`);
68
+ }
69
+ const text = await res.text();
70
+ if (res.status === 401) throw new AuthenticationError("API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401)");
71
+ if (res.status === 403) throw new ForbiddenError("IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)");
72
+ if (res.status >= 300) throw new ApiError(res.status, text);
73
+ return text ? JSON.parse(text) : {};
74
+ }
75
+ };
76
+
77
+ // src/crypto.ts
78
+ import { createHash, randomBytes } from "crypto";
79
+ function sha256Base64(data) {
80
+ return createHash("sha256").update(data).digest("base64");
81
+ }
82
+ function randomChallenge(bytes = 64) {
83
+ return randomBytes(bytes).toString("base64");
84
+ }
85
+ function nodeHash(h) {
86
+ return h.toLowerCase().replace("-", "");
87
+ }
88
+ function hashLen(h) {
89
+ return createHash(nodeHash(h)).digest().length;
90
+ }
91
+ function interactionsToBase64(interactions) {
92
+ return Buffer.from(JSON.stringify(interactions), "utf8").toString("base64");
93
+ }
94
+ var b64 = (s) => Buffer.from(s, "utf8").toString("base64");
95
+ function buildAcspV2Payload(p) {
96
+ const message = [
97
+ p.schemeName ?? "smart-id",
98
+ "ACSP_V2",
99
+ p.serverRandom,
100
+ p.rpChallenge,
101
+ p.userChallenge,
102
+ b64(p.relyingPartyName),
103
+ b64(p.brokeredRpName ?? ""),
104
+ createHash("sha256").update(p.interactions, "utf8").digest("base64"),
105
+ p.interactionTypeUsed,
106
+ p.initialCallbackUrl ?? "",
107
+ p.flowType
108
+ ].join("|");
109
+ return Buffer.from(message, "utf8");
110
+ }
111
+ function acspV2Digest(p, hashAlgorithm) {
112
+ return createHash(nodeHash(hashAlgorithm)).update(buildAcspV2Payload(p)).digest();
113
+ }
114
+
115
+ // src/auth.ts
116
+ function rec(v) {
117
+ return v && typeof v === "object" ? v : {};
118
+ }
119
+ var AuthApi = class {
120
+ constructor(http, creds, defaults) {
121
+ this.http = http;
122
+ this.creds = creds;
123
+ this.defaults = defaults;
124
+ }
125
+ http;
126
+ creds;
127
+ defaults;
128
+ /** v3 nested body + validator-т хэрэгтэй контекст (ижил утгуудаас — байт зөрөхгүй). */
129
+ baseBody(rpChallenge, interactions, flows, opts) {
130
+ const callback = opts?.callbackUrl?.trim() ?? "";
131
+ const acsp = {
132
+ rpChallenge,
133
+ relyingPartyName: this.creds.rpName,
134
+ brokeredRpName: "",
135
+ interactions: interactionsToBase64(interactions),
136
+ initialCallbackUrl: callback,
137
+ flowTypes: callback ? [...flows, "App2App", "Web2App"] : flows,
138
+ signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
139
+ hashAlgorithm: opts?.hashAlgorithm ?? "SHA-512"
140
+ };
141
+ const body = {
142
+ relyingPartyUUID: this.creds.rpUUID,
143
+ relyingPartyName: acsp.relyingPartyName,
144
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
145
+ signatureProtocol: "ACSP_V2",
146
+ signatureProtocolParameters: {
147
+ rpChallenge,
148
+ signatureAlgorithm: acsp.signatureAlgorithm,
149
+ signatureAlgorithmParameters: { hashAlgorithm: acsp.hashAlgorithm }
150
+ },
151
+ interactions: acsp.interactions
152
+ };
153
+ if (callback) body.initialCallbackUrl = callback;
154
+ if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
155
+ return { body, acsp };
156
+ }
157
+ parseNotification(raw, acsp) {
158
+ const r = rec(raw);
159
+ const vc = rec(r.vc).value;
160
+ return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: acsp.rpChallenge, acsp };
161
+ }
162
+ parseDeviceLink(raw, acsp) {
163
+ const r = rec(raw);
164
+ return {
165
+ sessionId: String(r.sessionID),
166
+ sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
167
+ sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
168
+ deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
169
+ rpChallenge: acsp.rpChallenge,
170
+ acsp
171
+ };
172
+ }
173
+ /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
174
+ async notificationByEtsi(id, interactions, opts) {
175
+ const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
176
+ const raw = await this.http.post(`/authentication/notification/etsi/${encodeURIComponent(id)}`, body);
177
+ return this.parseNotification(raw, acsp);
178
+ }
179
+ /** Push — баримтын дугаараар (тодорхой төхөөрөмж рүү). */
180
+ async notificationByDocument(documentNumber, interactions, opts) {
181
+ const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
182
+ const raw = await this.http.post(`/authentication/notification/document/${encodeURIComponent(documentNumber)}`, body);
183
+ return this.parseNotification(raw, acsp);
184
+ }
185
+ /** QR/App2App — нэр томьёололгүй (иргэн хэн болохыг урьдчилж мэдэхгүй). QR код үүсгэхэд. */
186
+ async deviceLinkAnonymous(interactions, opts) {
187
+ const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
188
+ const raw = await this.http.post(`/authentication/device-link/anonymous`, body);
189
+ return this.parseDeviceLink(raw, acsp);
190
+ }
191
+ /** QR/App2App — тодорхой иргэн (РД/civil_id/ETSI). */
192
+ async deviceLinkByEtsi(id, interactions, opts) {
193
+ const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
194
+ const raw = await this.http.post(`/authentication/device-link/etsi/${encodeURIComponent(id)}`, body);
195
+ return this.parseDeviceLink(raw, acsp);
196
+ }
197
+ };
198
+
199
+ // src/sign.ts
200
+ function rec2(v) {
201
+ return v && typeof v === "object" ? v : {};
202
+ }
203
+ var SignApi = class {
204
+ constructor(http, creds, defaults) {
205
+ this.http = http;
206
+ this.creds = creds;
207
+ this.defaults = defaults;
208
+ }
209
+ http;
210
+ creds;
211
+ defaults;
212
+ body(digestB64, interactions, opts) {
213
+ const body = {
214
+ relyingPartyUUID: this.creds.rpUUID,
215
+ relyingPartyName: this.creds.rpName,
216
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
217
+ signatureProtocol: "RAW_DIGEST_SIGNATURE",
218
+ signatureProtocolParameters: {
219
+ digest: digestB64,
220
+ signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
221
+ signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
222
+ },
223
+ interactions: interactionsToBase64(interactions)
224
+ };
225
+ if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
226
+ if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
227
+ return body;
228
+ }
229
+ parse(raw) {
230
+ const r = rec2(raw);
231
+ const vc = rec2(r.vc).value;
232
+ return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: "" };
233
+ }
234
+ /**
235
+ * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
236
+ * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
237
+ */
238
+ async digestByEtsi(id, digestB64, interactions, opts) {
239
+ const raw = await this.http.post(`/signature/notification/etsi/${encodeURIComponent(id)}`, this.body(digestB64, interactions, opts));
240
+ return this.parse(raw);
241
+ }
242
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
243
+ async digestByDocument(documentNumber, digestB64, interactions, opts) {
244
+ const raw = await this.http.post(`/signature/notification/document/${encodeURIComponent(documentNumber)}`, this.body(digestB64, interactions, opts));
245
+ return this.parse(raw);
246
+ }
247
+ /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
248
+ async textByEtsi(id, text, interactions, opts) {
249
+ return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
250
+ }
251
+ };
252
+
253
+ // src/session.ts
254
+ var SERVER_POLL_MS = 3e4;
255
+ var MAX_WAIT_MS = 15e4;
256
+ function asRecord(v) {
257
+ return v && typeof v === "object" ? v : {};
258
+ }
259
+ function str(v) {
260
+ return v != null ? String(v) : null;
261
+ }
262
+ function parseSessionResult(raw) {
263
+ const r = asRecord(raw);
264
+ const result = asRecord(r.result);
265
+ const sig = asRecord(r.signature);
266
+ const cert = asRecord(r.cert);
267
+ return {
268
+ state: String(r.state ?? ""),
269
+ endResult: str(result.endResult),
270
+ documentNumber: str(result.documentNumber),
271
+ certificateDerB64: str(cert.value),
272
+ certificateLevel: str(cert.certificateLevel),
273
+ signatureValueB64: str(sig.value),
274
+ signatureAlgorithm: str(sig.signatureAlgorithm),
275
+ signatureProtocol: str(r.signatureProtocol ?? sig.signatureProtocol),
276
+ serverRandom: str(sig.serverRandom),
277
+ userChallenge: str(sig.userChallenge),
278
+ flowType: str(sig.flowType),
279
+ signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
280
+ interactionTypeUsed: str(r.interactionTypeUsed),
281
+ onBehalfOf: parseOrg(r.onBehalfOf)
282
+ };
283
+ }
284
+ function parseSigParams(raw) {
285
+ const p = asRecord(raw);
286
+ const hash = str(p.hashAlgorithm);
287
+ if (!hash) return null;
288
+ const mgf = asRecord(p.maskGenAlgorithm);
289
+ return {
290
+ hashAlgorithm: hash,
291
+ maskGenAlgorithm: mgf.algorithm ? { algorithm: String(mgf.algorithm), parameters: { hashAlgorithm: String(asRecord(mgf.parameters).hashAlgorithm ?? hash) } } : void 0,
292
+ saltLength: p.saltLength != null ? Number(p.saltLength) : void 0,
293
+ trailerField: str(p.trailerField) ?? void 0
294
+ };
295
+ }
296
+ function parseOrg(raw) {
297
+ const o = asRecord(raw);
298
+ const etsi = str(o.orgEtsi);
299
+ if (!etsi) return null;
300
+ return {
301
+ orgEtsi: etsi,
302
+ orgName: str(o.orgName),
303
+ role: str(o.role),
304
+ rightType: str(o.rightType)
305
+ };
306
+ }
307
+ var SessionApi = class {
308
+ constructor(http) {
309
+ this.http = http;
310
+ }
311
+ http;
312
+ /** Нэг удаагийн long-poll. serverPollMs хүртэл хүлээнэ. */
313
+ async poll(sessionId, serverPollMs = SERVER_POLL_MS) {
314
+ const raw = await this.http.get(
315
+ `/session/${encodeURIComponent(sessionId)}?timeoutMs=${serverPollMs}`,
316
+ serverPollMs + 1e4
317
+ // HTTP timeout нь серверийн poll-оос урт
318
+ );
319
+ return parseSessionResult(raw);
320
+ }
321
+ /**
322
+ * COMPLETE болтол давтаж poll хийнэ. maxWaitMs дуустал хүлээгээд гараагүй бол сүүлчийн
323
+ * (RUNNING) үр дүнг буцаана — дуудагч endResult==null-ийг TIMEOUT мэт боловсруулна.
324
+ */
325
+ async waitForResult(sessionId, opts) {
326
+ const deadline = Date.now() + (opts?.maxWaitMs ?? MAX_WAIT_MS);
327
+ const pollMs = opts?.serverPollMs ?? SERVER_POLL_MS;
328
+ let last = { state: "RUNNING" };
329
+ while (Date.now() < deadline) {
330
+ const remaining = deadline - Date.now();
331
+ last = await this.poll(sessionId, Math.max(1e3, Math.min(pollMs, remaining)));
332
+ if (last.state === "COMPLETE") return last;
333
+ }
334
+ return last;
335
+ }
336
+ };
337
+
338
+ // src/validator.ts
339
+ import { X509Certificate, constants, createHash as createHash2, publicDecrypt, verify } from "crypto";
340
+ var ResponseValidator = class {
341
+ anchors;
342
+ intermediates;
343
+ requiredLevel;
344
+ skew;
345
+ allowUntrusted;
346
+ revocation;
347
+ revocationMode;
348
+ constructor(trust = {}) {
349
+ this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate(p));
350
+ this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate(p));
351
+ this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
352
+ this.skew = trust.clockSkewMs ?? 0;
353
+ this.allowUntrusted = trust.allowUntrusted ?? false;
354
+ this.revocation = trust.revocation;
355
+ this.revocationMode = trust.revocationMode ?? "hard-fail";
356
+ }
357
+ // ── нийтийн API ──
358
+ /**
359
+ * Auth session-ийн хариуг бүрэн шалгаж, баталгаажсан иргэнийг буцаана.
360
+ * @param acsp — session эхлүүлэхэд SDK-ийн буцаасан `acsp` контекст (RP хадгалсан).
361
+ */
362
+ validateAuth(result, acsp) {
363
+ const leaf = this.precheck(result, "ACSP_V2");
364
+ const { serverRandom, userChallenge, flowType, interactionTypeUsed } = result;
365
+ if (!serverRandom || Buffer.from(serverRandom, "base64").length < 18) throw new ValidationError("serverRandom \u0430\u043B\u0433\u0430/\u0431\u043E\u0433\u0438\u043D\u043E");
366
+ if (!userChallenge || !/^[A-Za-z0-9_-]{43}$/.test(userChallenge)) throw new ValidationError("userChallenge \u0431\u0443\u0440\u0443\u0443 \u0445\u044D\u043B\u0431\u044D\u0440\u0442\u044D\u0439");
367
+ if (!flowType || !acsp.flowTypes.includes(flowType)) {
368
+ throw new ValidationError(`flowType ${flowType} \u043D\u044C \u0441\u0430\u043D\u0430\u043B \u0431\u043E\u043B\u0433\u043E\u0441\u043E\u043D \u0443\u0440\u0441\u0433\u0430\u043B\u0434 \u0431\u0430\u0439\u0445\u0433\u04AF\u0439`);
369
+ }
370
+ if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
371
+ const { alg, hash } = this.algorithmOf(result, acsp.signatureAlgorithm, acsp.hashAlgorithm);
372
+ const payload = buildAcspV2Payload({
373
+ serverRandom,
374
+ rpChallenge: acsp.rpChallenge,
375
+ userChallenge,
376
+ relyingPartyName: acsp.relyingPartyName,
377
+ brokeredRpName: acsp.brokeredRpName,
378
+ interactions: acsp.interactions,
379
+ interactionTypeUsed,
380
+ initialCallbackUrl: acsp.initialCallbackUrl,
381
+ flowType
382
+ });
383
+ if (!verifyPayloadSignature(payload, result.signatureValueB64, leaf.publicKey, alg, hash)) {
384
+ throw new ValidationError("Auth \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 ACSP_V2 payload-\u0442\u043E\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439 (\u0445\u0443\u0443\u0440\u0430\u043C\u0447 \u0445\u0430\u0440\u0438\u0443 \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)");
385
+ }
386
+ return {
387
+ documentNumber: result.documentNumber,
388
+ certificate: leaf,
389
+ subject: leaf.subject,
390
+ certificateLevel: String(result.certificateLevel ?? "")
391
+ };
392
+ }
393
+ /**
394
+ * Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
395
+ * @param hashAlgorithm — digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256).
396
+ */
397
+ validateSign(result, digestB64, hashAlgorithm = "SHA-256") {
398
+ const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
399
+ const digest = Buffer.from(digestB64, "base64");
400
+ const { alg, hash } = this.algorithmOf(result, null, hashAlgorithm);
401
+ if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
402
+ if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
403
+ throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u04E9\u0433\u0441\u04E9\u043D digest-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
404
+ }
405
+ return {
406
+ documentNumber: result.documentNumber,
407
+ signatureValueB64: result.signatureValueB64,
408
+ signatureAlgorithm: result.signatureAlgorithm,
409
+ certificate: leaf,
410
+ subject: leaf.subject
411
+ };
412
+ }
413
+ /**
414
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
415
+ * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
416
+ */
417
+ algorithmOf(result, wantAlg, wantHash) {
418
+ const alg = result.signatureAlgorithm ?? "";
419
+ let hash;
420
+ if (alg === "rsassa-pss") {
421
+ hash = String(result.signatureAlgorithmParameters?.hashAlgorithm ?? "");
422
+ const mgf = result.signatureAlgorithmParameters?.maskGenAlgorithm?.parameters?.hashAlgorithm;
423
+ if (mgf && mgf !== hash) throw new ValidationError("MGF1 hash \u043D\u044C hashAlgorithm-\u0430\u0430\u0441 \u04E9\u04E9\u0440");
424
+ if (result.signatureAlgorithmParameters?.saltLength != null && result.signatureAlgorithmParameters.saltLength !== hashLen(hash)) {
425
+ throw new ValidationError("saltLength \u2260 hLen");
426
+ }
427
+ } else if (/^sha(256|384|512)WithRSAEncryption$/.test(alg)) {
428
+ hash = "SHA-" + alg.slice(3, 6);
429
+ } else if (alg === "ecdsa-with-SHA256") {
430
+ hash = "SHA-256";
431
+ } else {
432
+ throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 signatureAlgorithm: ${alg}`);
433
+ }
434
+ if (wantAlg && alg !== wantAlg && alg !== "ecdsa-with-SHA256") {
435
+ throw new ValidationError(`signatureAlgorithm ${alg} \u2260 \u0445\u04AF\u0441\u0441\u044D\u043D ${wantAlg}`);
436
+ }
437
+ if (hash !== wantHash) throw new ValidationError(`hashAlgorithm ${hash} \u2260 \u0445\u04AF\u0441\u0441\u044D\u043D ${wantHash}`);
438
+ return { alg, hash };
439
+ }
440
+ // ── дотоод ──
441
+ /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
442
+ precheck(result, protocol) {
443
+ if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
444
+ if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
445
+ if (result.signatureProtocol && result.signatureProtocol !== protocol) {
446
+ throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
447
+ }
448
+ if (!result.certificateDerB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
449
+ if (!result.signatureValueB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0430\u043B\u0433\u0430");
450
+ if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
451
+ const leaf = new X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
452
+ this.checkValidity(leaf);
453
+ this.checkLevel(String(result.certificateLevel ?? ""));
454
+ if (this.anchors.length === 0) {
455
+ if (!this.allowUntrusted) {
456
+ throw new ValidationError(
457
+ "Trust anchor \u0442\u043E\u0445\u0438\u0440\u0443\u0443\u043B\u0430\u0430\u0433\u04AF\u0439 \u2014 \u0438\u0442\u0433\u044D\u043B\u0446\u044D\u043B \u0448\u0430\u043B\u0433\u0430\u0445 \u0431\u043E\u043B\u043E\u043C\u0436\u0433\u04AF\u0439 (production-\u0434 trustAnchorsPem \u04E9\u0433, \u044D\u0441\u0432\u044D\u043B allowUntrusted=true)"
458
+ );
459
+ }
460
+ } else {
461
+ this.verifyChain(leaf);
462
+ }
463
+ return leaf;
464
+ }
465
+ checkValidity(cert) {
466
+ const now = Date.now();
467
+ const from = Date.parse(cert.validFrom);
468
+ const to = Date.parse(cert.validTo);
469
+ if (!Number.isNaN(from) && now + this.skew < from) throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0445\u04AF\u0447\u0438\u043D\u0442\u044D\u0439 \u0431\u043E\u043B\u043E\u043E\u0433\u04AF\u0439 \u0431\u0430\u0439\u043D\u0430");
470
+ if (!Number.isNaN(to) && now - this.skew > to) throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D");
471
+ }
472
+ checkLevel(level) {
473
+ const rank = { ADVANCED: 1, QUALIFIED: 2, QSCD: 3 };
474
+ const got = rank[level] ?? 0;
475
+ const need = rank[this.requiredLevel] ?? 2;
476
+ if (got < need) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u0432\u0448\u0438\u043D \u0445\u04AF\u0440\u044D\u043B\u0446\u044D\u0445\u0433\u04AF\u0439: ${level} < ${this.requiredLevel}`);
477
+ }
478
+ /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
479
+ verifyChain(leaf) {
480
+ let current = leaf;
481
+ const pool = [...this.intermediates, ...this.anchors];
482
+ for (let depth = 0; depth < 8; depth++) {
483
+ for (const anchor of this.anchors) {
484
+ if (isCa(anchor) && current.checkIssued(anchor) && current.verify(anchor.publicKey)) return;
485
+ }
486
+ const issuer = pool.find(
487
+ (c) => isCa(c) && current.checkIssued(c) && current.verify(c.publicKey)
488
+ );
489
+ if (!issuer) break;
490
+ current = issuer;
491
+ }
492
+ throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439");
493
+ }
494
+ /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
495
+ findIssuer(leaf) {
496
+ return [...this.intermediates, ...this.anchors].find(
497
+ (c) => isCa(c) && leaf.checkIssued(c) && leaf.verify(c.publicKey)
498
+ );
499
+ }
500
+ /**
501
+ * checkRevocation — тохируулсан RevocationChecker hook-оор cert-ийн revoke төлвийг
502
+ * шалгана. revocation тохируулаагүй бол no-op (true буцаана). "revoked" үед, мөн
503
+ * hard-fail горимд "unknown" үед ValidationError шиднэ. validateAuth/Sign-ийн дараа
504
+ * дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
505
+ */
506
+ async checkRevocation(cert) {
507
+ if (!this.revocation) return;
508
+ const status = await this.revocation(cert, this.findIssuer(cert));
509
+ if (status === "revoked") {
510
+ throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
511
+ }
512
+ if (status === "unknown" && this.revocationMode === "hard-fail") {
513
+ throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 revocation \u0442\u04E9\u043B\u04E9\u0432 \u0442\u043E\u0433\u0442\u043E\u043E\u0433\u0434\u0441\u043E\u043D\u0433\u04AF\u0439 (hard-fail)");
514
+ }
515
+ }
516
+ };
517
+ function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
518
+ const sig = Buffer.from(sigB64, "base64");
519
+ const h = nodeHash(hash);
520
+ if (alg === "rsassa-pss") {
521
+ return verify(h, payload, { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
522
+ }
523
+ if (alg === "ecdsa-with-SHA256") return verify(h, payload, { key, dsaEncoding: "der" }, sig);
524
+ return verify(h, payload, { key, padding: constants.RSA_PKCS1_PADDING }, sig);
525
+ }
526
+ function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
527
+ const sig = Buffer.from(sigB64, "base64");
528
+ if (alg === "ecdsa-with-SHA256") return ecdsaP256VerifyPrehashed(digest, sig, key);
529
+ const modBits = key.asymmetricKeyDetails?.modulusLength ?? 0;
530
+ if (!modBits || sig.length !== Math.ceil(modBits / 8)) return false;
531
+ let em;
532
+ try {
533
+ em = publicDecrypt({ key, padding: constants.RSA_NO_PADDING }, sig);
534
+ } catch {
535
+ return false;
536
+ }
537
+ if (alg === "rsassa-pss") return emsaPssVerify(digest, em, modBits - 1, hash);
538
+ const prefix = DIGEST_INFO[hash];
539
+ if (!prefix) return false;
540
+ const t = Buffer.concat([prefix, digest]);
541
+ const expected = Buffer.concat([Buffer.from([0, 1]), Buffer.alloc(em.length - t.length - 3, 255), Buffer.from([0]), t]);
542
+ return em.length === expected.length && em.equals(expected);
543
+ }
544
+ var P256 = {
545
+ p: 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn,
546
+ n: 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n,
547
+ b: 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn,
548
+ gx: 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296n,
549
+ gy: 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5n
550
+ };
551
+ var mod = (a, m) => (a % m + m) % m;
552
+ function inv(a, m) {
553
+ let [g, x, y, u] = [mod(a, m), 1n, 0n, m];
554
+ while (g !== 0n) {
555
+ const q = u / g;
556
+ [u, g, y, x] = [g, u - q * g, x, y - q * x];
557
+ }
558
+ return mod(y, m);
559
+ }
560
+ function ptAdd(P, Q) {
561
+ if (!P) return Q;
562
+ if (!Q) return P;
563
+ const { p } = P256;
564
+ let l;
565
+ if (P[0] === Q[0]) {
566
+ if (mod(P[1] + Q[1], p) === 0n) return null;
567
+ l = mod((3n * P[0] * P[0] - 3n) * inv(2n * P[1], p), p);
568
+ } else {
569
+ l = mod((Q[1] - P[1]) * inv(Q[0] - P[0], p), p);
570
+ }
571
+ const x = mod(l * l - P[0] - Q[0], p);
572
+ return [x, mod(l * (P[0] - x) - P[1], p)];
573
+ }
574
+ function ptMul(k, P) {
575
+ let R = null;
576
+ for (let b = P; k > 0n; k >>= 1n, b = ptAdd(b, b)) if (k & 1n) R = ptAdd(R, b);
577
+ return R;
578
+ }
579
+ function derInts(sig) {
580
+ if (sig[0] !== 48 || sig[1] !== sig.length - 2) return null;
581
+ let i = 2;
582
+ const out = [];
583
+ for (let k = 0; k < 2; k++) {
584
+ if (sig[i] !== 2) return null;
585
+ const len = sig[i + 1] ?? 0;
586
+ out.push(BigInt("0x" + (sig.subarray(i + 2, i + 2 + len).toString("hex") || "0")));
587
+ i += 2 + len;
588
+ }
589
+ return i === sig.length ? [out[0], out[1]] : null;
590
+ }
591
+ function ecdsaP256VerifyPrehashed(digest, sig, key) {
592
+ if (key.asymmetricKeyDetails?.namedCurve !== "prime256v1") return false;
593
+ const jwk = key.export({ format: "jwk" });
594
+ if (!jwk.x || !jwk.y) return false;
595
+ const Q = [BigInt("0x" + Buffer.from(jwk.x, "base64url").toString("hex")), BigInt("0x" + Buffer.from(jwk.y, "base64url").toString("hex"))];
596
+ const rs = derInts(sig);
597
+ if (!rs) return false;
598
+ const [r, sv] = rs;
599
+ const { n } = P256;
600
+ if (r <= 0n || r >= n || sv <= 0n || sv >= n) return false;
601
+ const z = BigInt("0x" + digest.subarray(0, 32).toString("hex"));
602
+ const w = inv(sv, n);
603
+ const R = ptAdd(ptMul(mod(z * w, n), [P256.gx, P256.gy]), ptMul(mod(r * w, n), Q));
604
+ return R !== null && mod(R[0], n) === r;
605
+ }
606
+ var DIGEST_INFO = {
607
+ "SHA-256": Buffer.from("3031300d060960864801650304020105000420", "hex"),
608
+ "SHA-384": Buffer.from("3041300d060960864801650304020205000430", "hex"),
609
+ "SHA-512": Buffer.from("3051300d060960864801650304020305000440", "hex")
610
+ };
611
+ function mgf1(hash, seed, length) {
612
+ const out = [];
613
+ for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
614
+ const c = Buffer.alloc(4);
615
+ c.writeUInt32BE(i, 0);
616
+ out.push(createHash2(nodeHash(hash)).update(seed).update(c).digest());
617
+ }
618
+ return Buffer.concat(out).subarray(0, length);
619
+ }
620
+ function emsaPssVerify(mHash, em, emBits, hash) {
621
+ const hLen = hashLen(hash);
622
+ const sLen = hLen;
623
+ const emLen = Math.ceil(emBits / 8);
624
+ if (em.length > emLen) {
625
+ if (!em.subarray(0, em.length - emLen).every((b) => b === 0)) return false;
626
+ em = em.subarray(em.length - emLen);
627
+ }
628
+ if (emLen < hLen + sLen + 2 || em[emLen - 1] !== 188) return false;
629
+ const maskedDB = em.subarray(0, emLen - hLen - 1);
630
+ const h = em.subarray(emLen - hLen - 1, emLen - 1);
631
+ const topBits = 8 * emLen - emBits;
632
+ if (topBits > 0 && (maskedDB[0] ?? 0) >> 8 - topBits !== 0) return false;
633
+ const db = Buffer.from(maskedDB);
634
+ const mask = mgf1(hash, h, db.length);
635
+ for (let i = 0; i < db.length; i++) db[i] = (db[i] ?? 0) ^ (mask[i] ?? 0);
636
+ if (topBits > 0) db[0] = (db[0] ?? 0) & 255 >> topBits;
637
+ const psLen = emLen - hLen - sLen - 2;
638
+ if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
639
+ const salt = db.subarray(psLen + 1);
640
+ const h2 = createHash2(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
641
+ return h.equals(h2);
642
+ }
643
+ function isCa(cert) {
644
+ return cert.ca === true;
645
+ }
646
+
647
+ // src/client.ts
648
+ var EidClient = class {
649
+ auth;
650
+ sign;
651
+ session;
652
+ validator;
653
+ constructor(cfg) {
654
+ if (!cfg.baseUrl) throw new Error("EidClient: baseUrl \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
655
+ if (!cfg.credentials?.apiSecret) throw new Error("EidClient: credentials.apiSecret \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
656
+ const base = cfg.baseUrl.replace(/\/+$/, "") + "/v3";
657
+ const http = new Http({
658
+ baseUrl: base,
659
+ apiSecret: cfg.credentials.apiSecret,
660
+ timeoutMs: cfg.timeoutMs,
661
+ dispatcher: cfg.dispatcher,
662
+ fetchImpl: cfg.fetchImpl
663
+ });
664
+ const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
665
+ this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
666
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
667
+ this.session = new SessionApi(http);
668
+ this.validator = new ResponseValidator(cfg.trust ?? {});
669
+ }
670
+ };
671
+ export {
672
+ ApiError,
673
+ AuthApi,
674
+ AuthenticationError,
675
+ EidClient,
676
+ EidError,
677
+ ForbiddenError,
678
+ Http,
679
+ NetworkError,
680
+ ResponseValidator,
681
+ SessionApi,
682
+ SessionFailedError,
683
+ SignApi,
684
+ ValidationError,
685
+ acspV2Digest,
686
+ buildAcspV2Payload,
687
+ interactionsToBase64,
688
+ parseSessionResult,
689
+ randomChallenge,
690
+ sha256Base64,
691
+ verifyPayloadSignature,
692
+ verifyPrehashedSignature
693
+ };