@gerege-systems/eid-mongolia-sdk 0.9.1 → 0.11.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 CHANGED
@@ -6,15 +6,20 @@ import {
6
6
  ForbiddenError,
7
7
  NetworkError,
8
8
  ON_BEHALF_ERROR_CODES,
9
+ RETRYABLE_ERROR_CODES,
10
+ ResponseTooLargeError,
9
11
  SessionFailedError,
10
12
  ValidationError,
11
13
  deviceLink,
12
14
  isMobileUserAgent,
13
- openOrShowQR
14
- } from "./chunk-NXDKQZIW.js";
15
+ maskPath,
16
+ openOrShowQR,
17
+ parseRetryAfter
18
+ } from "./chunk-QPJDZLHR.js";
15
19
 
16
20
  // src/http.ts
17
21
  var DEFAULT_TIMEOUT_MS = 15e3;
22
+ var MAX_JSON_BYTES = 4 << 20;
18
23
  var Http = class {
19
24
  constructor(cfg) {
20
25
  this.cfg = cfg;
@@ -30,14 +35,14 @@ var Http = class {
30
35
  postMultipart(path, form, timeoutMs) {
31
36
  return this.json("POST", path, form, void 0, timeoutMs);
32
37
  }
33
- /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
34
- async getBytes(path, timeoutMs) {
38
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
39
+ async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
35
40
  const res = await this.send("GET", path, void 0, void 0, timeoutMs);
36
- return new Uint8Array(await res.arrayBuffer());
41
+ return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
37
42
  }
38
43
  async json(method, path, body, contentType, timeoutMs) {
39
44
  const res = await this.send(method, path, body, contentType, timeoutMs);
40
- const text = await res.text();
45
+ const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
41
46
  return text ? JSON.parse(text) : {};
42
47
  }
43
48
  async send(method, path, body, contentType, timeoutMs) {
@@ -54,28 +59,62 @@ var Http = class {
54
59
  };
55
60
  if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
56
61
  let res;
62
+ const op = `${method} ${maskPath(path)}`;
57
63
  try {
58
64
  res = await f(url, init);
59
65
  } catch (e) {
60
66
  const err = e;
61
67
  if (err.name === "TimeoutError" || err.name === "AbortError") {
62
- 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}`);
68
+ throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${op}`, { op, timeout: true });
63
69
  }
64
- throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${method} ${path}): ${err.message}`);
70
+ throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${op}): ${err.message.split(url).join(op)}`, { op, timeout: false });
65
71
  }
66
72
  if (res.status < 300) return res;
67
73
  const text = await res.text();
68
74
  const { error, code } = errorBody(text);
69
- 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)");
75
+ const headers = res.headers;
76
+ const retryAfterMs = parseRetryAfter(headers.get("Retry-After"));
77
+ if (res.status === 401) {
78
+ throw new AuthenticationError(`API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401${code ? ` ${code}` : ""})`, { code, serverMessage: error, body: text, headers });
79
+ }
70
80
  if (res.status === 403) {
71
81
  throw new ForbiddenError(
72
82
  error ? `RP-API 403${code ? ` ${code}` : ""}: ${error}` : "IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)",
73
- code
83
+ code,
84
+ { headers, retryAfterMs }
74
85
  );
75
86
  }
76
- throw new ApiError(res.status, text, code);
87
+ throw new ApiError(res.status, text, code, { headers, retryAfterMs });
77
88
  }
78
89
  };
90
+ async function readLimited(res, limit, op) {
91
+ const declared = Number(res.headers.get("content-length") ?? NaN);
92
+ if (declared > limit) {
93
+ await res.body?.cancel().catch(() => void 0);
94
+ throw new ResponseTooLargeError(op, limit);
95
+ }
96
+ if (!res.body) return new Uint8Array(0);
97
+ const reader = res.body.getReader();
98
+ const chunks = [];
99
+ let total = 0;
100
+ for (; ; ) {
101
+ const { done, value } = await reader.read();
102
+ if (done) break;
103
+ total += value.byteLength;
104
+ if (total > limit) {
105
+ await reader.cancel().catch(() => void 0);
106
+ throw new ResponseTooLargeError(op, limit);
107
+ }
108
+ chunks.push(value);
109
+ }
110
+ const out = new Uint8Array(total);
111
+ let off = 0;
112
+ for (const c of chunks) {
113
+ out.set(c, off);
114
+ off += c.byteLength;
115
+ }
116
+ return out;
117
+ }
79
118
  function errorBody(text) {
80
119
  try {
81
120
  const v = JSON.parse(text);
@@ -90,6 +129,51 @@ function errorBody(text) {
90
129
  }
91
130
  }
92
131
 
132
+ // src/contract.ts
133
+ var RP_ROUTES = {
134
+ authNotificationEtsi: { method: "POST", path: "/authentication/notification/etsi/{semanticsIdentifier}" },
135
+ authNotificationDocument: { method: "POST", path: "/authentication/notification/document/{documentNumber}" },
136
+ authDeviceLinkAnonymous: { method: "POST", path: "/authentication/device-link/anonymous" },
137
+ authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
138
+ signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
139
+ signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
140
+ pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
141
+ pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
142
+ orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
143
+ session: { method: "GET", path: "/session/{sessionId}" },
144
+ personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
145
+ personDevices: { method: "GET", path: "/devices/etsi/{personEtsi}" },
146
+ personActivity: { method: "GET", path: "/rp/activity/etsi/{personEtsi}" },
147
+ personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
148
+ personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
149
+ tokenCertificate: { method: "POST", path: "/token/certificate" },
150
+ tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
151
+ };
152
+ var RP_ROUTES_UNDISCOVERED = {
153
+ tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
154
+ tokenAuthVerify: { method: "POST", path: "/token/auth/verify" },
155
+ pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
156
+ pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
157
+ pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
158
+ };
159
+ function routePath(route, ...args) {
160
+ let i = 0;
161
+ const out = route.path.replace(/\{[^}]*\}/g, () => {
162
+ const a = args[i++];
163
+ if (a === void 0) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u0443\u0442\u0443\u0443`);
164
+ return encodeURIComponent(a);
165
+ });
166
+ if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
167
+ return out;
168
+ }
169
+ var OIDC_EXPECT = {
170
+ defaultScope: "openid",
171
+ pkceMethod: "S256",
172
+ idTokenAlg: "ES256",
173
+ /** `Authorization: Basic` (RFC 6749 §2.3.1). */
174
+ tokenEndpointAuthMethod: "client_secret_basic"
175
+ };
176
+
93
177
  // src/crypto.ts
94
178
  import { createHash, randomBytes } from "crypto";
95
179
  function sha256Base64(data) {
@@ -192,25 +276,25 @@ var AuthApi = class {
192
276
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
193
277
  async notificationByEtsi(id, interactions, opts) {
194
278
  const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
195
- const raw = await this.http.post(`/authentication/notification/etsi/${encodeURIComponent(id)}`, body);
279
+ const raw = await this.http.post(routePath(RP_ROUTES.authNotificationEtsi, id), body);
196
280
  return this.parseNotification(raw, acsp);
197
281
  }
198
282
  /** Push — баримтын дугаараар (тодорхой төхөөрөмж рүү). */
199
283
  async notificationByDocument(documentNumber, interactions, opts) {
200
284
  const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
201
- const raw = await this.http.post(`/authentication/notification/document/${encodeURIComponent(documentNumber)}`, body);
285
+ const raw = await this.http.post(routePath(RP_ROUTES.authNotificationDocument, documentNumber), body);
202
286
  return this.parseNotification(raw, acsp);
203
287
  }
204
288
  /** QR/App2App — нэр томьёололгүй (иргэн хэн болохыг урьдчилж мэдэхгүй). QR код үүсгэхэд. */
205
289
  async deviceLinkAnonymous(interactions, opts) {
206
290
  const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
207
- const raw = await this.http.post(`/authentication/device-link/anonymous`, body);
291
+ const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkAnonymous), body);
208
292
  return this.parseDeviceLink(raw, acsp);
209
293
  }
210
294
  /** QR/App2App — тодорхой иргэн (РД/civil_id/ETSI). */
211
295
  async deviceLinkByEtsi(id, interactions, opts) {
212
296
  const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
213
- const raw = await this.http.post(`/authentication/device-link/etsi/${encodeURIComponent(id)}`, body);
297
+ const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkEtsi, id), body);
214
298
  return this.parseDeviceLink(raw, acsp);
215
299
  }
216
300
  };
@@ -243,6 +327,7 @@ var SignApi = class {
243
327
  };
244
328
  if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
245
329
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
330
+ if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
246
331
  return body;
247
332
  }
248
333
  parse(raw) {
@@ -255,12 +340,12 @@ var SignApi = class {
255
340
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
256
341
  */
257
342
  async digestByEtsi(id, digestB64, interactions, opts) {
258
- const raw = await this.http.post(`/signature/notification/etsi/${encodeURIComponent(id)}`, this.body(digestB64, interactions, opts));
343
+ const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
259
344
  return this.parse(raw);
260
345
  }
261
346
  /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
262
347
  async digestByDocument(documentNumber, digestB64, interactions, opts) {
263
- const raw = await this.http.post(`/signature/notification/document/${encodeURIComponent(documentNumber)}`, this.body(digestB64, interactions, opts));
348
+ const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
264
349
  return this.parse(raw);
265
350
  }
266
351
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
@@ -301,9 +386,44 @@ function parseSessionResult(raw) {
301
386
  signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
302
387
  interactionTypeUsed: str(r.interactionTypeUsed),
303
388
  onBehalfOf: parseOrg(r.onBehalfOf),
304
- pdf: parsePdf(r.pdf)
389
+ pdf: parsePdf(r.pdf),
390
+ person: parsePerson(r.person),
391
+ tokenCertificate: parseTokenCert(r.tokenCertificate),
392
+ raw
305
393
  };
306
394
  }
395
+ var PERSON_STRINGS = [
396
+ "firstName",
397
+ "lastName",
398
+ "familyName",
399
+ "firstNameEn",
400
+ "lastNameEn",
401
+ "civilId",
402
+ "regNo",
403
+ "birthDate",
404
+ "gender"
405
+ ];
406
+ function parsePerson(raw) {
407
+ const p = asRecord(raw);
408
+ const out = {};
409
+ for (const k of PERSON_STRINGS) if (typeof p[k] === "string" && p[k]) out[k] = p[k];
410
+ if (typeof p.geID === "number" && p.geID !== 0) out.geID = p.geID;
411
+ return Object.keys(out).length ? out : null;
412
+ }
413
+ function parseTokenCert(raw) {
414
+ const t = asRecord(raw);
415
+ const status = str(t.status);
416
+ if (!status) return null;
417
+ const out = { status };
418
+ if (t.errorCode) out.errorCode = String(t.errorCode);
419
+ if (t.authSerialHex) out.authSerialHex = String(t.authSerialHex);
420
+ if (t.signSerialHex) out.signSerialHex = String(t.signSerialHex);
421
+ return out;
422
+ }
423
+ function personMatchesCertificate(person, subject) {
424
+ const serial = typeof subject === "string" ? subject : subject?.serialNumber;
425
+ return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
426
+ }
307
427
  function parsePdf(raw) {
308
428
  const p = asRecord(raw);
309
429
  const docID = str(p.docID);
@@ -357,7 +477,7 @@ var SessionApi = class {
357
477
  /** Нэг удаагийн long-poll. serverPollMs хүртэл хүлээнэ. */
358
478
  async poll(sessionId, serverPollMs = SERVER_POLL_MS) {
359
479
  const raw = await this.http.get(
360
- `/session/${encodeURIComponent(sessionId)}?timeoutMs=${serverPollMs}`,
480
+ `${routePath(RP_ROUTES.session, sessionId)}?timeoutMs=${serverPollMs}`,
361
481
  serverPollMs + 1e4
362
482
  // HTTP timeout нь серверийн poll-оос урт
363
483
  );
@@ -382,6 +502,8 @@ var SessionApi = class {
382
502
 
383
503
  // src/pdf.ts
384
504
  import { createHash as createHash2 } from "crypto";
505
+ var DEFAULT_PDF_MAX_BYTES = 64 << 20;
506
+ var DEFAULT_PDF_TIMEOUT_MS = 8e4;
385
507
  function rec3(v) {
386
508
  return v && typeof v === "object" ? v : {};
387
509
  }
@@ -394,7 +516,10 @@ var PdfApi = class {
394
516
  http;
395
517
  creds;
396
518
  defaults;
397
- /** POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. */
519
+ /**
520
+ * POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
521
+ * PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
522
+ */
398
523
  async prepare(input) {
399
524
  const request = {
400
525
  relyingPartyUUID: this.creds.rpUUID,
@@ -410,7 +535,7 @@ var PdfApi = class {
410
535
  const form = new FormData();
411
536
  form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
412
537
  form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
413
- const r = rec3(await this.http.postMultipart("/pdf/sign/prepare", form, 6e4));
538
+ const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
414
539
  const receivedAt = Date.now();
415
540
  const vc = rec3(r.vc).value;
416
541
  const dl = rec3(r.deviceLink);
@@ -432,10 +557,80 @@ var PdfApi = class {
432
557
  }
433
558
  /**
434
559
  * GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
435
- * session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
560
+ * session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
561
+ * бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
562
+ * дараа дахин татна (`retryable`).
436
563
  */
437
564
  document(sessionId) {
438
- return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
565
+ return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
566
+ }
567
+ get timeout() {
568
+ return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
569
+ }
570
+ get maxBytes() {
571
+ return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
572
+ }
573
+ /**
574
+ * USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
575
+ * (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
576
+ * зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
577
+ * LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
578
+ * TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
579
+ */
580
+ async tokenPrepare(input) {
581
+ if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
582
+ throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
583
+ }
584
+ const request = {
585
+ relyingPartyUUID: this.creds.rpUUID,
586
+ relyingPartyName: this.creds.rpName,
587
+ signer: { etsi: input.signerEtsi.trim() },
588
+ certificate: Buffer.from(input.certificate).toString("base64"),
589
+ fileName: input.fileName
590
+ };
591
+ if (input.docID?.trim()) request.docID = input.docID.trim();
592
+ if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
593
+ const form = new FormData();
594
+ form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
595
+ form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
596
+ const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
597
+ const r = rec3(raw);
598
+ const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
599
+ if (!r.signingID || hashToSign.length !== 32) {
600
+ throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
601
+ }
602
+ return {
603
+ signingId: String(r.signingID),
604
+ docId: String(r.docID ?? ""),
605
+ hashToSign,
606
+ hashAlgorithm: String(r.hashAlgorithm ?? ""),
607
+ signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
608
+ signatureFormat: String(r.signatureFormat ?? ""),
609
+ expiresAt: String(r.expiresAt ?? ""),
610
+ raw
611
+ };
612
+ }
613
+ /**
614
+ * POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
615
+ * Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
616
+ * Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
617
+ * TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
618
+ */
619
+ async tokenFinalize(signingId, signature) {
620
+ if (signature?.length !== 64) throw new Error("eid: pdf token: signature \u043D\u044C \u0442\u04AF\u04AF\u0445\u0438\u0439 r||s 64 \u0431\u0430\u0439\u0442 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 (P-256, DER \u0431\u0438\u0448)");
621
+ const r = await this.http.post(
622
+ routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
623
+ { signature: Buffer.from(signature).toString("base64") },
624
+ this.timeout
625
+ );
626
+ return r;
627
+ }
628
+ /**
629
+ * GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
630
+ * хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
631
+ */
632
+ tokenDocument(signingId) {
633
+ return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
439
634
  }
440
635
  /**
441
636
  * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
@@ -467,14 +662,54 @@ var OrganizationApi = class {
467
662
  http;
468
663
  /** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
469
664
  async getRepresentations(personEtsi) {
470
- const raw = await this.http.get(`/organization/representations/etsi/${encodeURIComponent(personEtsi)}`);
665
+ const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
471
666
  const list = Array.isArray(raw.representations) ? raw.representations : [];
472
- return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
667
+ return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list, raw };
473
668
  }
474
669
  };
475
670
 
476
- // src/validator.ts
477
- import { X509Certificate as X509Certificate2, constants, createHash as createHash3, publicDecrypt, verify } from "crypto";
671
+ // src/person.ts
672
+ function rec4(v) {
673
+ return v && typeof v === "object" ? v : {};
674
+ }
675
+ var PersonApi = class {
676
+ constructor(http) {
677
+ this.http = http;
678
+ }
679
+ http;
680
+ async get(path) {
681
+ const raw = await this.http.get(path);
682
+ return { ...rec4(raw), raw };
683
+ }
684
+ /** Иргэний бүх гэрчилгээ + статусын тоолол. */
685
+ certificates(personEtsi) {
686
+ return this.get(routePath(RP_ROUTES.personCertificates, personEtsi));
687
+ }
688
+ /** Иргэний холбоотой төхөөрөмжүүд (идэвхтэй + идэвхгүй). */
689
+ devices(personEtsi) {
690
+ return this.get(routePath(RP_ROUTES.personDevices, personEtsi));
691
+ }
692
+ /** Иргэний үйл ажиллагааны түүх. */
693
+ activity(personEtsi, query) {
694
+ const q = new URLSearchParams();
695
+ if (query?.flow) q.set("flow", query.flow);
696
+ if (query?.limit && query.limit > 0) q.set("limit", String(query.limit));
697
+ if (query?.offset && query.offset > 0) q.set("offset", String(query.offset));
698
+ const qs = q.toString();
699
+ return this.get(routePath(RP_ROUTES.personActivity, personEtsi) + (qs ? `?${qs}` : ""));
700
+ }
701
+ /** Самбарын нэгдсэн тоо. */
702
+ summary(personEtsi) {
703
+ return this.get(routePath(RP_ROUTES.personSummary, personEtsi));
704
+ }
705
+ /** Асран хамгаалж буй хүүхдүүд (зөвхөн унших). */
706
+ children(personEtsi) {
707
+ return this.get(routePath(RP_ROUTES.personChildren, personEtsi));
708
+ }
709
+ };
710
+
711
+ // src/token.ts
712
+ import { X509Certificate as X509Certificate2, createHash as createHash3, createPublicKey, timingSafeEqual } from "crypto";
478
713
 
479
714
  // src/subject.ts
480
715
  import { X509Certificate } from "crypto";
@@ -580,8 +815,215 @@ function parseSubject(cert) {
580
815
  out.name = full || out.commonName || null;
581
816
  return out;
582
817
  }
818
+ function certExtension(der, oid) {
819
+ return certExtensions(der).find((e) => e.oid === oid)?.value;
820
+ }
821
+ function certExtensions(der) {
822
+ const out = [];
823
+ const top = tlv(der, 0, der.length);
824
+ const tbs = tlv(der, top.start, top.end);
825
+ let f = tlv(der, tbs.start, tbs.end);
826
+ if (f.tag === 160) f = tlv(der, f.end, tbs.end);
827
+ for (let k = 0; k < 5; k++) f = tlv(der, f.end, tbs.end);
828
+ for (let pos = f.end; pos < tbs.end; ) {
829
+ const t = tlv(der, pos, tbs.end);
830
+ if (t.tag === 163) {
831
+ const seq = tlv(der, t.start, t.end);
832
+ for (let ep = seq.start; ep < seq.end; ) {
833
+ const ext = tlv(der, ep, seq.end);
834
+ const id = tlv(der, ext.start, ext.end);
835
+ let v = tlv(der, id.end, ext.end);
836
+ let critical = false;
837
+ if (v.tag === 1) {
838
+ critical = v.end > v.start && der[v.start] !== 0;
839
+ v = tlv(der, v.end, ext.end);
840
+ }
841
+ if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
842
+ out.push({ oid: oidOf(der, id), critical, value: der.subarray(v.start, v.end) });
843
+ ep = ext.end;
844
+ }
845
+ return out;
846
+ }
847
+ pos = t.end;
848
+ }
849
+ return out;
850
+ }
851
+
852
+ // src/token.ts
853
+ function rec5(v) {
854
+ return v && typeof v === "object" ? v : {};
855
+ }
856
+ function hexSha256Equal(hex, data) {
857
+ if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
858
+ return timingSafeEqual(createHash3("sha256").update(data).digest(), Buffer.from(hex, "hex"));
859
+ }
860
+ var TOKEN_RESULT_TIMEOUT_MS = 3e4;
861
+ var TokenApi = class {
862
+ constructor(http, creds) {
863
+ this.http = http;
864
+ this.creds = creds;
865
+ }
866
+ http;
867
+ creds;
868
+ /**
869
+ * POST /token/certificate — токены AUTH + SIGN гэрчилгээ олголт эхлүүлнэ (иргэн утсаараа батална). CA-ийн буцаасан
870
+ * CSR hash-ууд илгээсэнтэй таарахгүй бол ValidationError.
871
+ */
872
+ async issueCertificate(input) {
873
+ if (!input.signerEtsi?.trim() || !input.csrAuth?.length || !input.csrSign?.length || !input.tokenSerial?.trim()) {
874
+ throw new Error("token: signerEtsi, csrAuth, csrSign, tokenSerial \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
875
+ }
876
+ const body = {
877
+ relyingPartyUUID: this.creds.rpUUID,
878
+ relyingPartyName: this.creds.rpName,
879
+ signer: { etsi: input.signerEtsi.trim() },
880
+ csrAuth: Buffer.from(input.csrAuth).toString("base64"),
881
+ csrSign: Buffer.from(input.csrSign).toString("base64"),
882
+ tokenSerial: input.tokenSerial.trim(),
883
+ onBehalfOf: input.onBehalfOf?.trim() ?? "",
884
+ flow: "notification"
885
+ };
886
+ if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
887
+ const r = rec5(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
888
+ if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
889
+ throw new ValidationError("token: CA-\u0438\u0439\u043D csrAuthSha256/csrSignSha256 \u0438\u043B\u0433\u044D\u044D\u0441\u044D\u043D CSR-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
890
+ }
891
+ const vc = rec5(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
892
+ return {
893
+ sessionId: String(r.sessionID ?? ""),
894
+ vc: vc != null ? String(vc) : null,
895
+ csrAuthSha256: String(r.csrAuthSha256).toLowerCase(),
896
+ csrSignSha256: String(r.csrSignSha256).toLowerCase(),
897
+ ...typeof r.expiresAt === "string" ? { expiresAt: r.expiresAt } : {}
898
+ };
899
+ }
900
+ /**
901
+ * GET /token/certificate/{sessionID} — олгосон AUTH + SIGN гэрчилгээ + гинж. Токенд бичихээс өмнө
902
+ * {@link verifyTokenCertificates}-ээр CSR-ийн түлхүүртэй тулга.
903
+ */
904
+ async certificateResult(sessionId) {
905
+ const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
906
+ const r = rec5(raw);
907
+ const item = (v) => v && typeof v === "object" ? v : null;
908
+ return {
909
+ status: String(r.status ?? ""),
910
+ auth: item(r.auth),
911
+ sign: item(r.sign),
912
+ chain: Array.isArray(r.chain) ? r.chain.map(String) : [],
913
+ chainPem: String(r.chainPem ?? ""),
914
+ organization: r.organization && typeof r.organization === "object" ? r.organization : null,
915
+ keyMedium: String(r.keyMedium ?? ""),
916
+ certificateLevel: String(r.certificateLevel ?? ""),
917
+ raw
918
+ };
919
+ }
920
+ /** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
921
+ async authChallenge() {
922
+ const r = rec5(
923
+ await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
924
+ relyingPartyUUID: this.creds.rpUUID,
925
+ relyingPartyName: this.creds.rpName
926
+ })
927
+ );
928
+ return {
929
+ challengeId: String(r.challengeId ?? ""),
930
+ challenge: String(r.challenge ?? ""),
931
+ rpUUID: String(r.rpUUID ?? ""),
932
+ expiresAt: String(r.expiresAt ?? "")
933
+ };
934
+ }
935
+ /**
936
+ * POST /token/auth/verify — токены AUTH гэрчилгээ (DER) + challenge дээрх түүхий r||s. CA гинж, төлөв, цуцлалт, гарын
937
+ * үсгийг шалгана; SDK нэмж endResult === OK ба identity.etsi === гэрчилгээний SERIALNUMBER-ийг тулгана (эс бөгөөс
938
+ * ValidationError). Challenge нэг удаагийн — алдааны дараа шинээр авна.
939
+ */
940
+ async authVerify(challengeId, certDer, signature) {
941
+ let serial;
942
+ try {
943
+ serial = parseSubject(new X509Certificate2(certDer)).serialNumber;
944
+ } catch {
945
+ throw new Error("token auth: \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
946
+ }
947
+ if (!challengeId || !signature?.length) throw new Error("token auth: challengeId \u0431\u0430 signature \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
948
+ const raw = await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthVerify), {
949
+ challengeId,
950
+ certificate: Buffer.from(certDer).toString("base64"),
951
+ signature: Buffer.from(signature).toString("base64")
952
+ });
953
+ const r = rec5(raw);
954
+ if (rec5(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
955
+ const id = rec5(r.identity);
956
+ if (!id.etsi || id.etsi !== serial) throw new ValidationError("token auth: identity.etsi \u043D\u044C \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 SERIALNUMBER \u0431\u0438\u0448");
957
+ const org = rec5(r.organization);
958
+ const cert = rec5(r.cert);
959
+ return {
960
+ identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
961
+ serialHex: String(cert.serialHex ?? ""),
962
+ notAfter: String(cert.notAfter ?? ""),
963
+ organization: org.etsi ? { etsi: String(org.etsi), name: String(org.name ?? "") } : null,
964
+ raw
965
+ };
966
+ }
967
+ };
968
+ function csrSpki(csr) {
969
+ const top = tlv(csr, 0, csr.length);
970
+ const info = tlv(csr, top.start, top.end);
971
+ const version = tlv(csr, info.start, info.end);
972
+ const subject = tlv(csr, version.end, info.end);
973
+ const spki = tlv(csr, subject.end, info.end);
974
+ return createPublicKey({ key: Buffer.from(csr.subarray(subject.end, spki.end)), format: "der", type: "spki" }).export({
975
+ format: "der",
976
+ type: "spki"
977
+ });
978
+ }
979
+ function verifyTokenCertificates(result, csrAuth, csrSign) {
980
+ if (!result?.auth || !result.sign) throw new ValidationError("token: auth/sign \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
981
+ for (const [name, item, csr] of [
982
+ ["auth", result.auth, csrAuth],
983
+ ["sign", result.sign, csrSign]
984
+ ]) {
985
+ let want;
986
+ try {
987
+ want = csrSpki(csr);
988
+ } catch {
989
+ throw new Error(`token: ${name} CSR \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
990
+ }
991
+ let got;
992
+ try {
993
+ got = new X509Certificate2(Buffer.from(item.certificate, "base64")).publicKey.export({ format: "der", type: "spki" });
994
+ } catch {
995
+ throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
996
+ }
997
+ if (!got.equals(want)) throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440 CSR-\u0438\u0439\u043D\u0445\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
998
+ }
999
+ }
1000
+ function tokenAuthDigest(rpUUID, challengeB64) {
1001
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(rpUUID)) {
1002
+ throw new Error("token auth: rpUUID \u0436\u0438\u0436\u0438\u0433 \u04AF\u0441\u0433\u0438\u0439\u043D 36 \u0442\u044D\u043C\u0434\u044D\u0433\u0442 UUID \u0431\u0438\u0448");
1003
+ }
1004
+ const ch = Buffer.from(challengeB64, "base64");
1005
+ if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
1006
+ return createHash3("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
1007
+ }
1008
+ function tokenLimitOf(err) {
1009
+ if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
1010
+ let b;
1011
+ try {
1012
+ b = rec5(JSON.parse(err.body));
1013
+ } catch {
1014
+ return void 0;
1015
+ }
1016
+ if (typeof b.limit !== "number" || !Number.isInteger(b.limit) || b.limit < 0) return void 0;
1017
+ if (b.kind === "PERSONAL") return { kind: "PERSONAL", limit: b.limit };
1018
+ if (b.kind === "ORGANIZATION" && typeof b.organization === "string" && b.organization.startsWith("NTRMN-")) {
1019
+ return { kind: "ORGANIZATION", limit: b.limit, organization: b.organization };
1020
+ }
1021
+ return void 0;
1022
+ }
583
1023
 
584
1024
  // src/validator.ts
1025
+ import { X509Certificate as X509Certificate3, constants, createHash as createHash4, publicDecrypt, verify } from "crypto";
1026
+ var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
585
1027
  var ResponseValidator = class {
586
1028
  anchors;
587
1029
  intermediates;
@@ -590,14 +1032,28 @@ var ResponseValidator = class {
590
1032
  allowUntrusted;
591
1033
  revocation;
592
1034
  revocationMode;
1035
+ allowedIssuers;
1036
+ checkKeyUsage;
1037
+ schemeName;
593
1038
  constructor(trust = {}) {
594
- this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate2(p));
595
- this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate2(p));
1039
+ this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate3(p));
1040
+ this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate3(p));
596
1041
  this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
597
1042
  this.skew = trust.clockSkewMs ?? 0;
598
1043
  this.allowUntrusted = trust.allowUntrusted ?? false;
599
1044
  this.revocation = trust.revocation;
600
1045
  this.revocationMode = trust.revocationMode ?? "hard-fail";
1046
+ if (trust.allowedIssuerSkis?.length) {
1047
+ this.allowedIssuers = new Set(
1048
+ trust.allowedIssuerSkis.map((s) => {
1049
+ const ski = s.trim().replace(/:/g, "").toLowerCase();
1050
+ if (!/^(?:[0-9a-f]{2})+$/.test(ski)) throw new Error(`ResponseValidator: allowedIssuerSkis: "${s}" hex key identifier \u0431\u0438\u0448`);
1051
+ return ski;
1052
+ })
1053
+ );
1054
+ }
1055
+ this.checkKeyUsage = trust.checkKeyUsage ?? false;
1056
+ this.schemeName = trust.schemeName;
601
1057
  }
