@gerege-systems/eid-mongolia-sdk 0.10.0 → 0.12.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.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  AuthenticationError: () => AuthenticationError,
26
26
  CITIZEN_ISSUING_CA_SKI: () => CITIZEN_ISSUING_CA_SKI,
27
27
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
28
+ DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
28
29
  DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
29
30
  DeviceLinkError: () => DeviceLinkError,
30
31
  EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
@@ -42,6 +43,7 @@ __export(index_exports, {
42
43
  PdfApi: () => PdfApi,
43
44
  PersonApi: () => PersonApi,
44
45
  RETRYABLE_ERROR_CODES: () => RETRYABLE_ERROR_CODES,
46
+ ResponseTooLargeError: () => ResponseTooLargeError,
45
47
  ResponseValidator: () => ResponseValidator,
46
48
  SessionApi: () => SessionApi,
47
49
  SessionFailedError: () => SessionFailedError,
@@ -174,6 +176,15 @@ var NetworkError = class extends EidError {
174
176
  this.timeout = init?.timeout;
175
177
  }
176
178
  };
179
+ var ResponseTooLargeError = class extends EidError {
180
+ constructor(op, limit) {
181
+ super(`${op}: \u0445\u0430\u0440\u0438\u0443 ${limit} \u0431\u0430\u0439\u0442\u0430\u0430\u0441 \u0438\u0445`);
182
+ this.op = op;
183
+ this.limit = limit;
184
+ }
185
+ op;
186
+ limit;
187
+ };
177
188
  function maskPath(path) {
178
189
  return path.split(/[?#]/)[0].replace(/\/(etsi|document)\/[^/?#\s]+/g, "/$1/\u2026");
179
190
  }
@@ -191,6 +202,7 @@ var DeviceLinkError = class extends EidError {
191
202
 
192
203
  // src/http.ts
193
204
  var DEFAULT_TIMEOUT_MS = 15e3;
205
+ var MAX_JSON_BYTES = 4 << 20;
194
206
  var Http = class {
195
207
  constructor(cfg) {
196
208
  this.cfg = cfg;
@@ -206,14 +218,14 @@ var Http = class {
206
218
  postMultipart(path, form, timeoutMs) {
207
219
  return this.json("POST", path, form, void 0, timeoutMs);
208
220
  }
209
- /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
210
- async getBytes(path, timeoutMs) {
221
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
222
+ async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
211
223
  const res = await this.send("GET", path, void 0, void 0, timeoutMs);
212
- return new Uint8Array(await res.arrayBuffer());
224
+ return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
213
225
  }
214
226
  async json(method, path, body, contentType, timeoutMs) {
215
227
  const res = await this.send(method, path, body, contentType, timeoutMs);
216
- const text = await res.text();
228
+ const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
217
229
  return text ? JSON.parse(text) : {};
218
230
  }
219
231
  async send(method, path, body, contentType, timeoutMs) {
@@ -223,7 +235,8 @@ var Http = class {
223
235
  method,
224
236
  headers: {
225
237
  Authorization: `Bearer ${this.cfg.apiSecret}`,
226
- ...contentType ? { "Content-Type": contentType } : {}
238
+ ...contentType ? { "Content-Type": contentType } : {},
239
+ ...this.cfg.language ? { "Accept-Language": this.cfg.language } : {}
227
240
  },
228
241
  body,
229
242
  signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
@@ -258,6 +271,34 @@ var Http = class {
258
271
  throw new ApiError(res.status, text, code, { headers, retryAfterMs });
259
272
  }
260
273
  };
274
+ async function readLimited(res, limit, op) {
275
+ const declared = Number(res.headers.get("content-length") ?? NaN);
276
+ if (declared > limit) {
277
+ await res.body?.cancel().catch(() => void 0);
278
+ throw new ResponseTooLargeError(op, limit);
279
+ }
280
+ if (!res.body) return new Uint8Array(0);
281
+ const reader = res.body.getReader();
282
+ const chunks = [];
283
+ let total = 0;
284
+ for (; ; ) {
285
+ const { done, value } = await reader.read();
286
+ if (done) break;
287
+ total += value.byteLength;
288
+ if (total > limit) {
289
+ await reader.cancel().catch(() => void 0);
290
+ throw new ResponseTooLargeError(op, limit);
291
+ }
292
+ chunks.push(value);
293
+ }
294
+ const out = new Uint8Array(total);
295
+ let off = 0;
296
+ for (const c of chunks) {
297
+ out.set(c, off);
298
+ off += c.byteLength;
299
+ }
300
+ return out;
301
+ }
261
302
  function errorBody(text) {
262
303
  try {
263
304
  const v = JSON.parse(text);
@@ -294,7 +335,10 @@ var RP_ROUTES = {
294
335
  };
295
336
  var RP_ROUTES_UNDISCOVERED = {
296
337
  tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
297
- tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
338
+ tokenAuthVerify: { method: "POST", path: "/token/auth/verify" },
339
+ pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
340
+ pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
341
+ pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
298
342
  };
299
343
  function routePath(route, ...args) {
300
344
  let i = 0;
@@ -602,10 +646,12 @@ function parseOrg(raw) {
602
646
  const o = asRecord(raw);
603
647
  const etsi = str(o.orgEtsi);
604
648
  if (!etsi) return null;
649
+ const roleLabel = str(o.roleLabel);
605
650
  return {
606
651
  orgEtsi: etsi,
607
652
  orgName: str(o.orgName),
608
653
  role: str(o.role),
654
+ ...roleLabel ? { roleLabel } : {},
609
655
  rightType: str(o.rightType)
610
656
  };
611
657
  }
@@ -642,6 +688,7 @@ var SessionApi = class {
642
688
 
643
689
  // src/pdf.ts
644
690
  var import_node_crypto2 = require("crypto");
691
+ var DEFAULT_PDF_MAX_BYTES = 64 << 20;
645
692
  var DEFAULT_PDF_TIMEOUT_MS = 8e4;
646
693
  function rec3(v) {
647
694
  return v && typeof v === "object" ? v : {};
@@ -674,7 +721,7 @@ var PdfApi = class {
674
721
  const form = new FormData();
675
722
  form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
676
723
  form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
677
- const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS));
724
+ const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
678
725
  const receivedAt = Date.now();
679
726
  const vc = rec3(r.vc).value;
680
727
  const dl = rec3(r.deviceLink);
@@ -701,7 +748,75 @@ var PdfApi = class {
701
748
  * дараа дахин татна (`retryable`).
702
749
  */
703
750
  document(sessionId) {
704
- return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS);
751
+ return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
752
+ }
753
+ get timeout() {
754
+ return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
755
+ }
756
+ get maxBytes() {
757
+ return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
758
+ }
759
+ /**
760
+ * USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
761
+ * (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
762
+ * зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
763
+ * LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
764
+ * TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
765
+ */
766
+ async tokenPrepare(input) {
767
+ if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
768
+ throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
769
+ }
770
+ const request = {
771
+ relyingPartyUUID: this.creds.rpUUID,
772
+ relyingPartyName: this.creds.rpName,
773
+ signer: { etsi: input.signerEtsi.trim() },
774
+ certificate: Buffer.from(input.certificate).toString("base64"),
775
+ fileName: input.fileName
776
+ };
777
+ if (input.docID?.trim()) request.docID = input.docID.trim();
778
+ if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
779
+ const form = new FormData();
780
+ form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
781
+ form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
782
+ const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
783
+ const r = rec3(raw);
784
+ const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
785
+ if (!r.signingID || hashToSign.length !== 32) {
786
+ throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
787
+ }
788
+ return {
789
+ signingId: String(r.signingID),
790
+ docId: String(r.docID ?? ""),
791
+ hashToSign,
792
+ hashAlgorithm: String(r.hashAlgorithm ?? ""),
793
+ signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
794
+ signatureFormat: String(r.signatureFormat ?? ""),
795
+ expiresAt: String(r.expiresAt ?? ""),
796
+ raw
797
+ };
798
+ }
799
+ /**
800
+ * POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
801
+ * Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
802
+ * Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
803
+ * TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
804
+ */
805
+ async tokenFinalize(signingId, signature) {
806
+ 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)");
807
+ const r = await this.http.post(
808
+ routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
809
+ { signature: Buffer.from(signature).toString("base64") },
810
+ this.timeout
811
+ );
812
+ return r;
813
+ }
814
+ /**
815
+ * GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
816
+ * хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
817
+ */
818
+ tokenDocument(signingId) {
819
+ return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
705
820
  }
706
821
  /**
707
822
  * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
@@ -735,7 +850,7 @@ var OrganizationApi = class {
735
850
  async getRepresentations(personEtsi) {
736
851
  const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
737
852
  const list = Array.isArray(raw.representations) ? raw.representations : [];
738
- return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
853
+ return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list, raw };
739
854
  }
740
855
  };
741
856
 
@@ -887,6 +1002,10 @@ function parseSubject(cert) {
887
1002
  return out;
888
1003
  }
889
1004
  function certExtension(der, oid) {
1005
+ return certExtensions(der).find((e) => e.oid === oid)?.value;
1006
+ }
1007
+ function certExtensions(der) {
1008
+ const out = [];
890
1009
  const top = tlv(der, 0, der.length);
891
1010
  const tbs = tlv(der, top.start, top.end);
892
1011
  let f = tlv(der, tbs.start, tbs.end);
@@ -900,16 +1019,20 @@ function certExtension(der, oid) {
900
1019
  const ext = tlv(der, ep, seq.end);
901
1020
  const id = tlv(der, ext.start, ext.end);
902
1021
  let v = tlv(der, id.end, ext.end);
903
- if (v.tag === 1) v = tlv(der, v.end, ext.end);
1022
+ let critical = false;
1023
+ if (v.tag === 1) {
1024
+ critical = v.end > v.start && der[v.start] !== 0;
1025
+ v = tlv(der, v.end, ext.end);
1026
+ }
904
1027
  if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
905
- if (oidOf(der, id) === oid) return der.subarray(v.start, v.end);
1028
+ out.push({ oid: oidOf(der, id), critical, value: der.subarray(v.start, v.end) });
906
1029
  ep = ext.end;
907
1030
  }
908
- return void 0;
1031
+ return out;
909
1032
  }
910
1033
  pos = t.end;
911
1034
  }
912
- return void 0;
1035
+ return out;
913
1036
  }
914
1037
 
915
1038
  // src/token.ts
@@ -1259,7 +1382,7 @@ var ResponseValidator = class {
1259
1382
  );
1260
1383
  }
1261
1384
  } else {
1262
- this.verifyChain(leaf);
1385
+ this.verifyChain(leaf, protocol === "ACSP_V2" ? EKU_CLIENT_AUTH : EKU_EMAIL_PROTECTION);
1263
1386
  }
1264
1387
  this.checkIssuer(leaf);
1265
1388
  return leaf;
@@ -1303,21 +1426,40 @@ var ResponseValidator = class {
1303
1426
  const need = rank[this.requiredLevel] ?? 2;
1304
1427
  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}`);
1305
1428
  }
1306
- /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
1307
- verifyChain(leaf) {
1308
- let current = leaf;
1309
- const pool = [...this.intermediates, ...this.anchors];
1310
- for (let depth = 0; depth < 8; depth++) {
1311
- for (const anchor of this.anchors) {
1312
- if (isCa(anchor) && current.checkIssued(anchor) && current.verify(anchor.publicKey)) return;
1429
+ /**
1430
+ * RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
1431
+ * замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
1432
+ * гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
1433
+ * завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
1434
+ * бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
1435
+ * fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
1436
+ * хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
1437
+ */
1438
+ verifyChain(leaf, usage) {
1439
+ const now = Math.min(Math.max(Date.now(), Date.parse(leaf.validFrom)), Date.parse(leaf.validTo));
1440
+ const leafReason = criticalReason(leaf) ?? ekuReason(leaf, usage);
1441
+ 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}`);
1442
+ let reason;
1443
+ const extend = (cur, cas) => {
1444
+ if (cas.length >= 8) return false;
1445
+ for (const [cand, isAnchor] of [
1446
+ ...this.anchors.map((c) => [c, true]),
1447
+ ...this.intermediates.map((c) => [c, false])
1448
+ ]) {
1449
+ if (cas.includes(cand) || !signedBy(cur, cand)) continue;
1450
+ const bad = caReason(cand, cas.length, now, usage);
1451
+ if (bad) {
1452
+ reason ??= bad;
1453
+ continue;
1454
+ }
1455
+ if (isAnchor || extend(cand, [...cas, cand])) return true;
1313
1456
  }
1314
- const issuer = pool.find(
1315
- (c) => isCa(c) && current.checkIssued(c) && current.verify(c.publicKey)
1316
- );
1317
- if (!issuer) break;
1318
- current = issuer;
1319
- }
1320
- 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");
1457
+ return false;
1458
+ };
1459
+ if (extend(leaf, [])) return;
1460
+ throw new ValidationError(
1461
+ 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"
1462
+ );
1321
1463
  }
1322
1464
  /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
1323
1465
  findIssuer(leaf) {
@@ -1528,6 +1670,79 @@ function authorityKeyId(cert) {
1528
1670
  function isCa(cert) {
1529
1671
  return cert.ca === true;
1530
1672
  }
1673
+ var EKU_ANY = "2.5.29.37.0";
1674
+ var EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
1675
+ var EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
1676
+ var OID_NAME_CONSTRAINTS = "2.5.29.30";
1677
+ var HANDLED_CRITICAL = /* @__PURE__ */ new Set([
1678
+ "2.5.29.14",
1679
+ // subjectKeyIdentifier
1680
+ "2.5.29.15",
1681
+ // keyUsage
1682
+ "2.5.29.17",
1683
+ // subjectAltName
1684
+ "2.5.29.19",
1685
+ // basicConstraints
1686
+ "2.5.29.30",
1687
+ // nameConstraints (CA-д — caReason fail-closed)
1688
+ "2.5.29.31",
1689
+ // cRLDistributionPoints
1690
+ "2.5.29.32",
1691
+ // certificatePolicies
1692
+ "2.5.29.35",
1693
+ // authorityKeyIdentifier
1694
+ "2.5.29.37",
1695
+ // extKeyUsage
1696
+ "1.3.6.1.5.5.7.1.1"
1697
+ // authorityInfoAccess
1698
+ ]);
1699
+ function signedBy(child, cand) {
1700
+ if (child.issuer !== cand.subject) return false;
1701
+ const aki = authorityKeyId(child);
1702
+ const ski = subjectKeyId(cand);
1703
+ if (aki && ski && aki !== ski) return false;
1704
+ try {
1705
+ return child.verify(cand.publicKey);
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ }
1710
+ function criticalReason(cert) {
1711
+ return certExtensions(cert.raw).some((e) => e.critical && !HANDLED_CRITICAL.has(e.oid)) ? "\u04AF\u043B \u0442\u0430\u043D\u0438\u0445 critical extension" : void 0;
1712
+ }
1713
+ function ekuReason(cert, usage) {
1714
+ const eku = extKeyUsage(cert);
1715
+ 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;
1716
+ }
1717
+ function caReason(ca, below, now, usage) {
1718
+ 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";
1719
+ if (certExtension(ca.raw, "2.5.29.15") && !(keyUsageBits(ca) & 4)) return "CA-\u0438\u0439\u043D KeyUsage-\u0434 keyCertSign \u0430\u043B\u0433\u0430";
1720
+ const from = Date.parse(ca.validFrom);
1721
+ const to = Date.parse(ca.validTo);
1722
+ if (Number.isNaN(from) || Number.isNaN(to) || now < from || now > to) {
1723
+ 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";
1724
+ }
1725
+ const pathLen = basicConstraintsPathLen(ca);
1726
+ 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)";
1727
+ 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)";
1728
+ return criticalReason(ca) ?? ekuReason(ca, usage);
1729
+ }
1730
+ function basicConstraintsPathLen(cert) {
1731
+ const v = certExtension(cert.raw, "2.5.29.19");
1732
+ if (!v) return void 0;
1733
+ const seq = tlv(v, 0, v.length);
1734
+ for (let p = seq.start; p < seq.end; ) {
1735
+ const t = tlv(v, p, seq.end);
1736
+ if (t.tag === 2) {
1737
+ if (t.end - t.start > 4 || (v[t.start] & 128) !== 0) throw new ValidationError("basicConstraints: pathLen \u0431\u0443\u0440\u0443\u0443");
1738
+ let n = 0;
1739
+ for (let i = t.start; i < t.end; i++) n = n * 256 + v[i];
1740
+ return n;
1741
+ }
1742
+ p = t.end;
1743
+ }
1744
+ return void 0;
1745
+ }
1531
1746
 
1532
1747
  // src/client.ts
1533
1748
  var DEFAULT_BASE_URL = "https://rp.eidmongolia.mn";
@@ -1564,12 +1779,17 @@ var EidClient = class {
1564
1779
  apiSecret: cfg.credentials.apiSecret,
1565
1780
  timeoutMs: cfg.timeoutMs,
1566
1781
  dispatcher: cfg.dispatcher,
1567
- fetchImpl: cfg.fetchImpl
1782
+ fetchImpl: cfg.fetchImpl,
1783
+ language: cfg.language?.trim() || void 0
1568
1784
  });
1569
1785
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1570
1786
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1571
1787
  this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1572
- this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level, timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS });
1788
+ this.pdf = new PdfApi(http, cfg.credentials, {
1789
+ certificateLevel: level,
1790
+ timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
1791
+ maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
1792
+ });
1573
1793
  this.organization = new OrganizationApi(http);
1574
1794
  this.person = new PersonApi(http);
1575
1795
  this.token = new TokenApi(http, cfg.credentials);
@@ -1973,6 +2193,7 @@ function callbackParams(input) {
1973
2193
  AuthenticationError,
1974
2194
  CITIZEN_ISSUING_CA_SKI,
1975
2195
  DEFAULT_BASE_URL,
2196
+ DEFAULT_PDF_MAX_BYTES,
1976
2197
  DEFAULT_PDF_TIMEOUT_MS,
1977
2198
  DeviceLinkError,
1978
2199
  EID_ACR_ADVANCED,
@@ -1990,6 +2211,7 @@ function callbackParams(input) {
1990
2211
  PdfApi,
1991
2212
  PersonApi,
1992
2213
  RETRYABLE_ERROR_CODES,
2214
+ ResponseTooLargeError,
1993
2215
  ResponseValidator,
1994
2216
  SessionApi,
1995
2217
  SessionFailedError,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PersonBlock, b as PdfSessionBlock, c as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-BWCDj8xO.cjs';
2
- export { d as ApiError, e as AuthenticationError, f as DeviceLinkError, g as DeviceLinkInput, h as EndResult, i as ForbiddenError, j as HttpErrorInit, k as NavigatorLike, l as NetworkError, O as ON_BEHALF_ERROR_CODES, m as OnBehalfErrorCode, n as OpenOrShowQROptions, o as OrgResult, p as PdfDocumentStatus, q as PdfSigner, r as RETRYABLE_ERROR_CODES, s as Representation, t as RetryableErrorCode, u as SessionFailedError, v as SessionState, w as SignatureAlgorithmParameters, x as SignatureProtocol, T as TokenCertSessionBlock, y as TokenCertStatus, V as ValidationError, z as deviceLink, B as isMobileUserAgent, G as openOrShowQR, J as parseRetryAfter } from './browser-BWCDj8xO.cjs';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PersonBlock, b as PdfSessionBlock, c as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-B_xuB7Rt.cjs';
2
+ export { d as ApiError, e as AuthenticationError, f as DeviceLinkError, g as DeviceLinkInput, h as EndResult, i as ForbiddenError, j as HttpErrorInit, k as NavigatorLike, l as NetworkError, O as ON_BEHALF_ERROR_CODES, m as OnBehalfErrorCode, n as OpenOrShowQROptions, o as OrgResult, p as PdfDocumentStatus, q as PdfSigner, r as RETRYABLE_ERROR_CODES, s as Representation, t as ResponseTooLargeError, u as RetryableErrorCode, v as SessionFailedError, w as SessionState, x as SignatureAlgorithmParameters, y as SignatureProtocol, T as TokenCertSessionBlock, z as TokenCertStatus, V as ValidationError, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR, K as parseRetryAfter } from './browser-B_xuB7Rt.cjs';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -17,6 +17,8 @@ interface HttpConfig {
17
17
  dispatcher?: unknown;
18
18
  /** Тест/custom орчинд fetch-ийг орлуулах (default — global fetch). */
19
19
  fetchImpl?: typeof fetch;
20
+ /** `Accept-Language` header-ийн утга (ж: "en"); хоосон бол илгээхгүй. */
21
+ language?: string;
20
22
  }
21
23
  declare class Http {
22
24
  private readonly cfg;
@@ -25,8 +27,8 @@ declare class Http {
25
27
  get(path: string, timeoutMs?: number): Promise<unknown>;
26
28
  /** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
27
29
  postMultipart(path: string, form: FormData, timeoutMs?: number): Promise<unknown>;
28
- /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
29
- getBytes(path: string, timeoutMs?: number): Promise<Uint8Array>;
30
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
31
+ getBytes(path: string, timeoutMs?: number, maxBytes?: number): Promise<Uint8Array>;
30
32
  private json;
31
33
  private send;
32
34
  }
@@ -153,7 +155,11 @@ interface PdfDefaults {
153
155
  certificateLevel: CertificateLevel;
154
156
  /** prepare / document дуудлагын хугацаа (мс); анхдагч {@link DEFAULT_PDF_TIMEOUT_MS}. */
155
157
  timeoutMs?: number;
158
+ /** document / tokenDocument-ийн санах ойд унших дээд хэмжээ (байт); анхдагч {@link DEFAULT_PDF_MAX_BYTES}. */
159
+ maxBytes?: number;
156
160
  }
161
+ /** Гарын үсэгтэй PDF татахад санах ойд уншиж болох анхдагч дээд хэмжээ (64 MiB) — `ClientConfig.pdfMaxBytes`. */
162
+ declare const DEFAULT_PDF_MAX_BYTES: number;
157
163
  /**
158
164
  * PAdES prepare / баримт татах нэг дуудлагын анхдагч хугацаа (мс). CA prepare нь ≤ 10 MiB уншиж, өмнөх гарын үсэг +
159
165
  * LTV өгөгдөл шалгаад push эхлүүлдэг; баримт татахад угсралтыг ≤ 30с хүлээдэг (PADES_SIGNING §3.4). 80с нь хоёуланд
@@ -209,6 +215,35 @@ interface PdfPrepareSession {
209
215
  /** SDK хариу хүлээн авсан мөч (ms epoch) — QR-ийн `receivedAt` ({@link buildDeviceLink}). */
210
216
  receivedAt: number;
211
217
  }
218
+ /** USB токены PAdES — POST /pdf/token/prepare-ийн оролт. */
219
+ interface PdfTokenPrepareInput {
220
+ /** Эх PDF (≤ 10 223 616 байт). */
221
+ pdf: Uint8Array;
222
+ /** Баримтын нэр (≤ 120 тэмдэгт, заавал). */
223
+ fileName: string;
224
+ /** Иргэний ETSI (`PNOMN-…`) — токены гэрчилгээний SERIALNUMBER-тэй таарна. */
225
+ signerEtsi: string;
226
+ /** Токены SIGN гэрчилгээ (DER): eID Mongolia Issuing CA-аас, ACTIVE токенд холбогдсон. */
227
+ certificate: Uint8Array;
228
+ /** Олон гарын үсэгтэй баримтын docID (заавал биш, UUID). */
229
+ docID?: string;
230
+ /** Байгууллагын токенд ЗААВАЛ тухайн байгууллага (`NTRMN-…`); хувийн токенд хоосон. Зөрвөл 403 TOKEN_ORG_MISMATCH. */
231
+ onBehalfOf?: string;
232
+ }
233
+ /** POST /pdf/token/prepare-ийн хариу. */
234
+ interface PdfTokenPrepareSession {
235
+ signingId: string;
236
+ docId: string;
237
+ /** Токеноор зурах 32 байт (SHA-256 DTBS): CKM_ECDSA (prehashed) → түүхий r||s (64 байт). */
238
+ hashToSign: Uint8Array;
239
+ hashAlgorithm: string;
240
+ signatureAlgorithm: string;
241
+ /** "RAW_RS". */
242
+ signatureFormat: string;
243
+ /** tokenFinalize-ийн эцсийн хугацаа (prepare-ээс 10 мин), ISO-8601. */
244
+ expiresAt: string;
245
+ raw: unknown;
246
+ }
212
247
  declare class PdfApi {
213
248
  private readonly http;
214
249
  private readonly creds;
@@ -226,6 +261,28 @@ declare class PdfApi {
226
261
  * дараа дахин татна (`retryable`).
227
262
  */
228
263
  document(sessionId: string): Promise<Uint8Array>;
264
+ private get timeout();
265
+ private get maxBytes();
266
+ /**
267
+ * USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
268
+ * (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
269
+ * зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
270
+ * LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
271
+ * TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
272
+ */
273
+ tokenPrepare(input: PdfTokenPrepareInput): Promise<PdfTokenPrepareSession>;
274
+ /**
275
+ * POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
276
+ * Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
277
+ * Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
278
+ * TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
279
+ */
280
+ tokenFinalize(signingId: string, signature: Uint8Array): Promise<PdfSessionBlock>;
281
+ /**
282
+ * GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
283
+ * хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
284
+ */
285
+ tokenDocument(signingId: string): Promise<Uint8Array>;
229
286
  /**
230
287
  * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
231
288
  * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
@@ -696,7 +753,15 @@ declare class ResponseValidator {
696
753
  private checkIssuer;
697
754
  private checkValidity;
698
755
  private checkLevel;
699
- /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
756
+ /**
757
+ * RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
758
+ * замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
759
+ * гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
760
+ * завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
761
+ * бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
762
+ * fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
763
+ * хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
764
+ */
700
765
  private verifyChain;
701
766
  /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
702
767
  private findIssuer;
@@ -734,10 +799,20 @@ interface ClientConfig {
734
799
  timeoutMs?: number;
735
800
  /** PAdES prepare / баримт татах дуудлагын хугацаа (мс). Анхдагч 80000 ({@link DEFAULT_PDF_TIMEOUT_MS}). */
736
801
  pdfTimeoutMs?: number;
802
+ /**
803
+ * `pdf.document` / `pdf.tokenDocument`-ийн санах ойд унших дээд хэмжээ (байт). Анхдагч 64 MiB
804
+ * ({@link DEFAULT_PDF_MAX_BYTES}); хэтэрвэл `ResponseTooLargeError`.
805
+ */
806
+ pdfMaxBytes?: number;
737
807
  /** undici Dispatcher (mTLS client cert). */
738
808
  dispatcher?: unknown;
739
809
  /** fetch орлуулагч (тест). */
740
810
  fetchImpl?: typeof fetch;
811
+ /**
812
+ * Хүнд харуулах текстийн хэл (`roleLabel` г.м.) — дуудлага бүрд `Accept-Language` болж илгээгдэнэ (`"mn"` | `"en"`).
813
+ * Анхдагч: илгээхгүй (сервер mn).
814
+ */
815
+ language?: string;
741
816
  /** Trust anchor (Root CA) — validator-д. Production-д ЗААВАЛ. */
742
817
  trust?: TrustConfig;
743
818
  }
@@ -989,4 +1064,4 @@ declare class EidOidcClient {
989
1064
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
990
1065
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
991
1066
 
992
- export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DEFAULT_PDF_TIMEOUT_MS, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, parseSubject, personMatchesCertificate, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates };
1067
+ export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DEFAULT_PDF_MAX_BYTES, DEFAULT_PDF_TIMEOUT_MS, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type PdfTokenPrepareInput, type PdfTokenPrepareSession, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, parseSubject, personMatchesCertificate, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates };