@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/index.js CHANGED
@@ -138,6 +138,11 @@ var RP_ROUTES = {
138
138
  authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
139
139
  signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
140
140
  signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
141
+ signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
142
+ signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
143
+ signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
144
+ certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
145
+ certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
141
146
  pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
142
147
  pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
143
148
  orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
@@ -264,7 +269,8 @@ var AuthApi = class {
264
269
  vc: vc != null ? String(vc) : null,
265
270
  rpChallenge: acsp.rpChallenge,
266
271
  acsp,
267
- receivedAt: Date.now()
272
+ receivedAt: Date.now(),
273
+ sessionType: "auth"
268
274
  };
269
275
  }
270
276
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
@@ -293,10 +299,20 @@ var AuthApi = class {
293
299
  }
294
300
  };
295
301
 
302
+ // src/types.ts
303
+ var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
304
+ function sessionExpiry(sentAtMs, ttlMs) {
305
+ return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
306
+ }
307
+
296
308
  // src/sign.ts
297
309
  function rec2(v) {
298
310
  return v && typeof v === "object" ? v : {};
299
311
  }
312
+ var vcOf = (r) => {
313
+ const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
314
+ return vc != null ? String(vc) : null;
315
+ };
300
316
  var SignApi = class {
301
317
  constructor(http, creds, defaults) {
302
318
  this.http = http;
@@ -306,46 +322,155 @@ var SignApi = class {
306
322
  http;
307
323
  creds;
308
324
  defaults;
309
- body(digestB64, interactions, opts) {
325
+ body(digestB64, interactions, opts, defAlg) {
326
+ const sign = {
327
+ digest: digestB64,
328
+ hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
329
+ signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
330
+ relyingPartyName: this.creds.rpName,
331
+ interactions: interactionsToBase64(interactions),
332
+ initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
333
+ };
310
334
  const body = {
311
335
  relyingPartyUUID: this.creds.rpUUID,
312
- relyingPartyName: this.creds.rpName,
336
+ relyingPartyName: sign.relyingPartyName,
313
337
  certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
314
338
  signatureProtocol: "RAW_DIGEST_SIGNATURE",
315
339
  signatureProtocolParameters: {
316
340
  digest: digestB64,
317
- signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
318
- signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
341
+ signatureAlgorithm: sign.signatureAlgorithm,
342
+ signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
319
343
  },
320
- interactions: interactionsToBase64(interactions)
344
+ interactions: sign.interactions
321
345
  };
322
- if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
346
+ if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
323
347
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
324
348
  if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
325
- return body;
349
+ return { body, sign };
350
+ }
351
+ async notification(path, body, sign) {
352
+ const sent = Date.now();
353
+ const r = rec2(await this.http.post(path, body));
354
+ return {
355
+ sessionId: String(r.sessionID),
356
+ vc: vcOf(r),
357
+ rpChallenge: "",
358
+ ...sign ? { sign } : {},
359
+ expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
360
+ };
326
361
  }
327
- parse(raw) {
328
- const r = rec2(raw);
329
- const vc = rec2(r.vc).value;
330
- return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: "" };
362
+ async deviceLink(path, body, extra) {
363
+ const sent = Date.now();
364
+ const r = rec2(await this.http.post(path, body));
365
+ return {
366
+ sessionId: String(r.sessionID),
367
+ sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
368
+ sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
369
+ deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
370
+ vc: vcOf(r),
371
+ rpChallenge: "",
372
+ receivedAt: Date.now(),
373
+ ...extra,
374
+ expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
375
+ };
331
376
  }
332
377
  /**
333
378
  * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
334
379
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
380
+ *
381
+ * ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
382
+ * (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
335
383
  */
336
384
  async digestByEtsi(id, digestB64, interactions, opts) {
337
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
338
- return this.parse(raw);
385
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
386
+ return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
339
387
  }
340
- /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
388
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
341
389
  async digestByDocument(documentNumber, digestB64, interactions, opts) {
342
- const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
343
- return this.parse(raw);
390
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
391
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
344
392
  }
345
393
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
346
394
  async textByEtsi(id, text, interactions, opts) {
347
395
  return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
348
396
  }
397
+ /**
398
+ * QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
399
+ * `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
400
+ * `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
401
+ */
402
+ async deviceLinkByEtsi(id, digestB64, interactions, opts) {
403
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
404
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
405
+ }
406
+ /** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
407
+ async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
408
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
409
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
410
+ }
411
+ // ── Certificate-choice ──
412
+ certChoiceBody(opts) {
413
+ return {
414
+ relyingPartyUUID: this.creds.rpUUID,
415
+ relyingPartyName: this.creds.rpName,
416
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
417
+ };
418
+ }
419
+ /**
420
+ * Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
421
+ * `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
422
+ */
423
+ async certificateChoiceByEtsi(id, opts) {
424
+ return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
425
+ }
426
+ /**
427
+ * QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
428
+ * initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
429
+ */
430
+ async certificateChoiceDeviceLink(opts) {
431
+ return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
432
+ sessionType: "cert",
433
+ relyingPartyName: this.creds.rpName
434
+ });
435
+ }
436
+ // ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
437
+ signerBody(signer, digestB64, interactions, opts) {
438
+ if (!signer?.documentNumber?.trim()) {
439
+ 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");
440
+ }
441
+ const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
442
+ const want = hashLen(out.sign.hashAlgorithm);
443
+ const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
444
+ if (!d || d.length !== want || d.toString("base64") !== digestB64) {
445
+ 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`);
446
+ }
447
+ return out;
448
+ }
449
+ /**
450
+ * Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
451
+ * etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
452
+ * хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
453
+ *
454
+ * `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
455
+ * бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
456
+ * 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
457
+ * `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
458
+ * s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
459
+ */
460
+ async digestForSigner(signer, digestB64, interactions, opts) {
461
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
462
+ const linked = signer.certChoiceSessionId?.trim();
463
+ if (linked) {
464
+ body.linkedSessionID = linked;
465
+ return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
466
+ }
467
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
468
+ }
469
+ /** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
470
+ async deviceLinkForSigner(signer, digestB64, interactions, opts) {
471
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
472
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
473
+ }
349
474
  };
350
475
 
351
476
  // src/session.ts
@@ -1106,13 +1231,19 @@ var ResponseValidator = class {
1106
1231
  * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
1107
1232
  */
1108
1233
  validateSign(result, digestB64, opts = {}) {
1109
- const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1234
+ const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1235
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
1110
1236
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
1111
1237
  if (expectedFlowType && result.flowType !== expectedFlowType) {
1112
1238
  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)`);
1113
1239
  }
1240
+ if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
1241
+ throw new ValidationError(
1242
+ `\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})`
1243
+ );
1244
+ }
1114
1245
  const digest = Buffer.from(digestB64, "base64");
1115
- const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
1246
+ const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
1116
1247
  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");
1117
1248
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
1118
1249
  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");
@@ -1126,6 +1257,24 @@ var ResponseValidator = class {
1126
1257
  parsedSubject: parseSubject(leaf)
1127
1258
  };
1128
1259
  }
