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

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/button.js CHANGED
@@ -436,7 +436,7 @@ var T = {
436
436
  minute: "1 minute left"
437
437
  }
438
438
  };
439
- var CSS = `
439
+ var CSS_BASE = `
440
440
  :host { display: inline-block;
441
441
  --eid-bg: hsl(0 0% 100%); --eid-bg-subtle: hsl(210 40% 98%); --eid-bg-muted: hsl(210 40% 96%);
442
442
  --eid-fg: hsl(222 47% 11%); --eid-fg-muted: hsl(215 19% 35%); --eid-fg-subtle: hsl(215 16% 45%);
@@ -444,22 +444,26 @@ var CSS = `
444
444
  --eid-accent: oklch(0.534 0.2 259); --eid-accent-fg: #fff; --eid-accent-subtle: oklch(0.97 0.03 259);
445
445
  --eid-accent-subtle-fg: oklch(0.474 0.19 259); --eid-ring: oklch(0.604 0.2 259);
446
446
  --eid-warning-bg: hsl(48 100% 96%); --eid-warning-border: hsl(45 95% 78%); --eid-warning-fg: hsl(23 75% 33%);
447
- --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; }
448
- @media (prefers-color-scheme: dark) { :host {
447
+ --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; color-scheme: light; }
448
+ button, input { font-family: inherit; }
449
+ `;
450
+ var DARK_CSS = `:host { color-scheme: dark;
449
451
  --eid-bg: hsl(229 50% 6%); --eid-bg-subtle: hsl(222 47% 9%); --eid-bg-muted: hsl(217 33% 13%);
450
452
  --eid-fg: hsl(210 40% 98%); --eid-fg-muted: hsl(213 27% 84%); --eid-fg-subtle: hsl(215 20% 65%);
451
453
  --eid-border: hsl(217 33% 17%); --eid-border-strong: hsl(215 25% 27%);
452
454
  --eid-accent: oklch(0.64 0.15 259); --eid-accent-fg: hsl(229 50% 6%); --eid-accent-subtle: oklch(0.24 0.06 259);
453
455
  --eid-accent-subtle-fg: oklch(0.82 0.1 259); --eid-ring: oklch(0.68 0.15 259);
454
456
  --eid-warning-bg: hsl(20 60% 10%); --eid-warning-border: hsl(22 70% 26%); --eid-warning-fg: hsl(40 92% 65%);
455
- --eid-danger-fg: hsl(0 91% 71%); } }
457
+ --eid-danger-fg: hsl(0 91% 71%); }
458
+ `;
459
+ var CSS_REST = `
456
460
  *, *::before, *::after { box-sizing: border-box; }
457
461
  [hidden] { display: none !important; }
458
462
  h2, p, ol { margin: 0; }
459
463
  :focus-visible { outline: 2px solid var(--eid-ring); outline-offset: 2px; }
460
464
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
461
465
  .btn { display: inline-flex; width: 100%; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 20px;
462
- border-radius: var(--eid-radius); font: 500 1rem/1.25 inherit; text-decoration: none; border: 1px solid transparent; cursor: pointer;
466
+ border-radius: var(--eid-radius); font-family: inherit; font-weight: 500; font-size: 1rem; line-height: 1.25; text-decoration: none; border: 1px solid transparent; cursor: pointer;
463
467
  transition: background-color 160ms cubic-bezier(0,0,0.2,1); }
464
468
  .btn:disabled { opacity: 0.6; cursor: progress; }
465
469
  .btn-primary { background: var(--eid-accent); color: var(--eid-accent-fg); }
@@ -496,7 +500,7 @@ h2, p, ol { margin: 0; }
496
500
  .field { display: grid; gap: 8px; }
497
501
  .field label { font-weight: 500; }
498
502
  .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--eid-border-strong); border-radius: var(--eid-radius);
499
- background: var(--eid-bg); color: var(--eid-fg); font: 1rem/1.5rem inherit; letter-spacing: 0.02em; text-transform: uppercase; }
503
+ background: var(--eid-bg); color: var(--eid-fg); font-family: inherit; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0.02em; text-transform: uppercase; }
500
504
  .field input::placeholder { color: var(--eid-fg-subtle); text-transform: none; }
501
505
  .field input[aria-invalid="true"] { border-color: var(--eid-danger-fg); }
502
506
  .hint { font-size: 0.8125rem; color: var(--eid-fg-subtle); }
@@ -516,6 +520,17 @@ h2, p, ol { margin: 0; }
516
520
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
517
521
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
518
522
  `;
523
+ var CSS = CSS_BASE + CSS_REST;
524
+ function resolveEidTheme(theme, doc = document) {
525
+ if (theme === "light" || theme === "dark") return theme;
526
+ const html = doc.documentElement;
527
+ const dt = html?.getAttribute("data-theme");
528
+ if (dt === "dark" || dt === "light") return dt;
529
+ if (html?.classList.contains("dark")) return "dark";
530
+ if (html?.classList.contains("light")) return "light";
531
+ const mq = doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
532
+ return mq?.matches ? "dark" : "light";
533
+ }
519
534
  var ID_RE = /^([А-ЯЁӨҮ]{2}[0-9]{8}|[0-9]{8,14})$/;
520
535
  var FOCUSABLE = 'button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
521
536
  function el(tag, attrs = {}, text) {
@@ -527,7 +542,7 @@ function el(tag, attrs = {}, text) {
527
542
  var Base = typeof HTMLElement === "undefined" ? class {
528
543
  } : HTMLElement;
529
544
  var EidLoginButton = class extends Base {
530
- static observedAttributes = ["lang"];
545
+ static observedAttributes = ["lang", "theme"];
531
546
  /** Тест/дотоод: гар утас эсэхийг дарж бичих. */
532
547
  isMobile = () => isMobileUserAgent();
533
548
  /** Тест/дотоод: шилжилт (location.assign). */
@@ -544,6 +559,10 @@ var EidLoginButton = class extends Base {
544
559
  gen = 0;
545
560
  // цонх нээх бүрд нэмэгдэнэ — хуучин fetch-ийн хариуг үл тооно
546
561
  lastFocus = null;
562
+ darkStyle;
563
+ themeObs = null;
564
+ mq = null;
565
+ onScheme = () => this.applyTheme();
547
566
  get t() {
548
567
  return T[this.getAttribute("lang") === "en" ? "en" : "mn"];
549
568
  }
@@ -551,21 +570,52 @@ var EidLoginButton = class extends Base {
551
570
  return this.getAttribute(name) ?? def;
552
571
  }
553
572
  connectedCallback() {
554
- if (this.root) return;
555
- this.root = this.attachShadow({ mode: "open" });
556
- const style = el("style");
557
- style.textContent = CSS;
558
- this.trigger = el("button", { type: "button", class: "btn btn-outline trigger", part: "button", "aria-haspopup": "dialog" });
559
- this.trigger.append(el("img", { src: EID_LOGO_DATA_URI, alt: "", width: "20", height: "20" }), el("span", {}, this.t.button));
560
- this.trigger.addEventListener("click", () => this.start());
561
- this.root.append(style, this.trigger);
562
- }
563
- attributeChangedCallback() {
573
+ if (!this.root) {
574
+ this.root = this.attachShadow({ mode: "open" });
575
+ const style = el("style");
576
+ style.textContent = CSS;
577
+ this.darkStyle = el("style", { "data-eid-theme": "dark" });
578
+ this.trigger = el("button", { type: "button", class: "btn btn-outline trigger", part: "button", "aria-haspopup": "dialog" });
579
+ this.trigger.append(el("img", { src: EID_LOGO_DATA_URI, alt: "", width: "20", height: "20" }), el("span", {}, this.t.button));
580
+ this.trigger.addEventListener("click", () => this.start());
581
+ this.root.append(style, this.darkStyle, this.trigger);
582
+ }
583
+ this.watchTheme();
584
+ this.applyTheme();
585
+ }
586
+ attributeChangedCallback(name) {
587
+ if (name === "theme") {
588
+ if (this.root) this.applyTheme();
589
+ return;
590
+ }
564
591
  const label = this.trigger?.querySelector("span");
565
592
  if (label) label.textContent = this.t.button;
566
593
  }
567
594
  disconnectedCallback() {
568
595
  this.close();
596
+ this.themeObs?.disconnect();
597
+ this.themeObs = null;
598
+ this.mq?.removeEventListener?.("change", this.onScheme);
599
+ this.mq = null;
600
+ }
601
+ /** Одоогийн бодит схем (`theme` + auto үед хуудас/OS). */
602
+ get scheme() {
603
+ return resolveEidTheme(this.getAttribute("theme"));
604
+ }
605
+ applyTheme() {
606
+ const css = this.scheme === "dark" ? DARK_CSS : "";
607
+ if (this.darkStyle.textContent !== css) this.darkStyle.textContent = css;
608
+ }
609
+ /** auto үед хуудасны html[data-theme]/class ба OS-ийн схемийн өөрчлөлтийг дагана. */
610
+ watchTheme() {
611
+ if (!this.themeObs && typeof MutationObserver !== "undefined" && document.documentElement) {
612
+ this.themeObs = new MutationObserver(this.onScheme);
613
+ this.themeObs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
614
+ }
615
+ if (!this.mq && typeof matchMedia === "function") {
616
+ this.mq = matchMedia("(prefers-color-scheme: dark)");
617
+ this.mq.addEventListener?.("change", this.onScheme);
618
+ }
569
619
  }
570
620
  /** Товчны үйлдэл (программаар ч дуудаж болно). */
571
621
  start() {
@@ -909,6 +959,7 @@ function mountEidLoginButton(target, opts) {
909
959
  b.setAttribute("status-url", opts.statusUrl);
910
960
  b.setAttribute("lang", opts.lang ?? "mn");
911
961
  if (opts.mode) b.setAttribute("mode", opts.mode);
962
+ if (opts.theme) b.setAttribute("theme", opts.theme);
912
963
  if (opts.successUrl) b.setAttribute("success-url", opts.successUrl);
913
964
  if (opts.onSuccess) b.addEventListener("eid-login-success", (e) => opts.onSuccess(e.detail));
914
965
  if (opts.onError) b.addEventListener("eid-login-error", (e) => opts.onError(e.detail.error));
@@ -919,5 +970,6 @@ defineEidLoginButton();
919
970
  export {
920
971
  EidLoginButton,
921
972
  defineEidLoginButton,
922
- mountEidLoginButton
973
+ mountEidLoginButton,
974
+ resolveEidTheme
923
975
  };
package/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
28
28
  DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
29
29
  DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
30
+ DEFAULT_SIGN_SESSION_TTL_MS: () => DEFAULT_SIGN_SESSION_TTL_MS,
30
31
  DeviceLinkError: () => DeviceLinkError,
31
32
  EidClient: () => EidClient,
32
33
  EidError: () => EidError,
@@ -50,6 +51,8 @@ __export(index_exports, {
50
51
  buildDeviceLink: () => buildDeviceLink,
51
52
  createEidLogin: () => createEidLogin,
52
53
  deviceLink: () => deviceLink,
54
+ deviceLinkInput: () => deviceLinkInput,
55
+ forwardedClientIp: () => forwardedClientIp,
53
56
  interactionsToBase64: () => interactionsToBase64,
54
57
  isMobileUserAgent: () => isMobileUserAgent,
55
58
  memoryLoginStore: () => memoryLoginStore,
@@ -61,6 +64,7 @@ __export(index_exports, {
61
64
  qrDeviceLinkTicker: () => qrDeviceLinkTicker,
62
65
  randomChallenge: () => randomChallenge,
63
66
  resolveBaseUrl: () => resolveBaseUrl,
67
+ sessionExpiry: () => sessionExpiry,
64
68
  sha256Base64: () => sha256Base64,
65
69
  tokenAuthDigest: () => tokenAuthDigest,
66
70
  tokenLimitOf: () => tokenLimitOf,
@@ -318,6 +322,11 @@ var RP_ROUTES = {
318
322
  authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
319
323
  signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
320
324
  signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
325
+ signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
326
+ signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
327
+ signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
328
+ certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
329
+ certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
321
330
  pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
322
331
  pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
323
332
  orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
@@ -444,7 +453,8 @@ var AuthApi = class {
444
453
  vc: vc != null ? String(vc) : null,
445
454
  rpChallenge: acsp.rpChallenge,
446
455
  acsp,
447
- receivedAt: Date.now()
456
+ receivedAt: Date.now(),
457
+ sessionType: "auth"
448
458
  };
449
459
  }
450
460
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
@@ -473,10 +483,20 @@ var AuthApi = class {
473
483
  }
474
484
  };
475
485
 
486
+ // src/types.ts
487
+ var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
488
+ function sessionExpiry(sentAtMs, ttlMs) {
489
+ return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
490
+ }
491
+
476
492
  // src/sign.ts
477
493
  function rec2(v) {
478
494
  return v && typeof v === "object" ? v : {};
479
495
  }
496
+ var vcOf = (r) => {
497
+ const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
498
+ return vc != null ? String(vc) : null;
499
+ };
480
500
  var SignApi = class {
481
501
  constructor(http, creds, defaults) {
482
502
  this.http = http;
@@ -486,46 +506,155 @@ var SignApi = class {
486
506
  http;
487
507
  creds;
488
508
  defaults;
489
- body(digestB64, interactions, opts) {
509
+ body(digestB64, interactions, opts, defAlg) {
510
+ const sign = {
511
+ digest: digestB64,
512
+ hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
513
+ signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
514
+ relyingPartyName: this.creds.rpName,
515
+ interactions: interactionsToBase64(interactions),
516
+ initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
517
+ };
490
518
  const body = {
491
519
  relyingPartyUUID: this.creds.rpUUID,
492
- relyingPartyName: this.creds.rpName,
520
+ relyingPartyName: sign.relyingPartyName,
493
521
  certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
494
522
  signatureProtocol: "RAW_DIGEST_SIGNATURE",
495
523
  signatureProtocolParameters: {
496
524
  digest: digestB64,
497
- signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
498
- signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
525
+ signatureAlgorithm: sign.signatureAlgorithm,
526
+ signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
499
527
  },
500
- interactions: interactionsToBase64(interactions)
528
+ interactions: sign.interactions
501
529
  };
502
- if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
530
+ if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
503
531
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
504
532
  if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
505
- return body;
533
+ return { body, sign };
534
+ }
535
+ async notification(path, body, sign) {
536
+ const sent = Date.now();
537
+ const r = rec2(await this.http.post(path, body));
538
+ return {
539
+ sessionId: String(r.sessionID),
540
+ vc: vcOf(r),
541
+ rpChallenge: "",
542
+ ...sign ? { sign } : {},
543
+ expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
544
+ };
506
545
  }
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: "" };
546
+ async deviceLink(path, body, extra) {
547
+ const sent = Date.now();
548
+ const r = rec2(await this.http.post(path, body));
549
+ return {
550
+ sessionId: String(r.sessionID),
551
+ sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
552
+ sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
553
+ deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
554
+ vc: vcOf(r),
555
+ rpChallenge: "",
556
+ receivedAt: Date.now(),
557
+ ...extra,
558
+ expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
559
+ };
511
560
  }
512
561
  /**
513
562
  * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
514
563
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
564
+ *
565
+ * ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
566
+ * (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
515
567
  */
516
568
  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);
569
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
570
+ return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
519
571
  }
520
- /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
572
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
521
573
  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);
574
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
575
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
524
576
  }
525
577
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
526
578
  async textByEtsi(id, text, interactions, opts) {
527
579
  return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
528
580
  }
581
+ /**
582
+ * QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
583
+ * `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
584
+ * `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
585
+ */
586
+ async deviceLinkByEtsi(id, digestB64, interactions, opts) {
587
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
588
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
589
+ }
590
+ /** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
591
+ async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
592
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
593
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
594
+ }
595
+ // ── Certificate-choice ──
596
+ certChoiceBody(opts) {
597
+ return {
598
+ relyingPartyUUID: this.creds.rpUUID,
599
+ relyingPartyName: this.creds.rpName,
600
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
601
+ };
602
+ }
603
+ /**
604
+ * Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
605
+ * `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
606
+ */
607
+ async certificateChoiceByEtsi(id, opts) {
608
+ return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
609
+ }
610
+ /**
611
+ * QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
612
+ * initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
613
+ */
614
+ async certificateChoiceDeviceLink(opts) {
615
+ return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
616
+ sessionType: "cert",
617
+ relyingPartyName: this.creds.rpName
618
+ });
619
+ }
620
+ // ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
621
+ signerBody(signer, digestB64, interactions, opts) {
622
+ if (!signer?.documentNumber?.trim()) {
623
+ 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");
624
+ }
625
+ const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
626
+ const want = hashLen(out.sign.hashAlgorithm);
627
+ const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
628
+ if (!d || d.length !== want || d.toString("base64") !== digestB64) {
629
+ 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`);
630
+ }
631
+ return out;
632
+ }
633
+ /**
634
+ * Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
635
+ * etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
636
+ * хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
637
+ *
638
+ * `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
639
+ * бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
640
+ * 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
641
+ * `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
642
+ * s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
643
+ */
644
+ async digestForSigner(signer, digestB64, interactions, opts) {
645
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
646
+ const linked = signer.certChoiceSessionId?.trim();
647
+ if (linked) {
648
+ body.linkedSessionID = linked;
649
+ return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
650
+ }
651
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
652
+ }
653
+ /** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
654
+ async deviceLinkForSigner(signer, digestB64, interactions, opts) {
655
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
656
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
657
+ }
529
658
  };
530
659
 
531
660
  // src/session.ts
@@ -1286,13 +1415,19 @@ var ResponseValidator = class {
1286
1415
  * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
1287
1416
  */
1288
1417
  validateSign(result, digestB64, opts = {}) {
1289
- const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1418
+ const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1419
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
1290
1420
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
1291
1421
  if (expectedFlowType && result.flowType !== expectedFlowType) {
1292
1422
  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
1423
  }
1424
+ if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
1425
+ throw new ValidationError(
1426
+ `\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})`
1427
+ );
1428
+ }
1294
1429
  const digest = Buffer.from(digestB64, "base64");
1295
- const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
1430
+ const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
1296
1431
  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
1432
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
1298
1433
  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 +1441,24 @@ var ResponseValidator = class {
1306
1441
  parsedSubject: parseSubject(leaf)
1307
1442
  };
1308
1443
  }
1444
+ /**
1445
+ * Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
1446
+ * documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
1447
+ * issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
1448
+ * гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
1449
+ */
1450
+ validateCertificateChoice(result, sessionId = null) {
1451
+ this.sessionDone(result);
1452
+ const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
1453
+ return {
1454
+ documentNumber: result.documentNumber,
1455
+ certificate: leaf,
1456
+ certificateDerB64: leaf.raw.toString("base64"),
1457
+ certificateLevel: String(result.certificateLevel ?? ""),
1458
+ parsedSubject: parseSubject(leaf),
1459
+ certChoiceSessionId: sessionId
1460
+ };
1461
+ }
1309
1462
  /**
1310
1463
  * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
1311
1464
  * (downgrade / alg confusion хаалт).
@@ -1355,13 +1508,22 @@ var ResponseValidator = class {
1355
1508
  // ── дотоод ──
1356
1509
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
1357
1510
  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");
1511
+ this.sessionDone(result);
1360
1512
  if (result.signatureProtocol && result.signatureProtocol !== protocol) {
1361
1513
  throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
1362
1514
  }
1363
1515
  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
1516
  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");
1517
+ return this.leafOf(result, protocol);
1518
+ }
1519
+ /** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
1520
+ sessionDone(result) {
1521
+ if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1522
+ if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1523
+ }
1524
+ /** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
1525
+ leafOf(result, protocol) {
1526
+ 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
1527
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
1366
1528
  const leaf = new import_node_crypto5.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
1367
1529
  this.checkValidity(leaf);
@@ -1778,7 +1940,7 @@ var EidClient = class {
1778
1940
  });
1779
1941
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1780
1942
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1781
- this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1943
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
1782
1944
  this.pdf = new PdfApi(http, cfg.credentials, {
1783
1945
  certificateLevel: level,
1784
1946
  timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
@@ -1896,6 +2058,37 @@ function buildDeviceLink(p) {
1896
2058
  const authCode = (0, import_node_crypto6.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1897
2059
  return `${unprotected}&authCode=${authCode}`;
1898
2060
  }
2061
+ function deviceLinkInput(s, deviceLinkType, lang) {
2062
+ const base = {
2063
+ deviceLinkBase: s.deviceLinkBase ?? "",
2064
+ deviceLinkType,
2065
+ sessionToken: s.sessionToken ?? "",
2066
+ sessionSecret: s.sessionSecret ?? "",
2067
+ lang,
2068
+ receivedAt: s.receivedAt
2069
+ };
2070
+ if (s.sessionType === "sign" && s.sign) {
2071
+ return {
2072
+ ...base,
2073
+ sessionType: "sign",
2074
+ digest: s.sign.digest,
2075
+ relyingPartyName: s.sign.relyingPartyName,
2076
+ interactions: s.sign.interactions,
2077
+ initialCallbackUrl: s.sign.initialCallbackUrl
2078
+ };
2079
+ }
2080
+ if (s.sessionType === "cert") {
2081
+ return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
2082
+ }
2083
+ return {
2084
+ ...base,
2085
+ sessionType: "auth",
2086
+ rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
2087
+ relyingPartyName: s.acsp?.relyingPartyName ?? "",
2088
+ interactions: s.acsp?.interactions ?? "",
2089
+ initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
2090
+ };
2091
+ }
1899
2092
  function qrDeviceLinkTicker(input, onLink, opts) {
1900
2093
  const clock = opts?.clock ?? Date.now;
1901
2094
  const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
@@ -1932,6 +2125,16 @@ function memoryLoginStore() {
1932
2125
  };
1933
2126
  }
1934
2127
  var CALLBACK_PATH = "/auth/eid/callback";
2128
+ function forwardedClientIp(headers, trustProxy) {
2129
+ if (trustProxy === false || trustProxy === 0) return void 0;
2130
+ const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
2131
+ if (xff.length) {
2132
+ if (trustProxy === true) return xff[0];
2133
+ const n = Math.floor(trustProxy);
2134
+ return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
2135
+ }
2136
+ return header(headers, "x-real-ip").trim() || void 0;
2137
+ }
1935
2138
  var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
1936
2139
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1937
2140
  var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
@@ -1983,11 +2186,12 @@ function safeEqualHex(a, b) {
1983
2186
  const y = Buffer.from(b, "hex");
1984
2187
  return x.length === y.length && x.length > 0 && (0, import_node_crypto7.timingSafeEqual)(x, y);
1985
2188
  }
1986
- function toIdentity(r, v, flowType) {
2189
+ function toIdentity(sessionId, r, v, flowType) {
1987
2190
  const s = v.parsedSubject;
1988
2191
  const etsi = s.serialNumber ?? "";
1989
2192
  const m = /^PNOMN-([0-9]+)$/.exec(etsi);
1990
2193
  return {
2194
+ sessionId,
1991
2195
  etsi,
1992
2196
  civilId: m ? m[1] : null,
1993
2197
  givenName: s.givenName ?? null,
@@ -2155,7 +2359,8 @@ function createEidLogin(opts) {
2155
2359
  const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
2156
2360
  if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
2157
2361
  const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
2158
- if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
2362
+ const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2363
+ if (ip && !await rateLimit(`start:${ip}`)) return limited();
2159
2364
  if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
2160
2365
  let r;
2161
2366
  try {
@@ -2177,7 +2382,7 @@ function createEidLogin(opts) {
2177
2382
  async function finish(req, tx, r, flow, verifier) {
2178
2383
  const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
2179
2384
  if (!await store.delete(tx.sessionId)) return void 0;
2180
- const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
2385
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
2181
2386
  return out;
2182
2387
  }
2183
2388
  async function status(req) {
@@ -2260,11 +2465,20 @@ function createEidLogin(opts) {
2260
2465
  }
2261
2466
  return json(404, { error: "not_found" });
2262
2467
  }
2468
+ let warnedNoIp = false;
2263
2469
  async function handle(req) {
2264
2470
  const headers = {};
2265
2471
  req.headers.forEach((v, k) => headers[k] = v);
2266
2472
  const body = req.method === "POST" ? await req.text() : void 0;
2267
- const r = await handleRequest({ method: req.method, url: req.url, headers, body });
2473
+ const ip = opts.getClientIp?.(req) || void 0;
2474
+ if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
2475
+ warnedNoIp = true;
2476
+ 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"), {
2477
+ request: { method: req.method, url: req.url, headers },
2478
+ mode: "qr"
2479
+ });
2480
+ }
2481
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
2268
2482
  const h = new Headers();
2269
2483
  for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2270
2484
  return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
@@ -2280,6 +2494,7 @@ function createEidLogin(opts) {
2280
2494
  DEFAULT_BASE_URL,
2281
2495
  DEFAULT_PDF_MAX_BYTES,
2282
2496
  DEFAULT_PDF_TIMEOUT_MS,
2497
+ DEFAULT_SIGN_SESSION_TTL_MS,
2283
2498
  DeviceLinkError,
2284
2499
  EidClient,
2285
2500
  EidError,
@@ -2303,6 +2518,8 @@ function createEidLogin(opts) {
2303
2518
  buildDeviceLink,
2304
2519
  createEidLogin,
2305
2520
  deviceLink,
2521
+ deviceLinkInput,
2522
+ forwardedClientIp,
2306
2523
  interactionsToBase64,
2307
2524
  isMobileUserAgent,
2308
2525
  memoryLoginStore,
@@ -2314,6 +2531,7 @@ function createEidLogin(opts) {
2314
2531
  qrDeviceLinkTicker,
2315
2532
  randomChallenge,
2316
2533
  resolveBaseUrl,
2534
+ sessionExpiry,
2317
2535
  sha256Base64,
2318
2536
  tokenAuthDigest,
2319
2537
  tokenLimitOf,