@gerege-systems/eid-mongolia-sdk 0.13.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/index.cjs CHANGED
@@ -27,14 +27,10 @@ __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
- EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
32
- EID_ACR_QUALIFIED: () => EID_ACR_QUALIFIED,
33
- EID_OIDC_ISSUER: () => EID_OIDC_ISSUER,
34
32
  EidClient: () => EidClient,
35
33
  EidError: () => EidError,
36
- EidOidcClient: () => EidOidcClient,
37
- EidOidcError: () => EidOidcError,
38
34
  ForbiddenError: () => ForbiddenError,
39
35
  Http: () => Http,
40
36
  NetworkError: () => NetworkError,
@@ -53,25 +49,30 @@ __export(index_exports, {
53
49
  acspV2Digest: () => acspV2Digest,
54
50
  buildAcspV2Payload: () => buildAcspV2Payload,
55
51
  buildDeviceLink: () => buildDeviceLink,
52
+ createEidLogin: () => createEidLogin,
56
53
  deviceLink: () => deviceLink,
54
+ deviceLinkInput: () => deviceLinkInput,
55
+ forwardedClientIp: () => forwardedClientIp,
57
56
  interactionsToBase64: () => interactionsToBase64,
58
57
  isMobileUserAgent: () => isMobileUserAgent,
58
+ memoryLoginStore: () => memoryLoginStore,
59
59
  openOrShowQR: () => openOrShowQR,
60
60
  parseRetryAfter: () => parseRetryAfter,
61
61
  parseSessionResult: () => parseSessionResult,
62
62
  parseSubject: () => parseSubject,
63
63
  personMatchesCertificate: () => personMatchesCertificate,
64
- pkceChallenge: () => pkceChallenge,
65
64
  qrDeviceLinkTicker: () => qrDeviceLinkTicker,
66
65
  randomChallenge: () => randomChallenge,
67
66
  resolveBaseUrl: () => resolveBaseUrl,
67
+ sessionExpiry: () => sessionExpiry,
68
68
  sha256Base64: () => sha256Base64,
69
69
  tokenAuthDigest: () => tokenAuthDigest,
70
70
  tokenLimitOf: () => tokenLimitOf,
71
71
  userChallengeOf: () => userChallengeOf,
72
72
  verifyPayloadSignature: () => verifyPayloadSignature,
73
73
  verifyPrehashedSignature: () => verifyPrehashedSignature,
74
- verifyTokenCertificates: () => verifyTokenCertificates
74
+ verifyTokenCertificates: () => verifyTokenCertificates,
75
+ withReturnHint: () => withReturnHint
75
76
  });
76
77
  module.exports = __toCommonJS(index_exports);
77
78
 
@@ -321,6 +322,11 @@ var RP_ROUTES = {
321
322
  authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
322
323
  signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
323
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}" },
324
330
  pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
325
331
  pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
326
332
  orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
@@ -350,13 +356,6 @@ function routePath(route, ...args) {
350
356
  if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
351
357
  return out;
352
358
  }
353
- var OIDC_EXPECT = {
354
- defaultScope: "openid",
355
- pkceMethod: "S256",
356
- idTokenAlg: "ES256",
357
- /** `Authorization: Basic` (RFC 6749 §2.3.1). */
358
- tokenEndpointAuthMethod: "client_secret_basic"
359
- };
360
359
 
361
360
  // src/crypto.ts
362
361
  var import_node_crypto = require("crypto");
@@ -454,7 +453,8 @@ var AuthApi = class {
454
453
  vc: vc != null ? String(vc) : null,
455
454
  rpChallenge: acsp.rpChallenge,
456
455
  acsp,
457
- receivedAt: Date.now()
456
+ receivedAt: Date.now(),
457
+ sessionType: "auth"
458
458
  };
459
459
  }
460
460
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
@@ -483,10 +483,20 @@ var AuthApi = class {
483
483
  }
484
484
  };
485
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
+
486
492
  // src/sign.ts
487
493
  function rec2(v) {
488
494
  return v && typeof v === "object" ? v : {};
489
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
+ };
490
500
  var SignApi = class {
491
501
  constructor(http, creds, defaults) {
492
502
  this.http = http;
@@ -496,46 +506,155 @@ var SignApi = class {
496
506
  http;
497
507
  creds;
498
508
  defaults;
499
- 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
+ };
500
518
  const body = {
501
519
  relyingPartyUUID: this.creds.rpUUID,
502
- relyingPartyName: this.creds.rpName,
520
+ relyingPartyName: sign.relyingPartyName,
503
521
  certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
504
522
  signatureProtocol: "RAW_DIGEST_SIGNATURE",
505
523
  signatureProtocolParameters: {
506
524
  digest: digestB64,
507
- signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
508
- signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
525
+ signatureAlgorithm: sign.signatureAlgorithm,
526
+ signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
509
527
  },
510
- interactions: interactionsToBase64(interactions)
528
+ interactions: sign.interactions
511
529
  };
512
- if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
530
+ if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
513
531
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
514
532
  if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
515
- 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
+ };
516
545
  }
517
- parse(raw) {
518
- const r = rec2(raw);
519
- const vc = rec2(r.vc).value;
520
- 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
+ };
521
560
  }
522
561
  /**
523
562
  * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
524
563
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
564
+ *
565
+ * ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
566
+ * (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
525
567
  */
526
568
  async digestByEtsi(id, digestB64, interactions, opts) {
527
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
528
- 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);
529
571
  }
530
- /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
572
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
531
573
  async digestByDocument(documentNumber, digestB64, interactions, opts) {
532
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
533
- 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);
534
576
  }
535
577
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
536
578
  async textByEtsi(id, text, interactions, opts) {
537
579
  return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
538
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
+ }
539
658
  };
540
659
 
541
660
  // src/session.ts
@@ -1296,13 +1415,19 @@ var ResponseValidator = class {
1296
1415
  * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
1297
1416
  */
1298
1417
  validateSign(result, digestB64, opts = {}) {
1299
- 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;
1300
1420
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
1301
1421
  if (expectedFlowType && result.flowType !== expectedFlowType) {
1302
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)`);
1303
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
+ }
1304
1429
  const digest = Buffer.from(digestB64, "base64");
1305
- const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
1430
+ const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
1306
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");
1307
1432
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
1308
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");
@@ -1316,6 +1441,24 @@ var ResponseValidator = class {
1316
1441
  parsedSubject: parseSubject(leaf)
1317
1442
  };
1318
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
+ }
1319
1462
  /**
1320
1463
  * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
1321
1464
  * (downgrade / alg confusion хаалт).
@@ -1365,13 +1508,22 @@ var ResponseValidator = class {
1365
1508
  // ── дотоод ──
1366
1509
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
1367
1510
  precheck(result, protocol) {
1368
- if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1369
- if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1511
+ this.sessionDone(result);
1370
1512
  if (result.signatureProtocol && result.signatureProtocol !== protocol) {
1371
1513
  throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
1372
1514
  }
1373
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");
1374
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");
1375
1527
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
1376
1528
  const leaf = new import_node_crypto5.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
1377
1529
  this.checkValidity(leaf);
@@ -1773,6 +1925,8 @@ var EidClient = class {
1773
1925
  token;
1774
1926
  session;
1775
1927
  validator;
1928
+ /** `credentials.rpName` — иргэний утсанд харагдах RP нэр. */
1929
+ rpName;
1776
1930
  constructor(cfg) {
1777
1931
  if (!cfg.credentials?.apiSecret) throw new Error("EidClient: credentials.apiSecret \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
1778
1932
  const base = resolveBaseUrl(cfg.baseUrl);
@@ -1786,7 +1940,7 @@ var EidClient = class {
1786
1940
  });
1787
1941
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1788
1942
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1789
- this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1943
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
1790
1944
  this.pdf = new PdfApi(http, cfg.credentials, {
1791
1945
  certificateLevel: level,
1792
1946
  timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
@@ -1797,6 +1951,7 @@ var EidClient = class {
1797
1951
  this.token = new TokenApi(http, cfg.credentials);
1798
1952
  this.session = new SessionApi(http);
1799
1953
  this.validator = new ResponseValidator(cfg.trust ?? {});
1954
+ this.rpName = cfg.credentials.rpName;
1800
1955
  }
1801
1956
  };
1802
1957
 
@@ -1903,6 +2058,37 @@ function buildDeviceLink(p) {
1903
2058
  const authCode = (0, import_node_crypto6.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1904
2059
  return `${unprotected}&authCode=${authCode}`;
1905
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
+ }
1906
2092
  function qrDeviceLinkTicker(input, onLink, opts) {
1907
2093
  const clock = opts?.clock ?? Date.now;
1908
2094
  const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
@@ -1912,293 +2098,392 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1912
2098
  return () => clearInterval(t);
1913
2099
  }
1914
2100
 
1915
- // src/oidc.ts
2101
+ // src/login.ts
1916
2102
  var import_node_crypto7 = require("crypto");
1917
- var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1918
- var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1919
- var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
1920
- var CLOCK_SKEW_S = 60;
1921
- var CACHE_MS = 60 * 60 * 1e3;
1922
- var DEFAULT_TIMEOUT_MS2 = 1e4;
1923
- var KNOWN_CODES = /* @__PURE__ */ new Set([
1924
- "access_denied",
1925
- "login_required",
1926
- "invalid_request",
1927
- "invalid_scope",
1928
- "unauthorized_client",
1929
- "unsupported_response_type",
1930
- "request_not_supported",
1931
- "temporarily_unavailable",
1932
- "server_error",
1933
- "invalid_client",
1934
- "invalid_grant",
1935
- "unsupported_grant_type"
1936
- ]);
1937
- var EidOidcError = class extends EidError {
1938
- constructor(code, message, description) {
1939
- super(message);
1940
- this.code = code;
1941
- this.description = description;
2103
+ function memoryLoginStore() {
2104
+ const m = /* @__PURE__ */ new Map();
2105
+ return {
2106
+ get(k) {
2107
+ const e = m.get(k);
2108
+ if (!e) return void 0;
2109
+ if (e.exp <= Date.now()) {
2110
+ m.delete(k);
2111
+ return void 0;
2112
+ }
2113
+ return e.tx;
2114
+ },
2115
+ set(k, tx, ttl) {
2116
+ const now = Date.now();
2117
+ if (m.size > 1e3) {
2118
+ for (const [key, e] of m) if (e.exp <= now) m.delete(key);
2119
+ }
2120
+ m.set(k, { tx, exp: now + ttl });
2121
+ },
2122
+ delete(k) {
2123
+ return m.delete(k);
2124
+ }
2125
+ };
2126
+ }
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
+ }
2138
+ var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
2139
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
2140
+ var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
2141
+ var IOS_BROWSERS = [
2142
+ [/CriOS/i, "googlechromes"],
2143
+ [/EdgiOS/i, "microsoft-edge-https"],
2144
+ [/FxiOS/i, "firefox"],
2145
+ [/OPT\//i, "touch-https"],
2146
+ [/OPiOS|OPR\//i, "opera-https"],
2147
+ [/DuckDuckGo/i, "ddgQuickLink"]
2148
+ ];
2149
+ var ANDROID_BROWSERS = [
2150
+ [/EdgA\//i, "com.microsoft.emmx"],
2151
+ [/Opera Mini/i, "com.opera.mini.native"],
2152
+ [/OPR\//i, "com.opera.browser"],
2153
+ [/SamsungBrowser\//i, "com.sec.android.app.sbrowser"],
2154
+ [/YaBrowser\//i, "com.yandex.browser"],
2155
+ [/DuckDuckGo\//i, "com.duckduckgo.mobile.android"],
2156
+ [/Vivaldi\//i, "com.vivaldi.browser"],
2157
+ [/Firefox\//i, "org.mozilla.firefox"],
2158
+ [/Chrome\//i, "com.android.chrome"]
2159
+ ];
2160
+ function withReturnHint(callbackUrl, userAgent) {
2161
+ const u = new URL(callbackUrl);
2162
+ const pick = (t) => t.find(([re]) => re.test(userAgent))?.[1];
2163
+ if (/iPhone|iPad|iPod/i.test(userAgent)) {
2164
+ const v = pick(IOS_BROWSERS);
2165
+ if (v) u.searchParams.set("retScheme", v);
2166
+ } else if (/Android/i.test(userAgent)) {
2167
+ const v = pick(ANDROID_BROWSERS);
2168
+ if (v) u.searchParams.set("retPkg", v);
1942
2169
  }
1943
- code;
1944
- description;
1945
- };
1946
- var EidOidcClient = class {
1947
- constructor(cfg) {
1948
- this.cfg = cfg;
1949
- if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
1950
- if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
1951
- if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
1952
- this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
1953
- this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
2170
+ return u.toString();
2171
+ }
2172
+ function header(h, name) {
2173
+ const v = h[name] ?? h[name.toLowerCase()];
2174
+ return (Array.isArray(v) ? v[0] : v) ?? "";
2175
+ }
2176
+ function readCookie(h, name) {
2177
+ for (const part of header(h, "cookie").split(";")) {
2178
+ const i = part.indexOf("=");
2179
+ if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
1954
2180
  }
1955
- cfg;
1956
- issuer;
1957
- scope;
1958
- discovery;
1959
- jwks;
1960
- /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
1961
- discover() {
1962
- const c = this.discovery;
1963
- if (c && Date.now() - c.at < CACHE_MS) return c.d;
1964
- const d = this.fetchDiscovery();
1965
- this.discovery = { at: Date.now(), d };
1966
- d.catch(() => {
1967
- if (this.discovery?.d === d) this.discovery = void 0;
1968
- });
1969
- return d;
2181
+ return "";
2182
+ }
2183
+ var sha256hex = (s) => (0, import_node_crypto7.createHash)("sha256").update(s).digest("hex");
2184
+ function safeEqualHex(a, b) {
2185
+ const x = Buffer.from(a, "hex");
2186
+ const y = Buffer.from(b, "hex");
2187
+ return x.length === y.length && x.length > 0 && (0, import_node_crypto7.timingSafeEqual)(x, y);
2188
+ }
2189
+ function toIdentity(sessionId, r, v, flowType) {
2190
+ const s = v.parsedSubject;
2191
+ const etsi = s.serialNumber ?? "";
2192
+ const m = /^PNOMN-([0-9]+)$/.exec(etsi);
2193
+ return {
2194
+ sessionId,
2195
+ etsi,
2196
+ civilId: m ? m[1] : null,
2197
+ givenName: s.givenName ?? null,
2198
+ surname: s.surname ?? null,
2199
+ name: s.name,
2200
+ country: s.country ?? null,
2201
+ documentNumber: v.documentNumber,
2202
+ certificateLevel: v.certificateLevel,
2203
+ certificateDerB64: r.certificateDerB64 ?? "",
2204
+ flowType,
2205
+ person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
2206
+ };
2207
+ }
2208
+ function createEidLogin(opts) {
2209
+ const client = opts.client;
2210
+ if (!opts.onSuccess) throw new Error("createEidLogin: onSuccess \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
2211
+ if (opts.callbackUrl) {
2212
+ const u = new URL(opts.callbackUrl);
2213
+ if (u.protocol !== "https:" || u.pathname !== CALLBACK_PATH) {
2214
+ throw new Error(`createEidLogin: callbackUrl \u043D\u044C https://<\u0442\u0430\u043D\u044B host>${CALLBACK_PATH} \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
2215
+ }
1970
2216
  }
