@gerege-systems/eid-mongolia-sdk 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,20 +24,24 @@ __export(index_exports, {
24
24
  AuthApi: () => AuthApi,
25
25
  AuthenticationError: () => AuthenticationError,
26
26
  CITIZEN_ISSUING_CA_SKI: () => CITIZEN_ISSUING_CA_SKI,
27
+ CertChoiceInvalidError: () => CertChoiceInvalidError,
27
28
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
28
29
  DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
29
30
  DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
31
+ DEFAULT_SIGN_SESSION_TTL_MS: () => DEFAULT_SIGN_SESSION_TTL_MS,
30
32
  DeviceLinkError: () => DeviceLinkError,
31
33
  EidClient: () => EidClient,
32
34
  EidError: () => EidError,
33
35
  ForbiddenError: () => ForbiddenError,
34
36
  Http: () => Http,
35
37
  NetworkError: () => NetworkError,
38
+ NoSigningCertificateError: () => NoSigningCertificateError,
36
39
  ON_BEHALF_ERROR_CODES: () => ON_BEHALF_ERROR_CODES,
37
40
  OrganizationApi: () => OrganizationApi,
38
41
  PdfApi: () => PdfApi,
39
42
  PersonApi: () => PersonApi,
40
43
  RETRYABLE_ERROR_CODES: () => RETRYABLE_ERROR_CODES,
44
+ RequestInvalidError: () => RequestInvalidError,
41
45
  ResponseTooLargeError: () => ResponseTooLargeError,
42
46
  ResponseValidator: () => ResponseValidator,
43
47
  SessionApi: () => SessionApi,
@@ -50,10 +54,13 @@ __export(index_exports, {
50
54
  buildDeviceLink: () => buildDeviceLink,
51
55
  createEidLogin: () => createEidLogin,
52
56
  deviceLink: () => deviceLink,
57
+ deviceLinkInput: () => deviceLinkInput,
58
+ forwardedClientIp: () => forwardedClientIp,
53
59
  interactionsToBase64: () => interactionsToBase64,
54
60
  isMobileUserAgent: () => isMobileUserAgent,
55
61
  memoryLoginStore: () => memoryLoginStore,
56
62
  openOrShowQR: () => openOrShowQR,
63
+ parseExpiresAt: () => parseExpiresAt,
57
64
  parseRetryAfter: () => parseRetryAfter,
58
65
  parseSessionResult: () => parseSessionResult,
59
66
  parseSubject: () => parseSubject,
@@ -61,7 +68,9 @@ __export(index_exports, {
61
68
  qrDeviceLinkTicker: () => qrDeviceLinkTicker,
62
69
  randomChallenge: () => randomChallenge,
63
70
  resolveBaseUrl: () => resolveBaseUrl,
71
+ sessionExpiry: () => sessionExpiry,
64
72
  sha256Base64: () => sha256Base64,
73
+ startExpiry: () => startExpiry,
65
74
  tokenAuthDigest: () => tokenAuthDigest,
66
75
  tokenLimitOf: () => tokenLimitOf,
67
76
  userChallengeOf: () => userChallengeOf,
@@ -134,6 +143,12 @@ var ApiError = class extends EidError {
134
143
  return (this.status === 429 || this.status === 503) && (this.retryAfterMs ?? 0) > 0;
135
144
  }
136
145
  };
146
+ var NoSigningCertificateError = class extends ApiError {
147
+ };
148
+ var CertChoiceInvalidError = class extends ApiError {
149
+ };
150
+ var RequestInvalidError = class extends ApiError {
151
+ };
137
152
  var RETRYABLE_ERROR_CODES = [
138
153
  "PDF_PREPARE_BUSY",
139
154
  "DOCUMENT_PENDING",
@@ -265,7 +280,8 @@ var Http = class {
265
280
  { headers, retryAfterMs }
266
281
  );
267
282
  }
268
- throw new ApiError(res.status, text, code, { headers, retryAfterMs });
283
+ const Typed = code === "NO_SIGNING_CERTIFICATE" ? NoSigningCertificateError : code === "CERT_CHOICE_INVALID" ? CertChoiceInvalidError : code === "REQUEST_INVALID" ? RequestInvalidError : ApiError;
284
+ throw new Typed(res.status, text, code, { headers, retryAfterMs });
269
285
  }
270
286
  };
271
287
  async function readLimited(res, limit, op) {
@@ -318,6 +334,11 @@ var RP_ROUTES = {
318
334
  authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
319
335
  signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
320
336
  signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
337
+ signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
338
+ signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
339
+ signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
340
+ certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
341
+ certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
321
342
  pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
322
343
  pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
323
344
  orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
@@ -386,6 +407,21 @@ function acspV2Digest(p, hashAlgorithm) {
386
407
  return (0, import_node_crypto.createHash)(nodeHash(hashAlgorithm)).update(buildAcspV2Payload(p)).digest();
387
408
  }
388
409
 
410
+ // src/types.ts
411
+ var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
412
+ function sessionExpiry(sentAtMs, ttlMs) {
413
+ return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
414
+ }
415
+ function parseExpiresAt(v) {
416
+ if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(v)) return void 0;
417
+ const t = Date.parse(v);
418
+ return Number.isFinite(t) ? t : void 0;
419
+ }
420
+ function startExpiry(raw, sentAtMs, ttlMs) {
421
+ const r = raw && typeof raw === "object" ? raw : {};
422
+ return parseExpiresAt(r.expiresAt) ?? sessionExpiry(sentAtMs, ttlMs);
423
+ }
424
+
389
425
  // src/auth.ts
390
426
  function rec(v) {
391
427
  return v && typeof v === "object" ? v : {};
@@ -431,11 +467,19 @@ var AuthApi = class {
431
467
  parseNotification(raw, acsp) {
432
468
  const r = rec(raw);
433
469
  const vc = rec(r.vc).value;
434
- return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: acsp.rpChallenge, acsp };
470
+ const expiresAt = parseExpiresAt(r.expiresAt);
471
+ return {
472
+ sessionId: String(r.sessionID),
473
+ vc: vc != null ? String(vc) : null,
474
+ rpChallenge: acsp.rpChallenge,
475
+ acsp,
476
+ ...expiresAt !== void 0 ? { expiresAt } : {}
477
+ };
435
478
  }
436
479
  parseDeviceLink(raw, acsp) {
437
480
  const r = rec(raw);
438
481
  const vc = typeof r.vc === "object" && r.vc !== null ? rec(r.vc).value : r.vc;
482
+ const expiresAt = parseExpiresAt(r.expiresAt);
439
483
  return {
440
484
  sessionId: String(r.sessionID),
441
485
  sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
@@ -444,7 +488,9 @@ var AuthApi = class {
444
488
  vc: vc != null ? String(vc) : null,
445
489
  rpChallenge: acsp.rpChallenge,
446
490
  acsp,
447
- receivedAt: Date.now()
491
+ receivedAt: Date.now(),
492
+ sessionType: "auth",
493
+ ...expiresAt !== void 0 ? { expiresAt } : {}
448
494
  };
449
495
  }
450
496
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
@@ -477,6 +523,10 @@ var AuthApi = class {
477
523
  function rec2(v) {
478
524
  return v && typeof v === "object" ? v : {};
479
525
  }
526
+ var vcOf = (r) => {
527
+ const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
528
+ return vc != null ? String(vc) : null;
529
+ };
480
530
  var SignApi = class {
481
531
  constructor(http, creds, defaults) {
482
532
  this.http = http;
@@ -486,46 +536,155 @@ var SignApi = class {
486
536
  http;
487
537
  creds;
488
538
  defaults;
489
- body(digestB64, interactions, opts) {
539
+ body(digestB64, interactions, opts, defAlg) {
540
+ const sign = {
541
+ digest: digestB64,
542
+ hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
543
+ signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
544
+ relyingPartyName: this.creds.rpName,
545
+ interactions: interactionsToBase64(interactions),
546
+ initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
547
+ };
490
548
  const body = {
491
549
  relyingPartyUUID: this.creds.rpUUID,
492
- relyingPartyName: this.creds.rpName,
550
+ relyingPartyName: sign.relyingPartyName,
493
551
  certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
494
552
  signatureProtocol: "RAW_DIGEST_SIGNATURE",
495
553
  signatureProtocolParameters: {
496
554
  digest: digestB64,
497
- signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
498
- signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
555
+ signatureAlgorithm: sign.signatureAlgorithm,
556
+ signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
499
557
  },
500
- interactions: interactionsToBase64(interactions)
558
+ interactions: sign.interactions
501
559
  };
502
- if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
560
+ if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
503
561
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
504
562
  if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
505
- return body;
563
+ return { body, sign };
506
564
  }
507
- parse(raw) {
508
- const r = rec2(raw);
509
- const vc = rec2(r.vc).value;
510
- return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: "" };
565
+ async notification(path, body, sign) {
566
+ const sent = Date.now();
567
+ const r = rec2(await this.http.post(path, body));
568
+ return {
569
+ sessionId: String(r.sessionID),
570
+ vc: vcOf(r),
571
+ rpChallenge: "",
572
+ ...sign ? { sign } : {},
573
+ expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
574
+ };
575
+ }
576
+ async deviceLink(path, body, extra) {
577
+ const sent = Date.now();
578
+ const r = rec2(await this.http.post(path, body));
579
+ return {
580
+ sessionId: String(r.sessionID),
581
+ sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
582
+ sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
583
+ deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
584
+ vc: vcOf(r),
585
+ rpChallenge: "",
586
+ receivedAt: Date.now(),
587
+ ...extra,
588
+ expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
589
+ };
511
590
  }
512
591
  /**
513
592
  * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
514
593
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
594
+ *
595
+ * ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
596
+ * (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
515
597
  */
516
598
  async digestByEtsi(id, digestB64, interactions, opts) {
517
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
518
- return this.parse(raw);
599
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
600
+ return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
519
601
  }
520
- /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
602
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
521
603
  async digestByDocument(documentNumber, digestB64, interactions, opts) {
522
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
523
- return this.parse(raw);
604
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
605
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
524
606
  }
525
607
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
526
608
  async textByEtsi(id, text, interactions, opts) {
527
609
  return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
528
610
  }
611
+ /**
612
+ * QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
613
+ * `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
614
+ * `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
615
+ */
616
+ async deviceLinkByEtsi(id, digestB64, interactions, opts) {
617
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
618
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
619
+ }
620
+ /** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
621
+ async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
622
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
623
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
624
+ }
625
+ // ── Certificate-choice ──
626
+ certChoiceBody(opts) {
627
+ return {
628
+ relyingPartyUUID: this.creds.rpUUID,
629
+ relyingPartyName: this.creds.rpName,
630
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
631
+ };
632
+ }
633
+ /**
634
+ * Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
635
+ * `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
636
+ */
637
+ async certificateChoiceByEtsi(id, opts) {
638
+ return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
639
+ }
640
+ /**
641
+ * QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
642
+ * initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
643
+ */
644
+ async certificateChoiceDeviceLink(opts) {
645
+ return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
646
+ sessionType: "cert",
647
+ relyingPartyName: this.creds.rpName
648
+ });
649
+ }
650
+ // ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
651
+ signerBody(signer, digestB64, interactions, opts) {
652
+ if (!signer?.documentNumber?.trim()) {
653
+ throw new Error("eid: signer.documentNumber \u0445\u043E\u043E\u0441\u043E\u043D \u2014 validateCertificateChoice-\u0438\u0439\u043D \u04AF\u0440 \u0434\u04AF\u043D\u0433 \u04E9\u0433");
654
+ }
655
+ const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
656
+ const want = hashLen(out.sign.hashAlgorithm);
657
+ const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
658
+ if (!d || d.length !== want || d.toString("base64") !== digestB64) {
659
+ throw new Error(`eid: digest \u043D\u044C \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 base64, ${out.sign.hashAlgorithm}-\u0438\u0439\u043D \u0443\u0440\u0442 (${want} \u0431\u0430\u0439\u0442) \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
660
+ }
661
+ return out;
662
+ }
663
+ /**
664
+ * Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
665
+ * etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
666
+ * хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
667
+ *
668
+ * `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
669
+ * бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
670
+ * 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
671
+ * `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
672
+ * s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
673
+ */
674
+ async digestForSigner(signer, digestB64, interactions, opts) {
675
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
676
+ const linked = signer.certChoiceSessionId?.trim();
677
+ if (linked) {
678
+ body.linkedSessionID = linked;
679
+ return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
680
+ }
681
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
682
+ }
683
+ /** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
684
+ async deviceLinkForSigner(signer, digestB64, interactions, opts) {
685
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
686
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
687
+ }
529
688
  };
530
689
 
531
690
  // src/session.ts
@@ -1286,13 +1445,19 @@ var ResponseValidator = class {
1286
1445
  * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
1287
1446
  */
1288
1447
  validateSign(result, digestB64, opts = {}) {
1289
- const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1448
+ const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1449
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
1290
1450
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
1291
1451
  if (expectedFlowType && result.flowType !== expectedFlowType) {
1292
1452
  throw new ValidationError(`flowType ${result.flowType} \u2260 \u044D\u0445\u043B\u04AF\u04AF\u043B\u0441\u044D\u043D \u0443\u0440\u0441\u0433\u0430\u043B ${expectedFlowType} (relay \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)`);
1293
1453
  }
1454
+ if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
1455
+ throw new ValidationError(
1456
+ `\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D certificate-choice-\u043E\u043E\u0440 \u0441\u043E\u043D\u0433\u043E\u0441\u043E\u043D\u0442\u043E\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (serial ${leaf.serialNumber} \u2260 ${o.expectedCertificate.serialNumber})`
1457
+ );
1458
+ }
1294
1459
  const digest = Buffer.from(digestB64, "base64");
1295
- const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
1460
+ const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
1296
1461
  if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1297
1462
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
1298
1463
  throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u04E9\u0433\u0441\u04E9\u043D digest-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
@@ -1306,6 +1471,24 @@ var ResponseValidator = class {
1306
1471
  parsedSubject: parseSubject(leaf)
1307
1472
  };
1308
1473
  }
1474
+ /**
1475
+ * Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
1476
+ * documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
1477
+ * issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
1478
+ * гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
1479
+ */
1480
+ validateCertificateChoice(result, sessionId = null) {
1481
+ this.sessionDone(result);
1482
+ const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
1483
+ return {
1484
+ documentNumber: result.documentNumber,
1485
+ certificate: leaf,
1486
+ certificateDerB64: leaf.raw.toString("base64"),
1487
+ certificateLevel: String(result.certificateLevel ?? ""),
1488
+ parsedSubject: parseSubject(leaf),
1489
+ certChoiceSessionId: sessionId
1490
+ };
1491
+ }
1309
1492
  /**
1310
1493
  * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
1311
1494
  * (downgrade / alg confusion хаалт).
@@ -1355,13 +1538,22 @@ var ResponseValidator = class {
1355
1538
  // ── дотоод ──
1356
1539
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
1357
1540
  precheck(result, protocol) {
1358
- if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1359
- if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1541
+ this.sessionDone(result);
1360
1542
  if (result.signatureProtocol && result.signatureProtocol !== protocol) {
1361
1543
  throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
1362
1544
  }
1363
1545
  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");
1364
1546
  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");
1547
+ return this.leafOf(result, protocol);
1548
+ }
1549
+ /** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
1550
+ sessionDone(result) {
1551
+ if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1552
+ if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1553
+ }
1554
+ /** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
1555
+ leafOf(result, protocol) {
1556
+ 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");
1365
1557
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
1366
1558
  const leaf = new import_node_crypto5.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
1367
1559
  this.checkValidity(leaf);
@@ -1778,7 +1970,7 @@ var EidClient = class {
1778
1970
  });
1779
1971
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1780
1972
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1781
- this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1973
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
1782
1974
  this.pdf = new PdfApi(http, cfg.credentials, {
1783
1975
  certificateLevel: level,
1784
1976
  timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
@@ -1896,6 +2088,37 @@ function buildDeviceLink(p) {
1896
2088
  const authCode = (0, import_node_crypto6.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1897
2089
  return `${unprotected}&authCode=${authCode}`;
1898
2090
  }
2091
+ function deviceLinkInput(s, deviceLinkType, lang) {
2092
+ const base = {
2093
+ deviceLinkBase: s.deviceLinkBase ?? "",
2094
+ deviceLinkType,
2095
+ sessionToken: s.sessionToken ?? "",
2096
+ sessionSecret: s.sessionSecret ?? "",
2097
+ lang,
2098
+ receivedAt: s.receivedAt
2099
+ };
2100
+ if (s.sessionType === "sign" && s.sign) {
2101
+ return {
2102
+ ...base,
2103
+ sessionType: "sign",
2104
+ digest: s.sign.digest,
2105
+ relyingPartyName: s.sign.relyingPartyName,
2106
+ interactions: s.sign.interactions,
2107
+ initialCallbackUrl: s.sign.initialCallbackUrl
2108
+ };
2109
+ }
2110
+ if (s.sessionType === "cert") {
2111
+ return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
2112
+ }
2113
+ return {
2114
+ ...base,
2115
+ sessionType: "auth",
2116
+ rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
2117
+ relyingPartyName: s.acsp?.relyingPartyName ?? "",
2118
+ interactions: s.acsp?.interactions ?? "",
2119
+ initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
2120
+ };
2121
+ }
1899
2122
  function qrDeviceLinkTicker(input, onLink, opts) {
1900
2123
  const clock = opts?.clock ?? Date.now;
1901
2124
  const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
@@ -1932,6 +2155,16 @@ function memoryLoginStore() {
1932
2155
  };
1933
2156
  }
1934
2157
  var CALLBACK_PATH = "/auth/eid/callback";
2158
+ function forwardedClientIp(headers, trustProxy) {
2159
+ if (trustProxy === false || trustProxy === 0) return void 0;
2160
+ const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
2161
+ if (xff.length) {
2162
+ if (trustProxy === true) return xff[0];
2163
+ const n = Math.floor(trustProxy);
2164
+ return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
2165
+ }
2166
+ return header(headers, "x-real-ip").trim() || void 0;
2167
+ }
1935
2168
  var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
1936
2169
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1937
2170
  var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
@@ -1983,11 +2216,12 @@ function safeEqualHex(a, b) {
1983
2216
  const y = Buffer.from(b, "hex");
1984
2217
  return x.length === y.length && x.length > 0 && (0, import_node_crypto7.timingSafeEqual)(x, y);
1985
2218
  }
1986
- function toIdentity(r, v, flowType) {
2219
+ function toIdentity(sessionId, r, v, flowType) {
1987
2220
  const s = v.parsedSubject;
1988
2221
  const etsi = s.serialNumber ?? "";
1989
2222
  const m = /^PNOMN-([0-9]+)$/.exec(etsi);
1990
2223
  return {
2224
+ sessionId,
1991
2225
  etsi,
1992
2226
  civilId: m ? m[1] : null,
1993
2227
  givenName: s.givenName ?? null,
@@ -2155,7 +2389,8 @@ function createEidLogin(opts) {
2155
2389
  const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
2156
2390
  if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
2157
2391
  const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
2158
- if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
2392
+ const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2393
+ if (ip && !await rateLimit(`start:${ip}`)) return limited();
2159
2394
  if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
2160
2395
  let r;
2161
2396
  try {
@@ -2177,7 +2412,7 @@ function createEidLogin(opts) {
2177
2412
  async function finish(req, tx, r, flow, verifier) {
2178
2413
  const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
2179
2414
  if (!await store.delete(tx.sessionId)) return void 0;
2180
- const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
2415
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
2181
2416
  return out;
2182
2417
  }
2183
2418
  async function status(req) {
@@ -2260,11 +2495,20 @@ function createEidLogin(opts) {
2260
2495
  }
2261
2496
  return json(404, { error: "not_found" });
2262
2497
  }
2498
+ let warnedNoIp = false;
2263
2499
  async function handle(req) {
2264
2500
  const headers = {};
2265
2501
  req.headers.forEach((v, k) => headers[k] = v);
2266
2502
  const body = req.method === "POST" ? await req.text() : void 0;
2267
- const r = await handleRequest({ method: req.method, url: req.url, headers, body });
2503
+ const ip = opts.getClientIp?.(req) || void 0;
2504
+ if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
2505
+ warnedNoIp = true;
2506
+ onError(new Error("eid-login: handle(Request)-\u0434 \u043A\u043B\u0438\u0435\u043D\u0442 IP \u0430\u043B\u0433\u0430 \u2014 `start:<ip>` \u0445\u044F\u0437\u0433\u0430\u0430\u0440 \u0430\u0436\u0438\u043B\u043B\u0430\u0445\u0433\u04AF\u0439; getClientIp \u044D\u0441\u0432\u044D\u043B trustProxy \u04E9\u0433"), {
2507
+ request: { method: req.method, url: req.url, headers },
2508
+ mode: "qr"
2509
+ });
2510
+ }
2511
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
2268
2512
  const h = new Headers();
2269
2513
  for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2270
2514
  return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
@@ -2277,20 +2521,24 @@ function createEidLogin(opts) {
2277
2521
  AuthApi,
2278
2522
  AuthenticationError,
2279
2523
  CITIZEN_ISSUING_CA_SKI,
2524
+ CertChoiceInvalidError,
2280
2525
  DEFAULT_BASE_URL,
2281
2526
  DEFAULT_PDF_MAX_BYTES,
2282
2527
  DEFAULT_PDF_TIMEOUT_MS,
2528
+ DEFAULT_SIGN_SESSION_TTL_MS,
2283
2529
  DeviceLinkError,
2284
2530
  EidClient,
2285
2531
  EidError,
2286
2532
  ForbiddenError,
2287
2533
  Http,
2288
2534
  NetworkError,
2535
+ NoSigningCertificateError,
2289
2536
  ON_BEHALF_ERROR_CODES,
2290
2537
  OrganizationApi,
2291
2538
  PdfApi,
2292
2539
  PersonApi,
2293
2540
  RETRYABLE_ERROR_CODES,
2541
+ RequestInvalidError,
2294
2542
  ResponseTooLargeError,
2295
2543
  ResponseValidator,
2296
2544
  SessionApi,
@@ -2303,10 +2551,13 @@ function createEidLogin(opts) {
2303
2551
  buildDeviceLink,
2304
2552
  createEidLogin,
2305
2553
  deviceLink,
2554
+ deviceLinkInput,
2555
+ forwardedClientIp,
2306
2556
  interactionsToBase64,
2307
2557
  isMobileUserAgent,
2308
2558
  memoryLoginStore,
2309
2559
  openOrShowQR,
2560
+ parseExpiresAt,
2310
2561
  parseRetryAfter,
2311
2562
  parseSessionResult,
2312
2563
  parseSubject,
@@ -2314,7 +2565,9 @@ function createEidLogin(opts) {
2314
2565
  qrDeviceLinkTicker,
2315
2566
  randomChallenge,
2316
2567
  resolveBaseUrl,
2568
+ sessionExpiry,
2317
2569
  sha256Base64,
2570
+ startExpiry,
2318
2571
  tokenAuthDigest,
2319
2572
  tokenLimitOf,
2320
2573
  userChallengeOf,