602
1058
  // ── нийтийн API ──
603
1059
  /**
@@ -625,6 +1081,7 @@ var ResponseValidator = class {
625
1081
  if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
626
1082
  const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
627
1083
  const payload = buildAcspV2Payload({
1084
+ ...this.schemeName ? { schemeName: this.schemeName } : {},
628
1085
  serverRandom,
629
1086
  rpChallenge: acsp.rpChallenge,
630
1087
  userChallenge,
@@ -728,9 +1185,10 @@ var ResponseValidator = class {
728
1185
  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");
729
1186
  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");
730
1187
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
731
- const leaf = new X509Certificate2(Buffer.from(result.certificateDerB64, "base64"));
1188
+ const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
732
1189
  this.checkValidity(leaf);
733
1190
  this.checkLevel(String(result.certificateLevel ?? ""));
1191
+ this.checkPurpose(leaf, protocol);
734
1192
  if (this.anchors.length === 0) {
735
1193
  if (!this.allowUntrusted) {
736
1194
  throw new ValidationError(
@@ -738,10 +1196,37 @@ var ResponseValidator = class {
738
1196
  );
739
1197
  }
740
1198
  } else {
741
- this.verifyChain(leaf);
1199
+ this.verifyChain(leaf, protocol === "ACSP_V2" ? EKU_CLIENT_AUTH : EKU_EMAIL_PROTECTION);
742
1200
  }
1201
+ this.checkIssuer(leaf);
743
1202
  return leaf;
744
1203
  }
1204
+ /**
1205
+ * checkKeyUsage үед гэрчилгээний зориулалт протоколд тохирох эсэх (CA crypto/ca_profile.go): ACSP_V2 (auth) → KU
1206
+ * digitalSignature + EKU clientAuth (эсвэл anyExtendedKeyUsage); RAW_DIGEST_SIGNATURE (sign) → KU contentCommitment.
1207
+ */
1208
+ checkPurpose(leaf, protocol) {
1209
+ if (!this.checkKeyUsage) return;
1210
+ const ku = keyUsageBits(leaf);
1211
+ if (protocol === "ACSP_V2") {
1212
+ if (!(ku & 128)) throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage digitalSignature \u0430\u043B\u0433\u0430");
1213
+ const eku = extKeyUsage(leaf);
1214
+ if (!eku.includes("1.3.6.1.5.5.7.3.2") && !eku.includes("2.5.29.37.0")) {
1215
+ throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 ExtKeyUsage clientAuth \u0430\u043B\u0433\u0430 (\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)");
1216
+ }
1217
+ return;
1218
+ }
1219
+ if (!(ku & 64)) throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage nonRepudiation (contentCommitment) \u0430\u043B\u0433\u0430");
1220
+ }
1221
+ /** allowedIssuerSkis үед leaf-ийн шууд issuer-ийн SKI (issuer-т алга бол leaf-ийн AKI) жагсаалтад байх ёстой. */
1222
+ checkIssuer(leaf) {
1223
+ if (!this.allowedIssuers) return;
1224
+ const issuer = this.findIssuer(leaf);
1225
+ const ski = (issuer && subjectKeyId(issuer)) ?? authorityKeyId(leaf);
1226
+ if (!ski || !this.allowedIssuers.has(ski)) {
1227
+ throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u0433 \u0433\u0430\u0440\u0433\u0430\u0441\u0430\u043D CA \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u0433\u0434\u0441\u04E9\u043D \u0438\u0440\u0433\u044D\u043D\u0438\u0439 issuer \u0431\u0438\u0448 (allowedIssuerSkis)");
1228
+ }
1229
+ }
745
1230
  checkValidity(cert) {
746
1231
  const now = Date.now();
747
1232
  const from = Date.parse(cert.validFrom);
@@ -755,21 +1240,40 @@ var ResponseValidator = class {
755
1240
  const need = rank[this.requiredLevel] ?? 2;
756
1241
  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}`);
757
1242
  }
758
- /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
759
- verifyChain(leaf) {
760
- let current = leaf;
761
- const pool = [...this.intermediates, ...this.anchors];
762
- for (let depth = 0; depth < 8; depth++) {
763
- for (const anchor of this.anchors) {
764
- if (isCa(anchor) && current.checkIssued(anchor) && current.verify(anchor.publicKey)) return;
1243
+ /**
1244
+ * RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
1245
+ * замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
1246
+ * гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
1247
+ * завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
1248
+ * бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
1249
+ * fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
1250
+ * хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
1251
+ */
1252
+ verifyChain(leaf, usage) {
1253
+ const now = Math.min(Math.max(Date.now(), Date.parse(leaf.validFrom)), Date.parse(leaf.validTo));
1254
+ const leafReason = criticalReason(leaf) ?? ekuReason(leaf, usage);
1255
+ if (leafReason) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${leafReason}`);
1256
+ let reason;
1257
+ const extend = (cur, cas) => {
1258
+ if (cas.length >= 8) return false;
1259
+ for (const [cand, isAnchor] of [
1260
+ ...this.anchors.map((c) => [c, true]),
1261
+ ...this.intermediates.map((c) => [c, false])
1262
+ ]) {
1263
+ if (cas.includes(cand) || !signedBy(cur, cand)) continue;
1264
+ const bad = caReason(cand, cas.length, now, usage);
1265
+ if (bad) {
1266
+ reason ??= bad;
1267
+ continue;
1268
+ }
1269
+ if (isAnchor || extend(cand, [...cas, cand])) return true;
765
1270
  }
766
- const issuer = pool.find(
767
- (c) => isCa(c) && current.checkIssued(c) && current.verify(c.publicKey)
768
- );
769
- if (!issuer) break;
770
- current = issuer;
771
- }
772
- 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");
1271
+ return false;
1272
+ };
1273
+ if (extend(leaf, [])) return;
1274
+ throw new ValidationError(
1275
+ reason ? `\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${reason}` : "\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"
1276
+ );
773
1277
  }