1260
+ /**
1261
+ * Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
1262
+ * documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
1263
+ * issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
1264
+ * гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
1265
+ */
1266
+ validateCertificateChoice(result, sessionId = null) {
1267
+ this.sessionDone(result);
1268
+ const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
1269
+ return {
1270
+ documentNumber: result.documentNumber,
1271
+ certificate: leaf,
1272
+ certificateDerB64: leaf.raw.toString("base64"),
1273
+ certificateLevel: String(result.certificateLevel ?? ""),
1274
+ parsedSubject: parseSubject(leaf),
1275
+ certChoiceSessionId: sessionId
1276
+ };
1277
+ }
1129
1278
  /**
1130
1279
  * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
1131
1280
  * (downgrade / alg confusion хаалт).
@@ -1175,13 +1324,22 @@ var ResponseValidator = class {
1175
1324
  // ── дотоод ──
1176
1325
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
1177
1326
  precheck(result, protocol) {
1178
- if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1179
- if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1327
+ this.sessionDone(result);
1180
1328
  if (result.signatureProtocol && result.signatureProtocol !== protocol) {
1181
1329
  throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
1182
1330
  }
1183
1331
  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");
1184
1332
  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");
1333
+ return this.leafOf(result, protocol);
1334
+ }
1335
+ /** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
1336
+ sessionDone(result) {
1337
+ if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1338
+ if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1339
+ }
1340
+ /** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
1341
+ leafOf(result, protocol) {
1342
+ 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");
1185
1343
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
1186
1344
  const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
1187
1345
  this.checkValidity(leaf);
@@ -1598,7 +1756,7 @@ var EidClient = class {
1598
1756
  });
1599
1757
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1600
1758
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1601
- this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1759
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
1602
1760
  this.pdf = new PdfApi(http, cfg.credentials, {
1603
1761
  certificateLevel: level,
1604
1762
  timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
@@ -1674,6 +1832,37 @@ function buildDeviceLink(p) {
1674
1832
  const authCode = createHmac("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1675
1833
  return `${unprotected}&authCode=${authCode}`;
1676
1834
  }
1835
+ function deviceLinkInput(s, deviceLinkType, lang) {
1836
+ const base = {
1837
+ deviceLinkBase: s.deviceLinkBase ?? "",
1838
+ deviceLinkType,
1839
+ sessionToken: s.sessionToken ?? "",
1840
+ sessionSecret: s.sessionSecret ?? "",
1841
+ lang,
1842
+ receivedAt: s.receivedAt
1843
+ };
1844
+ if (s.sessionType === "sign" && s.sign) {
1845
+ return {
1846
+ ...base,
1847
+ sessionType: "sign",
1848
+ digest: s.sign.digest,
1849
+ relyingPartyName: s.sign.relyingPartyName,
1850
+ interactions: s.sign.interactions,
1851
+ initialCallbackUrl: s.sign.initialCallbackUrl
1852
+ };
1853
+ }
1854
+ if (s.sessionType === "cert") {
1855
+ return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
1856
+ }
1857
+ return {
1858
+ ...base,
1859
+ sessionType: "auth",
1860
+ rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
1861
+ relyingPartyName: s.acsp?.relyingPartyName ?? "",
1862
+ interactions: s.acsp?.interactions ?? "",
1863
+ initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
1864
+ };
1865
+ }
1677
1866
  function qrDeviceLinkTicker(input, onLink, opts) {
1678
1867
  const clock = opts?.clock ?? Date.now;
1679
1868
  const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
@@ -1710,6 +1899,16 @@ function memoryLoginStore() {
1710
1899
  };
1711
1900
  }
1712
1901
  var CALLBACK_PATH = "/auth/eid/callback";
1902
+ function forwardedClientIp(headers, trustProxy) {
1903
+ if (trustProxy === false || trustProxy === 0) return void 0;
1904
+ const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
1905
+ if (xff.length) {
1906
+ if (trustProxy === true) return xff[0];
1907
+ const n = Math.floor(trustProxy);
1908
+ return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
1909
+ }
1910
+ return header(headers, "x-real-ip").trim() || void 0;
1911
+ }
1713
1912
  var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
1714
1913
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1715
1914
  var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
@@ -1761,11 +1960,12 @@ function safeEqualHex(a, b) {
1761
1960
  const y = Buffer.from(b, "hex");
1762
1961
  return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
1763
1962
  }
1764
- function toIdentity(r, v, flowType) {
1963
+ function toIdentity(sessionId, r, v, flowType) {
1765
1964
  const s = v.parsedSubject;
1766
1965
  const etsi = s.serialNumber ?? "";
1767
1966
  const m = /^PNOMN-([0-9]+)$/.exec(etsi);
1768
1967
  return {
1968
+ sessionId,
1769
1969
  etsi,
1770
1970
  civilId: m ? m[1] : null,
1771
1971
  givenName: s.givenName ?? null,
@@ -1933,7 +2133,8 @@ function createEidLogin(opts) {
1933
2133
  const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
1934
2134
  if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
1935
2135
  const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
1936
- if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
2136
+ const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2137
+ if (ip && !await rateLimit(`start:${ip}`)) return limited();
1937
2138
  if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
1938
2139
  let r;
1939
2140
  try {
@@ -1955,7 +2156,7 @@ function createEidLogin(opts) {
1955
2156
  async function finish(req, tx, r, flow, verifier) {
1956
2157
  const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
1957
2158
  if (!await store.delete(tx.sessionId)) return void 0;
1958
- const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
2159
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
1959
2160
  return out;
1960
2161
  }
1961
2162
  async function status(req) {
@@ -2038,11 +2239,20 @@ function createEidLogin(opts) {
2038
2239
  }
2039
2240
  return json(404, { error: "not_found" });
2040
2241
  }
2242
+ let warnedNoIp = false;
2041
2243
  async function handle(req) {
2042
2244
  const headers = {};
2043
2245
  req.headers.forEach((v, k) => headers[k] = v);
2044
2246
  const body = req.method === "POST" ? await req.text() : void 0;
2045
- const r = await handleRequest({ method: req.method, url: req.url, headers, body });
2247
+ const ip = opts.getClientIp?.(req) || void 0;
2248
+ if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
2249
+ warnedNoIp = true;
2250
+ 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"), {
2251
+ request: { method: req.method, url: req.url, headers },
2252
+ mode: "qr"
2253
+ });
2254
+ }
2255
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
2046
2256
  const h = new Headers();
2047
2257
  for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2048
2258
  return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
@@ -2057,6 +2267,7 @@ export {
2057
2267
  DEFAULT_BASE_URL,
2058
2268
  DEFAULT_PDF_MAX_BYTES,
2059
2269
  DEFAULT_PDF_TIMEOUT_MS,
2270
+ DEFAULT_SIGN_SESSION_TTL_MS,
2060
2271
  DeviceLinkError,
2061
2272
  EidClient,
2062
2273
  EidError,
@@ -2080,6 +2291,8 @@ export {
2080
2291
  buildDeviceLink,
2081
2292
  createEidLogin,
2082
2293
  deviceLink,
2294
+ deviceLinkInput,
2295
+ forwardedClientIp,
2083
2296
  interactionsToBase64,
2084
2297
  isMobileUserAgent,
2085
2298
  memoryLoginStore,
@@ -2091,6 +2304,7 @@ export {
2091
2304
  qrDeviceLinkTicker,
2092
2305
  randomChallenge,
2093
2306
  resolveBaseUrl,
2307
+ sessionExpiry,
2094
2308
  sha256Base64,
2095
2309
  tokenAuthDigest,
2096
2310
  tokenLimitOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsup src/index.ts src/browser.ts --format esm,cjs --dts --clean && tsup src/button.ts --format esm,cjs --dts --tsconfig tsconfig.button.json",
37
- "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/portal-gaps.test.ts test/chain.test.ts test/qr.test.ts test/login.test.ts test/button.test.ts",
37
+ "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/portal-gaps.test.ts test/chain.test.ts test/qr.test.ts test/login.test.ts test/button.test.ts test/sign-gaps.test.ts",
38
38
  "test:contract": "npm run build && node --experimental-strip-types test/contract.test.ts",
39
39
  "lint": "tsc --noEmit && tsc --noEmit -p tsconfig.button.json"
40
40
  },