1971
- async fetchDiscovery() {
1972
- const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
1973
- if (!d || d.issuer !== this.issuer) {
1974
- throw new EidOidcError("discovery_failed", `OIDC discovery-\u0438\u0439\u043D issuer \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (\u0445\u04AF\u043B\u044D\u044D\u0441\u044D\u043D ${this.issuer})`);
2217
+ const store = opts.store ?? memoryLoginStore();
2218
+ const ttl = opts.ttlMs ?? 12e4;
2219
+ const pollMs = opts.pollMs ?? 1e3;
2220
+ const cookieName = opts.cookie?.name ?? "eid_login";
2221
+ const cookiePath = opts.cookie?.path ?? "/";
2222
+ const secure = opts.cookie?.secure ?? true;
2223
+ const successRedirect = opts.successRedirect ?? "/";
2224
+ const failureRedirect = opts.failureRedirect ?? "/";
2225
+ const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
2226
+ const pushHits = /* @__PURE__ */ new Map();
2227
+ const rateLimit = opts.rateLimit ?? ((key) => {
2228
+ if (!key.startsWith("push:")) return true;
2229
+ const now = Date.now();
2230
+ const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
2231
+ if (pushHits.size > 1e4) pushHits.clear();
2232
+ if (hits.length >= 3) return false;
2233
+ hits.push(now);
2234
+ pushHits.set(key, hits);
2235
+ return true;
2236
+ });
2237
+ const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
2238
+ const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
2239
+ const json = (status2, body, extra = {}) => ({
2240
+ status: status2,
2241
+ headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...extra },
2242
+ body: JSON.stringify(body)
2243
+ });
2244
+ const cookieAttrs = (maxAge) => `Path=${cookiePath}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` + (opts.cookie?.domain ? `; Domain=${opts.cookie.domain}` : "");
2245
+ const clearCookie = `${cookieName}=; ${cookieAttrs(0)}`;
2246
+ const redirect = (to, extra = {}) => ({
2247
+ status: 303,
2248
+ headers: { Location: to, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", ...extra },
2249
+ body: ""
2250
+ });
2251
+ const failTo = (code) => {
2252
+ const u = new URL(failureRedirect, "http://x");
2253
+ u.searchParams.set("eid_error", code);
2254
+ return /^https?:/i.test(failureRedirect) ? u.toString() : u.pathname + u.search + u.hash;
2255
+ };
2256
+ const mergeHeaders = (a, b) => {
2257
+ const out = { ...a };
2258
+ for (const [k, v] of Object.entries(b ?? {})) {
2259
+ const key = Object.keys(out).find((x) => x.toLowerCase() === k.toLowerCase()) ?? k;
2260
+ const prev = out[key];
2261
+ out[key] = key.toLowerCase() === "set-cookie" && prev ? [...[prev].flat(), ...[v].flat()] : v;
1975
2262
  }
1976
- for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
1977
- if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
1978
- throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
1979
- }
2263
+ return out;
2264
+ };
2265
+ async function loadTx(req) {
2266
+ const raw = readCookie(req.headers, cookieName);
2267
+ const dot = raw.indexOf(".");
2268
+ const sid = raw.slice(0, dot);
2269
+ const binding = raw.slice(dot + 1);
2270
+ if (dot < 0 || !UUID_RE.test(sid) || !B64URL_RE.test(binding)) return void 0;
2271
+ const tx = await store.get(sid);
2272
+ if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
2273
+ if (tx.expiresAt <= Date.now()) {
2274
+ await store.delete(sid);
2275
+ return void 0;
1980
2276
  }
1981
- return d;
1982
- }
1983
- /**
1984
- * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
1985
- * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
1986
- */
1987
- async createAuthorization(opts = {}) {
1988
- const d = await this.discover();
1989
- const state = randomB64url(32);
1990
- const nonce = randomB64url(32);
1991
- const codeVerifier = randomB64url(32);
1992
- const u = new URL(d.authorization_endpoint);
1993
- const p = new URLSearchParams({
1994
- response_type: "code",
1995
- client_id: this.cfg.clientId,
1996
- redirect_uri: this.cfg.redirectUri,
1997
- scope: opts.scope ?? this.scope,
1998
- state,
1999
- nonce,
2000
- code_challenge: await pkceChallenge(codeVerifier),
2001
- code_challenge_method: OIDC_EXPECT.pkceMethod
2277
+ return tx;
2278
+ }
2279
+ function qrLink(tx) {
2280
+ if (tx.mode !== "qr" || !tx.link) return void 0;
2281
+ return buildDeviceLink({
2282
+ deviceLinkBase: tx.link.base,
2283
+ deviceLinkType: "QR",
2284
+ sessionToken: tx.link.token,
2285
+ sessionSecret: tx.link.secret,
2286
+ sessionType: "auth",
2287
+ lang: tx.link.lang,
2288
+ receivedAt: tx.link.receivedAt,
2289
+ rpChallenge: tx.acsp.rpChallenge,
2290
+ relyingPartyName: tx.acsp.relyingPartyName,
2291
+ interactions: tx.acsp.interactions
2002
2292
  });
2003
- if (opts.display) p.set("display", opts.display);
2004
- u.search = p.toString();
2005
- return { url: u.toString(), state, nonce, codeVerifier };
2006
2293
  }
2007
- /**
2008
- * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
2009
- * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
2010
- * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
2011
- */
2012
- async handleCallback(input, checks) {
2013
- const q = callbackParams(input);
2014
- const state = q.get("state") ?? "";
2015
- if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
2016
- throw new EidOidcError("state_mismatch", "OIDC state \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439 \u2014 \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u0433 \u0434\u0430\u0445\u0438\u043D \u044D\u0445\u043B\u04AF\u04AF\u043B\u043D\u044D \u04AF\u04AF");
2017
- }
2018
- const iss = q.get("iss");
2019
- if (iss !== null && iss !== this.issuer) {
2020
- throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
2294
+ async function begin(req, mode, idCode, lang) {
2295
+ const level = opts.certificateLevel ? { certificateLevel: opts.certificateLevel } : {};
2296
+ let tx;
2297
+ let appLink;
2298
+ if (mode === "push") {
2299
+ const s = await client.auth.notificationByEtsi(idCode, interactions, level);
2300
+ tx = { sessionId: s.sessionId, mode, vc: s.vc, acsp: s.acsp };
2301
+ } else {
2302
+ const callbackUrl = mode === "web2app" ? withReturnHint(opts.callbackUrl, header(req.headers, "user-agent")) : void 0;
2303
+ const s = await client.auth.deviceLinkAnonymous(interactions, { ...level, callbackUrl });
2304
+ if (!s.deviceLinkBase || !s.sessionToken || !s.sessionSecret || !s.acsp) throw new Error("eID \u0445\u0430\u0440\u0438\u0443\u043D\u0434 device link \u0442\u0430\u043B\u0431\u0430\u0440 \u0434\u0443\u0442\u0443\u0443");
2305
+ tx = {
2306
+ sessionId: s.sessionId,
2307
+ mode,
2308
+ vc: s.vc,
2309
+ acsp: s.acsp,
2310
+ link: { base: s.deviceLinkBase, token: s.sessionToken, secret: s.sessionSecret, receivedAt: s.receivedAt, lang }
2311
+ };
2312
+ if (mode === "web2app") {
2313
+ appLink = buildDeviceLink({
2314
+ deviceLinkBase: s.deviceLinkBase,
2315
+ deviceLinkType: "Web2App",
2316
+ sessionToken: s.sessionToken,
2317
+ sessionSecret: s.sessionSecret,
2318
+ sessionType: "auth",
2319
+ lang,
2320
+ rpChallenge: s.acsp.rpChallenge,
2321
+ relyingPartyName: s.acsp.relyingPartyName,
2322
+ interactions: s.acsp.interactions,
2323
+ initialCallbackUrl: s.acsp.initialCallbackUrl
2324
+ });
2325
+ }
2021
2326
  }
2022
- const err = q.get("error");
2023
- if (err) {
2024
- const code2 = KNOWN_CODES.has(err) ? err : "server_error";
2025
- throw new EidOidcError(code2, `eID \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442 \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${code2}`, q.get("error_description") ?? void 0);
2327
+ const binding = (0, import_node_crypto7.randomBytes)(32).toString("base64url");
2328
+ const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
2329
+ await store.set(full.sessionId, full, ttl);
2330
+ const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
2331
+ return { tx: full, setCookie, appLink };
2332
+ }
2333
+ async function start(req) {
2334
+ const url = new URL(req.url, "http://x");
2335
+ const site = header(req.headers, "sec-fetch-site");
2336
+ if (site && site !== "same-origin" && site !== "same-site" && site !== "none") return json(403, { error: "cross_site" });
2337
+ let body = {};
2338
+ if (req.method === "GET") {
2339
+ if (url.searchParams.get("mode") !== "web2app") return json(405, { error: "method_not_allowed" });
2340
+ body = { mode: "web2app", lang: url.searchParams.get("lang") ?? void 0 };
2341
+ } else if (req.method === "POST") {
2342
+ if (!header(req.headers, "content-type").toLowerCase().startsWith("application/json")) {
2343
+ return json(415, { error: "json_required" });
2344
+ }
2345
+ try {
2346
+ const b = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
2347
+ if (b && typeof b === "object") body = b;
2348
+ } catch {
2349
+ return json(400, { error: "invalid_body" });
2350
+ }
2351
+ } else {
2352
+ return json(405, { error: "method_not_allowed" });
2026
2353
  }
2027
- const code = q.get("code") ?? "";
2028
- if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
2029
- const idToken = await this.exchangeCode(code, checks.codeVerifier);
2030
- const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
2031
- return { claims, idToken };
2032
- }
2033
- /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
2034
- async exchangeCode(code, codeVerifier) {
2035
- const d = await this.discover();
2036
- const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
2037
- const res = await this.fetch(this.tokenEndpoint(d), {
2038
- method: "POST",
2039
- headers: {
2040
- Authorization: "Basic " + basic,
2041
- "Content-Type": "application/x-www-form-urlencoded",
2042
- Accept: "application/json"
2043
- },
2044
- body: new URLSearchParams({
2045
- grant_type: "authorization_code",
2046
- code,
2047
- redirect_uri: this.cfg.redirectUri,
2048
- code_verifier: codeVerifier
2049
- }).toString()
2050
- });
2051
- let j = {};
2354
+ const mode = body.mode;
2355
+ if (mode !== "qr" && mode !== "push" && mode !== "web2app") return json(400, { error: "invalid_mode" });
2356
+ const lang = body.lang === "en" ? "eng" : "mon";
2357
+ const ctx = { request: req, mode };
2358
+ if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
2359
+ const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
2360
+ if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
2361
+ const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
2362
+ const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2363
+ if (ip && !await rateLimit(`start:${ip}`)) return limited();
2364
+ if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
2365
+ let r;
2052
2366
  try {
2053
- j = await res.json();
2054
- } catch {
2367
+ r = await begin(req, mode, idCode, lang);
2368
+ } catch (e) {
2369
+ onError(e, ctx);
2370
+ const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
2371
+ return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
2055
2372
  }
2056
- if (!res.ok) {
2057
- const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
2058
- throw new EidOidcError(
2059
- e,
2060
- `OIDC token ${res.status}: ${e}`,
2061
- typeof j.error_description === "string" ? j.error_description : void 0
2062
- );
2373
+ if (mode === "web2app") {
2374
+ return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
2063
2375
  }
2064
- if (typeof j.id_token !== "string" || j.id_token === "") {
2065
- throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
2376
+ return json(
2377
+ 200,
2378
+ { mode, vc: r.tx.vc, ...mode === "qr" ? { qr: qrLink(r.tx) } : {}, expiresIn: Math.ceil(ttl / 1e3) },
2379
+ { "Set-Cookie": r.setCookie }
2380
+ );
2381
+ }
2382
+ async function finish(req, tx, r, flow, verifier) {
2383
+ const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
2384
+ if (!await store.delete(tx.sessionId)) return void 0;
2385
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
2386
+ return out;
2387
+ }
2388
+ async function status(req) {
2389
+ if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
2390
+ const tx = await loadTx(req);
2391
+ if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2392
+ const ctx = { request: req, mode: tx.mode };
2393
+ let r;
2394
+ try {
2395
+ r = await client.session.poll(tx.sessionId, pollMs);
2396
+ } catch (e) {
2397
+ if (e instanceof ApiError && e.status === 404) {
2398
+ await store.delete(tx.sessionId);
2399
+ return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2400
+ }
2401
+ onError(e, ctx);
2402
+ return json(502, { error: "eid_unavailable" });
2066
2403
  }
2067
- return j.id_token;
2068
- }
2069
- /** Code солих endpoint: `useMtlsEndpointAliases` + alias байвал alias (RFC 8705 §5), эс бөгөөс `token_endpoint`. */
2070
- tokenEndpoint(d) {
2071
- if (!this.cfg.useMtlsEndpointAliases) return d.token_endpoint;
2072
- const alias = d.mtls_endpoint_aliases?.token_endpoint;
2073
- if (alias === void 0) return d.token_endpoint;
2074
- if (typeof alias !== "string" || !alias.startsWith("https://")) {
2075
- throw new EidOidcError("discovery_failed", "OIDC discovery: mtls_endpoint_aliases.token_endpoint \u0431\u0443\u0440\u0443\u0443");
2404
+ const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
2405
+ if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
2406
+ if (r.endResult !== "OK") {
2407
+ await store.delete(tx.sessionId);
2408
+ return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
2076
2409
  }
2077
- return alias;
2078
- }
2079
- /**
2080
- * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
2081
- * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
2082
- */
2083
- async verifyIdToken(idToken, opts) {
2084
- const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
2085
- const parts = idToken.split(".");
2086
- if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
2087
- const header = decodeJsonPart(parts[0]);
2088
- const payload = decodeJsonPart(parts[1]);
2089
- if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
2090
- if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
2091
- const kid = typeof header.kid === "string" ? header.kid : "";
2092
- if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
2093
- const pick = (ks) => ks.find(
2094
- (k) => k.kid === kid && k.kty === "EC" && k.crv === "P-256" && (k.use === void 0 || k.use === "sig") && (k.alg === void 0 || k.alg === "ES256")
2095
- );
2096
- const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
2097
- if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
2098
- const sig = Buffer.from(parts[2], "base64url");
2099
- if (sig.length !== 64) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
2100
- let ok = false;
2410
+ if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
2101
2411
  try {
2102
- const key = await globalThis.crypto.subtle.importKey(
2103
- "jwk",
2104
- { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
2105
- { name: "ECDSA", namedCurve: "P-256" },
2106
- false,
2107
- ["verify"]
2108
- );
2109
- ok = await globalThis.crypto.subtle.verify(
2110
- { name: "ECDSA", hash: "SHA-256" },
2111
- key,
2112
- sig,
2113
- new TextEncoder().encode(parts[0] + "." + parts[1])
2114
- );
2115
- } catch {
2116
- ok = false;
2412
+ const out = await finish(req, tx, r, tx.mode === "qr" ? "QR" : "Notification");
2413
+ if (!out) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2414
+ return json(200, { state: "OK", redirect: out.redirect, data: out.data }, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
2415
+ } catch (e) {
2416
+ onError(e, ctx);
2417
+ if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
2418
+ await store.delete(tx.sessionId);
2419
+ return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
2117
2420
  }
2118
- if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
2119
- const c = payload;
2120
- const now = Math.floor(Date.now() / 1e3);
2121
- if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
2122
- const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
2123
- if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
2124
- if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
2125
- if (typeof c.exp !== "number" || now > c.exp + CLOCK_SKEW_S) throw bad("\u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D");
2126
- if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
2127
- if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
2128
- if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
2129
- return c;
2130
- }
2131
- keys(force) {
2132
- const c = this.jwks;
2133
- if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
2134
- const keys = this.discover().then(async (d) => {
2135
- const j = await this.getJson(d.jwks_uri);
2136
- return Array.isArray(j?.keys) ? j.keys : [];
2137
- });
2138
- this.jwks = { at: Date.now(), keys };
2139
- keys.catch(() => {
2140
- if (this.jwks?.keys === keys) this.jwks = void 0;
2141
- });
2142
- return keys;
2143
2421
  }
2144
- async getJson(url) {
2145
- const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
2146
- if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
2422
+ async function callback(req) {
2423
+ if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
2424
+ const q = new URL(req.url, "http://x").searchParams;
2425
+ const sid = q.get("sessionId") ?? "";
2426
+ const verifier = q.get("userChallengeVerifier") ?? "";
2427
+ const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
2428
+ const tx = await loadTx(req);
2429
+ if (!tx || tx.sessionId !== sid || tx.mode !== "web2app") return fail2(tx ? "unbound" : "expired");
2430
+ if (!verifier) return fail2("unbound");
2431
+ const ctx = { request: req, mode: tx.mode };
2432
+ let r;
2147
2433
  try {
2148
- return await res.json();
2149
- } catch {
2150
- throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
2434
+ r = await client.session.poll(tx.sessionId, 1e4);
2435
+ } catch (e) {
2436
+ onError(e, ctx);
2437
+ return fail2("eid_unavailable");
2438
+ }
2439
+ if (r.state !== "COMPLETE") return fail2("not_complete");
2440
+ if (r.endResult !== "OK") {
2441
+ await store.delete(tx.sessionId);
2442
+ return fail2(r.endResult ?? "UNKNOWN");
2151
2443
  }
2152
- }
2153
- async fetch(url, init) {
2154
- const f = this.cfg.fetchImpl ?? fetch;
2155
- const req = {
2156
- ...init,
2157
- redirect: "error",
2158
- signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
2159
- };
2160
- if (this.cfg.dispatcher) req.dispatcher = this.cfg.dispatcher;
2161
2444
  try {
2162
- return await f(url, req);
2445
+ const out = await finish(req, tx, r, "Web2App", verifier);
2446
+ if (!out) return fail2("expired");
2447
+ return redirect(out.redirect ?? successRedirect, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
2163
2448
  } catch (e) {
2164
- const err = e;
2165
- if (err.name === "TimeoutError" || err.name === "AbortError") {
2166
- throw new NetworkError(`OIDC \u0445\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${url}`);
2167
- }
2168
- throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
2449
+ onError(e, ctx);
2450
+ if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
2451
+ await store.delete(tx.sessionId);
2452
+ return fail2("VALIDATION");
2169
2453
  }
2170
2454
  }
2171
- };
2172
- async function pkceChallenge(codeVerifier) {
2173
- const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
2174
- return Buffer.from(h).toString("base64url");
2175
- }
2176
- function randomB64url(bytes) {
2177
- return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
2178
- }
2179
- function formEncode(s) {
2180
- return new URLSearchParams({ v: s }).toString().slice(2);
2181
- }
2182
- function timingSafeEqualStr(a, b) {
2183
- const x = Buffer.from(a, "utf8");
2184
- const y = Buffer.from(b, "utf8");
2185
- return x.length === y.length && (0, import_node_crypto7.timingSafeEqual)(x, y);
2186
- }
2187
- function decodeJsonPart(s) {
2188
- try {
2189
- const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
2190
- return v && typeof v === "object" && !Array.isArray(v) ? v : null;
2191
- } catch {
2192
- return null;
2455
+ async function handleRequest(req) {
2456
+ const seg = new URL(req.url, "http://x").pathname.replace(/\/+$/, "").split("/").pop();
2457
+ try {
2458
+ if (seg === "start") return await start(req);
2459
+ if (seg === "status") return await status(req);
2460
+ if (seg === "callback") return await callback(req);
2461
+ } catch (e) {
2462
+ onError(e, { request: req, mode: "qr" });
2463
+ if (e instanceof AuthenticationError || e instanceof ForbiddenError) return json(502, { error: "eid_unavailable" });
2464
+ return json(500, { error: "internal" });
2465
+ }
2466
+ return json(404, { error: "not_found" });
2467
+ }
2468
+ let warnedNoIp = false;
2469
+ async function handle(req) {
2470
+ const headers = {};
2471
+ req.headers.forEach((v, k) => headers[k] = v);
2472
+ const body = req.method === "POST" ? await req.text() : void 0;
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 });
2482
+ const h = new Headers();
2483
+ for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2484
+ return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
2193
2485
  }
2194
- }
2195
- function callbackParams(input) {
2196
- if (input instanceof URLSearchParams) return input;
2197
- if (input instanceof URL) return input.searchParams;
2198
- const s = input.trim();
2199
- if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
2200
- const i = s.indexOf("?");
2201
- return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
2486
+ return { handleRequest, handle };
2202
2487
  }
2203
2488
  // Annotate the CommonJS export names for ESM import in node:
2204
2489
  0 && (module.exports = {
@@ -2209,14 +2494,10 @@ function callbackParams(input) {
2209
2494
  DEFAULT_BASE_URL,
2210
2495
  DEFAULT_PDF_MAX_BYTES,
2211
2496
  DEFAULT_PDF_TIMEOUT_MS,
2497
+ DEFAULT_SIGN_SESSION_TTL_MS,
2212
2498
  DeviceLinkError,
2213
- EID_ACR_ADVANCED,
2214
- EID_ACR_QUALIFIED,
2215
- EID_OIDC_ISSUER,
2216
2499
  EidClient,
2217
2500
  EidError,
2218
- EidOidcClient,
2219
- EidOidcError,
2220
2501
  ForbiddenError,
2221
2502
  Http,
2222
2503
  NetworkError,
@@ -2235,23 +2516,28 @@ function callbackParams(input) {
2235
2516
  acspV2Digest,
2236
2517
  buildAcspV2Payload,
2237
2518
  buildDeviceLink,
2519
+ createEidLogin,
2238
2520
  deviceLink,
2521
+ deviceLinkInput,
2522
+ forwardedClientIp,
2239
2523
  interactionsToBase64,
2240
2524
  isMobileUserAgent,
2525
+ memoryLoginStore,
2241
2526
  openOrShowQR,
2242
2527
  parseRetryAfter,
2243
2528
  parseSessionResult,
2244
2529
  parseSubject,
2245
2530
  personMatchesCertificate,
2246
- pkceChallenge,
2247
2531
  qrDeviceLinkTicker,
2248
2532
  randomChallenge,
2249
2533
  resolveBaseUrl,
2534
+ sessionExpiry,
2250
2535
  sha256Base64,
2251
2536
  tokenAuthDigest,
2252
2537
  tokenLimitOf,
2253
2538
  userChallengeOf,
2254
2539
  verifyPayloadSignature,
2255
2540
  verifyPrehashedSignature,
2256
- verifyTokenCertificates
2541
+ verifyTokenCertificates,
2542
+ withReturnHint
2257
2543
  });