774
1278
  /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
775
1279
  findIssuer(leaf) {
@@ -784,14 +1288,23 @@ var ResponseValidator = class {
784
1288
  * дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
785
1289
  */
786
1290
  async checkRevocation(cert) {
787
- if (!this.revocation) return;
1291
+ await this.checkRevocationStatus(cert);
1292
+ }
1293
+ /**
1294
+ * {@link checkRevocation} + юу шийдсэнийг буцаана (лог/аудитад): "good" | "unchecked" (unknown, soft-fail) |
1295
+ * "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
1296
+ */
1297
+ async checkRevocationStatus(cert) {
1298
+ if (!this.revocation) return "skipped";
788
1299
  const status = await this.revocation(cert, this.findIssuer(cert));
1300
+ if (status === "good") return "good";
789
1301
  if (status === "revoked") {
790
1302
  throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
791
1303
  }
792
- if (status === "unknown" && this.revocationMode === "hard-fail") {
1304
+ if (this.revocationMode === "hard-fail") {
793
1305
  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)");
794
1306
  }
1307
+ return "unchecked";
795
1308
  }
796
1309
  };
797
1310
  function signedCallbackUrl(fromResponse, rpCallback, flowType) {
@@ -806,7 +1319,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
806
1319
  return fromResponse;
807
1320
  }
808
1321
  function userChallengeOf(verifier) {
809
- return createHash3("sha256").update(verifier, "utf8").digest("base64url");
1322
+ return createHash4("sha256").update(verifier, "utf8").digest("base64url");
810
1323
  }
811
1324
  function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
812
1325
  const sig = Buffer.from(sigB64, "base64");
@@ -908,7 +1421,7 @@ function mgf1(hash, seed, length) {
908
1421
  for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
909
1422
  const c = Buffer.alloc(4);
910
1423
  c.writeUInt32BE(i, 0);
911
- out.push(createHash3(nodeHash(hash)).update(seed).update(c).digest());
1424
+ out.push(createHash4(nodeHash(hash)).update(seed).update(c).digest());
912
1425
  }
913
1426
  return Buffer.concat(out).subarray(0, length);
914
1427
  }
@@ -932,12 +1445,118 @@ function emsaPssVerify(mHash, em, emBits, hash) {
932
1445
  const psLen = emLen - hLen - sLen - 2;
933
1446
  if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
934
1447
  const salt = db.subarray(psLen + 1);
935
- const h2 = createHash3(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
1448
+ const h2 = createHash4(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
936
1449
  return h.equals(h2);
937
1450
  }
1451
+ function keyUsageBits(cert) {
1452
+ const v = certExtension(cert.raw, "2.5.29.15");
1453
+ if (!v) return 0;
1454
+ const bs = tlv(v, 0, v.length);
1455
+ if (bs.tag !== 3 || bs.end - bs.start < 2) return 0;
1456
+ return v[bs.start + 1];
1457
+ }
1458
+ function extKeyUsage(cert) {
1459
+ const v = certExtension(cert.raw, "2.5.29.37");
1460
+ if (!v) return [];
1461
+ const seq = tlv(v, 0, v.length);
1462
+ const out = [];
1463
+ for (let p = seq.start; p < seq.end; ) {
1464
+ const t = tlv(v, p, seq.end);
1465
+ out.push(oidOf(v, t));
1466
+ p = t.end;
1467
+ }
1468
+ return out;
1469
+ }
1470
+ function subjectKeyId(cert) {
1471
+ const v = certExtension(cert.raw, "2.5.29.14");
1472
+ if (!v) return void 0;
1473
+ const o = tlv(v, 0, v.length);
1474
+ return o.tag === 4 && o.end > o.start ? Buffer.from(v.subarray(o.start, o.end)).toString("hex") : void 0;
1475
+ }
1476
+ function authorityKeyId(cert) {
1477
+ const v = certExtension(cert.raw, "2.5.29.35");
1478
+ if (!v) return void 0;
1479
+ const seq = tlv(v, 0, v.length);
1480
+ if (seq.start >= seq.end) return void 0;
1481
+ const k = tlv(v, seq.start, seq.end);
1482
+ return k.tag === 128 && k.end > k.start ? Buffer.from(v.subarray(k.start, k.end)).toString("hex") : void 0;
1483
+ }
938
1484
  function isCa(cert) {
939
1485
  return cert.ca === true;
940
1486
  }
1487
+ var EKU_ANY = "2.5.29.37.0";
1488
+ var EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
1489
+ var EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
1490
+ var OID_NAME_CONSTRAINTS = "2.5.29.30";
1491
+ var HANDLED_CRITICAL = /* @__PURE__ */ new Set([
1492
+ "2.5.29.14",
1493
+ // subjectKeyIdentifier
1494
+ "2.5.29.15",
1495
+ // keyUsage
1496
+ "2.5.29.17",
1497
+ // subjectAltName
1498
+ "2.5.29.19",
1499
+ // basicConstraints
1500
+ "2.5.29.30",
1501
+ // nameConstraints (CA-д — caReason fail-closed)
1502
+ "2.5.29.31",
1503
+ // cRLDistributionPoints
1504
+ "2.5.29.32",
1505
+ // certificatePolicies
1506
+ "2.5.29.35",
1507
+ // authorityKeyIdentifier
1508
+ "2.5.29.37",
1509
+ // extKeyUsage
1510
+ "1.3.6.1.5.5.7.1.1"
1511
+ // authorityInfoAccess
1512
+ ]);
1513
+ function signedBy(child, cand) {
1514
+ if (child.issuer !== cand.subject) return false;
1515
+ const aki = authorityKeyId(child);
1516
+ const ski = subjectKeyId(cand);
1517
+ if (aki && ski && aki !== ski) return false;
1518
+ try {
1519
+ return child.verify(cand.publicKey);
1520
+ } catch {
1521
+ return false;
1522
+ }
1523
+ }
1524
+ function criticalReason(cert) {
1525
+ return certExtensions(cert.raw).some((e) => e.critical && !HANDLED_CRITICAL.has(e.oid)) ? "\u04AF\u043B \u0442\u0430\u043D\u0438\u0445 critical extension" : void 0;
1526
+ }
1527
+ function ekuReason(cert, usage) {
1528
+ const eku = extKeyUsage(cert);
1529
+ return eku.length && !eku.includes(usage) && !eku.includes(EKU_ANY) ? "\u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 ExtKeyUsage \u044D\u043D\u044D \u04AF\u0439\u043B\u0434\u044D\u043B\u0434 \u0442\u043E\u0445\u0438\u0440\u043E\u0445\u0433\u04AF\u0439 (\u0433\u0438\u043D\u0436\u0438\u0439\u043D EKU)" : void 0;
1530
+ }
1531
+ function caReason(ca, below, now, usage) {
1532
+ if (!isCa(ca)) return "\u0437\u0430\u0432\u0441\u0440\u044B\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D CA \u0431\u0438\u0448 \u044D\u0441\u0432\u044D\u043B keyCertSign \u044D\u0440\u0445\u0433\u04AF\u0439";
1533
+ if (certExtension(ca.raw, "2.5.29.15") && !(keyUsageBits(ca) & 4)) return "CA-\u0438\u0439\u043D KeyUsage-\u0434 keyCertSign \u0430\u043B\u0433\u0430";
1534
+ const from = Date.parse(ca.validFrom);
1535
+ const to = Date.parse(ca.validTo);
1536
+ if (Number.isNaN(from) || Number.isNaN(to) || now < from || now > to) {
1537
+ return "\u0433\u0438\u043D\u0436\u0438\u0439\u043D \u0433\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 \u044D\u0441\u0432\u044D\u043B \u0445\u04AF\u0447\u0438\u043D\u0442\u044D\u0439 \u0431\u043E\u043B\u043E\u043E\u0433\u04AF\u0439";
1538
+ }
1539
+ const pathLen = basicConstraintsPathLen(ca);
1540
+ if (pathLen !== void 0 && below > pathLen) return "pathLenConstraint \u0437\u04E9\u0440\u0447\u0441\u04E9\u043D (\u0437\u0430\u0432\u0441\u0440\u044B\u043D CA \u0445\u044D\u0442 \u043E\u043B\u043E\u043D)";
1541
+ if (certExtension(ca.raw, OID_NAME_CONSTRAINTS)) return "CA-\u0434 name constraints \u0431\u0430\u0439\u043D\u0430 (SDK \u04AF\u043D\u044D\u043B\u0434\u044D\u0433\u0433\u04AF\u0439 \u2014 fail-closed)";
1542
+ return criticalReason(ca) ?? ekuReason(ca, usage);
1543
+ }
1544
+ function basicConstraintsPathLen(cert) {
1545
+ const v = certExtension(cert.raw, "2.5.29.19");
1546
+ if (!v) return void 0;
1547
+ const seq = tlv(v, 0, v.length);
1548
+ for (let p = seq.start; p < seq.end; ) {
1549
+ const t = tlv(v, p, seq.end);
1550
+ if (t.tag === 2) {
1551
+ if (t.end - t.start > 4 || (v[t.start] & 128) !== 0) throw new ValidationError("basicConstraints: pathLen \u0431\u0443\u0440\u0443\u0443");
1552
+ let n = 0;
1553
+ for (let i = t.start; i < t.end; i++) n = n * 256 + v[i];
1554
+ return n;
1555
+ }
1556
+ p = t.end;
1557
+ }
1558
+ return void 0;
1559
+ }
941
1560
 
942
1561
  // src/client.ts
943
1562
  var DEFAULT_BASE_URL = "https://rp.eidmongolia.mn";
@@ -962,6 +1581,8 @@ var EidClient = class {
962
1581
  sign;
963
1582
  pdf;
964
1583
  organization;
1584
+ person;
1585
+ token;
965
1586
  session;
966
1587
  validator;
967
1588
  constructor(cfg) {
@@ -977,8 +1598,14 @@ var EidClient = class {
977
1598
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
978
1599
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
979
1600
  this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
980
- this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level });
1601
+ this.pdf = new PdfApi(http, cfg.credentials, {
1602
+ certificateLevel: level,
1603
+ timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
1604
+ maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
1605
+ });
981
1606
  this.organization = new OrganizationApi(http);
1607
+ this.person = new PersonApi(http);
1608
+ this.token = new TokenApi(http, cfg.credentials);
982
1609
  this.session = new SessionApi(http);
983
1610
  this.validator = new ResponseValidator(cfg.trust ?? {});
984
1611
  }
@@ -1055,7 +1682,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1055
1682
  }
1056
1683
 
1057
1684
  // src/oidc.ts
1058
- import { timingSafeEqual } from "crypto";
1685
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1059
1686
  var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1060
1687
  var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1061
1688
  var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
@@ -1092,7 +1719,7 @@ var EidOidcClient = class {
1092
1719
  if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
1093
1720
  if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
1094
1721
  this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
1095
- this.scope = cfg.scope ?? "openid";
1722
+ this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
1096
1723
  }
1097
1724
  cfg;
1098
1725
  issuer;
@@ -1140,7 +1767,7 @@ var EidOidcClient = class {
1140
1767
  state,
1141
1768
  nonce,
1142
1769
  code_challenge: await pkceChallenge(codeVerifier),
1143
- code_challenge_method: "S256"
1770
+ code_challenge_method: OIDC_EXPECT.pkceMethod
1144
1771
  });
1145
1772
  if (opts.display) p.set("display", opts.display);
1146
1773
  u.search = p.toString();
@@ -1219,7 +1846,7 @@ var EidOidcClient = class {
1219
1846
  const header = decodeJsonPart(parts[0]);
1220
1847
  const payload = decodeJsonPart(parts[1]);
1221
1848
  if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1222
- if (header.alg !== "ES256") throw bad("alg ES256 \u0431\u0438\u0448");
1849
+ if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
1223
1850
  const kid = typeof header.kid === "string" ? header.kid : "";
1224
1851
  if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
1225
1852
  const pick = (ks) => ks.find(
@@ -1312,7 +1939,7 @@ function formEncode(s) {
1312
1939
  function timingSafeEqualStr(a, b) {
1313
1940
  const x = Buffer.from(a, "utf8");
1314
1941
  const y = Buffer.from(b, "utf8");
1315
- return x.length === y.length && timingSafeEqual(x, y);
1942
+ return x.length === y.length && timingSafeEqual2(x, y);
1316
1943
  }
1317
1944
  function decodeJsonPart(s) {
1318
1945
  try {
@@ -1334,7 +1961,10 @@ export {
1334
1961
  ApiError,
1335
1962
  AuthApi,
1336
1963
  AuthenticationError,
1964
+ CITIZEN_ISSUING_CA_SKI,
1337
1965
  DEFAULT_BASE_URL,
1966
+ DEFAULT_PDF_MAX_BYTES,
1967
+ DEFAULT_PDF_TIMEOUT_MS,
1338
1968
  DeviceLinkError,
1339
1969
  EID_ACR_ADVANCED,
1340
1970
  EID_ACR_QUALIFIED,
@@ -1349,10 +1979,14 @@ export {
1349
1979
  ON_BEHALF_ERROR_CODES,
1350
1980
  OrganizationApi,
1351
1981
  PdfApi,
1982
+ PersonApi,
1983
+ RETRYABLE_ERROR_CODES,
1984
+ ResponseTooLargeError,
1352
1985
  ResponseValidator,
1353
1986
  SessionApi,
1354
1987
  SessionFailedError,
1355
1988
  SignApi,
1989
+ TokenApi,
1356
1990
  ValidationError,
1357
1991
  acspV2Digest,
1358
1992
  buildAcspV2Payload,
@@ -1361,14 +1995,19 @@ export {
1361
1995
  interactionsToBase64,
1362
1996
  isMobileUserAgent,
1363
1997
  openOrShowQR,
1998
+ parseRetryAfter,
1364
1999
  parseSessionResult,
1365
2000
  parseSubject,
2001
+ personMatchesCertificate,
1366
2002
  pkceChallenge,
1367
2003
  qrDeviceLinkTicker,
1368
2004
  randomChallenge,
1369
2005
  resolveBaseUrl,
1370
2006
  sha256Base64,
2007
+ tokenAuthDigest,
2008
+ tokenLimitOf,
1371
2009
  userChallengeOf,
1372
2010
  verifyPayloadSignature,
1373
- verifyPrehashedSignature
2011
+ verifyPrehashedSignature,
2012
+ verifyTokenCertificates
1374
2013
  };