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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  ApiError,
3
3
  AuthenticationError,
4
+ CertChoiceInvalidError,
4
5
  DeviceLinkError,
5
6
  EidError,
6
7
  ForbiddenError,
7
8
  NetworkError,
9
+ NoSigningCertificateError,
8
10
  ON_BEHALF_ERROR_CODES,
9
11
  RETRYABLE_ERROR_CODES,
12
+ RequestInvalidError,
10
13
  ResponseTooLargeError,
11
14
  SessionFailedError,
12
15
  ValidationError,
@@ -15,7 +18,7 @@ import {
15
18
  maskPath,
16
19
  openOrShowQR,
17
20
  parseRetryAfter
18
- } from "./chunk-QPJDZLHR.js";
21
+ } from "./chunk-DXVHOZO7.js";
19
22
 
20
23
  // src/http.ts
21
24
  var DEFAULT_TIMEOUT_MS = 15e3;
@@ -85,7 +88,8 @@ var Http = class {
85
88
  { headers, retryAfterMs }
86
89
  );
87
90
  }
88
- throw new ApiError(res.status, text, code, { headers, retryAfterMs });
91
+ const Typed = code === "NO_SIGNING_CERTIFICATE" ? NoSigningCertificateError : code === "CERT_CHOICE_INVALID" ? CertChoiceInvalidError : code === "REQUEST_INVALID" ? RequestInvalidError : ApiError;
92
+ throw new Typed(res.status, text, code, { headers, retryAfterMs });
89
93
  }
90
94
  };
91
95
  async function readLimited(res, limit, op) {
@@ -138,6 +142,11 @@ var RP_ROUTES = {
138
142
  authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
139
143
  signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
140
144
  signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
145
+ signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
146
+ signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
147
+ signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
148
+ certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
149
+ certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
141
150
  pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
142
151
  pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
143
152
  orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
@@ -206,6 +215,21 @@ function acspV2Digest(p, hashAlgorithm) {
206
215
  return createHash(nodeHash(hashAlgorithm)).update(buildAcspV2Payload(p)).digest();
207
216
  }
208
217
 
218
+ // src/types.ts
219
+ var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
220
+ function sessionExpiry(sentAtMs, ttlMs) {
221
+ return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
222
+ }
223
+ function parseExpiresAt(v) {
224
+ if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(v)) return void 0;
225
+ const t = Date.parse(v);
226
+ return Number.isFinite(t) ? t : void 0;
227
+ }
228
+ function startExpiry(raw, sentAtMs, ttlMs) {
229
+ const r = raw && typeof raw === "object" ? raw : {};
230
+ return parseExpiresAt(r.expiresAt) ?? sessionExpiry(sentAtMs, ttlMs);
231
+ }
232
+
209
233
  // src/auth.ts
210
234
  function rec(v) {
211
235
  return v && typeof v === "object" ? v : {};
@@ -251,11 +275,19 @@ var AuthApi = class {
251
275
  parseNotification(raw, acsp) {
252
276
  const r = rec(raw);
253
277
  const vc = rec(r.vc).value;
254
- return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, rpChallenge: acsp.rpChallenge, acsp };
278
+ const expiresAt = parseExpiresAt(r.expiresAt);
279
+ return {
280
+ sessionId: String(r.sessionID),
281
+ vc: vc != null ? String(vc) : null,
282
+ rpChallenge: acsp.rpChallenge,
283
+ acsp,
284
+ ...expiresAt !== void 0 ? { expiresAt } : {}
285
+ };
255
286
  }
256
287
  parseDeviceLink(raw, acsp) {
257
288
  const r = rec(raw);
258
289
  const vc = typeof r.vc === "object" && r.vc !== null ? rec(r.vc).value : r.vc;
290
+ const expiresAt = parseExpiresAt(r.expiresAt);
259
291
  return {
260
292
  sessionId: String(r.sessionID),
261
293
  sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
@@ -264,7 +296,9 @@ var AuthApi = class {
264
296
  vc: vc != null ? String(vc) : null,
265
297
  rpChallenge: acsp.rpChallenge,
266
298
  acsp,
267
- receivedAt: Date.now()
299
+ receivedAt: Date.now(),
300
+ sessionType: "auth",
301
+ ...expiresAt !== void 0 ? { expiresAt } : {}
268
302
  };
269
303
  }
270
304
  /** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
@@ -297,6 +331,10 @@ var AuthApi = class {
297
331
  function rec2(v) {
298
332
  return v && typeof v === "object" ? v : {};
299
333
  }
334
+ var vcOf = (r) => {
335
+ const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
336
+ return vc != null ? String(vc) : null;
337
+ };
300
338
  var SignApi = class {
301
339
  constructor(http, creds, defaults) {
302
340
  this.http = http;
@@ -306,46 +344,155 @@ var SignApi = class {
306
344
  http;
307
345
  creds;
308
346
  defaults;
309
- body(digestB64, interactions, opts) {
347
+ body(digestB64, interactions, opts, defAlg) {
348
+ const sign = {
349
+ digest: digestB64,
350
+ hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
351
+ signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
352
+ relyingPartyName: this.creds.rpName,
353
+ interactions: interactionsToBase64(interactions),
354
+ initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
355
+ };
310
356
  const body = {
311
357
  relyingPartyUUID: this.creds.rpUUID,
312
- relyingPartyName: this.creds.rpName,
358
+ relyingPartyName: sign.relyingPartyName,
313
359
  certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
314
360
  signatureProtocol: "RAW_DIGEST_SIGNATURE",
315
361
  signatureProtocolParameters: {
316
362
  digest: digestB64,
317
- signatureAlgorithm: opts?.signatureAlgorithm ?? "rsassa-pss",
318
- signatureAlgorithmParameters: { hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256" }
363
+ signatureAlgorithm: sign.signatureAlgorithm,
364
+ signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
319
365
  },
320
- interactions: interactionsToBase64(interactions)
366
+ interactions: sign.interactions
321
367
  };
322
- if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
368
+ if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
323
369
  if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
324
370
  if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
325
- return body;
371
+ return { body, sign };
326
372
  }
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: "" };
373
+ async notification(path, body, sign) {
374
+ const sent = Date.now();
375
+ const r = rec2(await this.http.post(path, body));
376
+ return {
377
+ sessionId: String(r.sessionID),
378
+ vc: vcOf(r),
379
+ rpChallenge: "",
380
+ ...sign ? { sign } : {},
381
+ expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
382
+ };
383
+ }
384
+ async deviceLink(path, body, extra) {
385
+ const sent = Date.now();
386
+ const r = rec2(await this.http.post(path, body));
387
+ return {
388
+ sessionId: String(r.sessionID),
389
+ sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
390
+ sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
391
+ deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
392
+ vc: vcOf(r),
393
+ rpChallenge: "",
394
+ receivedAt: Date.now(),
395
+ ...extra,
396
+ expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
397
+ };
331
398
  }
332
399
  /**
333
400
  * Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
334
401
  * digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
402
+ *
403
+ * ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
404
+ * (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
335
405
  */
336
406
  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);
407
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
408
+ return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
339
409
  }
340
- /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
410
+ /** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
341
411
  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);
412
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
413
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
344
414
  }
345
415
  /** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
346
416
  async textByEtsi(id, text, interactions, opts) {
347
417
  return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
348
418
  }
419
+ /**
420
+ * QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
421
+ * `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
422
+ * `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
423
+ */
424
+ async deviceLinkByEtsi(id, digestB64, interactions, opts) {
425
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
426
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
427
+ }
428
+ /** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
429
+ async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
430
+ const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
431
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
432
+ }
433
+ // ── Certificate-choice ──
434
+ certChoiceBody(opts) {
435
+ return {
436
+ relyingPartyUUID: this.creds.rpUUID,
437
+ relyingPartyName: this.creds.rpName,
438
+ certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
439
+ };
440
+ }
441
+ /**
442
+ * Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
443
+ * `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
444
+ */
445
+ async certificateChoiceByEtsi(id, opts) {
446
+ return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
447
+ }
448
+ /**
449
+ * QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
450
+ * initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
451
+ */
452
+ async certificateChoiceDeviceLink(opts) {
453
+ return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
454
+ sessionType: "cert",
455
+ relyingPartyName: this.creds.rpName
456
+ });
457
+ }
458
+ // ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
459
+ signerBody(signer, digestB64, interactions, opts) {
460
+ if (!signer?.documentNumber?.trim()) {
461
+ 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");
462
+ }
463
+ const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
464
+ const want = hashLen(out.sign.hashAlgorithm);
465
+ const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
466
+ if (!d || d.length !== want || d.toString("base64") !== digestB64) {
467
+ 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`);
468
+ }
469
+ return out;
470
+ }
471
+ /**
472
+ * Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
473
+ * etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
474
+ * хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
475
+ *
476
+ * `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
477
+ * бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
478
+ * 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
479
+ * `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
480
+ * s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
481
+ */
482
+ async digestForSigner(signer, digestB64, interactions, opts) {
483
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
484
+ const linked = signer.certChoiceSessionId?.trim();
485
+ if (linked) {
486
+ body.linkedSessionID = linked;
487
+ return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
488
+ }
489
+ return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
490
+ }
491
+ /** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
492
+ async deviceLinkForSigner(signer, digestB64, interactions, opts) {
493
+ const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
494
+ return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
495
+ }
349
496
  };
350
497
 
351
498
  // src/session.ts
@@ -1106,13 +1253,19 @@ var ResponseValidator = class {
1106
1253
  * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
1107
1254
  */
1108
1255
  validateSign(result, digestB64, opts = {}) {
1109
- const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1256
+ const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
1257
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
1110
1258
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
1111
1259
  if (expectedFlowType && result.flowType !== expectedFlowType) {
1112
1260
  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
1261
  }
1262
+ if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
1263
+ throw new ValidationError(
1264
+ `\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})`
1265
+ );
1266
+ }
1114
1267
  const digest = Buffer.from(digestB64, "base64");
1115
- const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
1268
+ const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
1116
1269
  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
1270
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
1118
1271
  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 +1279,24 @@ var ResponseValidator = class {
1126
1279
  parsedSubject: parseSubject(leaf)
1127
1280
  };
1128
1281
  }
1282
+ /**
1283
+ * Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
1284
+ * documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
1285
+ * issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
1286
+ * гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
1287
+ */
1288
+ validateCertificateChoice(result, sessionId = null) {
1289
+ this.sessionDone(result);
1290
+ const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
1291
+ return {
1292
+ documentNumber: result.documentNumber,
1293
+ certificate: leaf,
1294
+ certificateDerB64: leaf.raw.toString("base64"),
1295
+ certificateLevel: String(result.certificateLevel ?? ""),
1296
+ parsedSubject: parseSubject(leaf),
1297
+ certChoiceSessionId: sessionId
1298
+ };
1299
+ }
1129
1300
  /**
1130
1301
  * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
1131
1302
  * (downgrade / alg confusion хаалт).
@@ -1175,13 +1346,22 @@ var ResponseValidator = class {
1175
1346
  // ── дотоод ──
1176
1347
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
1177
1348
  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");
1349
+ this.sessionDone(result);
1180
1350
  if (result.signatureProtocol && result.signatureProtocol !== protocol) {
1181
1351
  throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
1182
1352
  }
1183
1353
  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
1354
  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");
1355
+ return this.leafOf(result, protocol);
1356
+ }
1357
+ /** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
1358
+ sessionDone(result) {
1359
+ if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
1360
+ if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
1361
+ }
1362
+ /** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
1363
+ leafOf(result, protocol) {
1364
+ 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
1365
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
1186
1366
  const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
1187
1367
  this.checkValidity(leaf);
@@ -1598,7 +1778,7 @@ var EidClient = class {
1598
1778
  });
1599
1779
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1600
1780
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1601
- this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1781
+ this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
1602
1782
  this.pdf = new PdfApi(http, cfg.credentials, {
1603
1783
  certificateLevel: level,
1604
1784
  timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
@@ -1674,6 +1854,37 @@ function buildDeviceLink(p) {
1674
1854
  const authCode = createHmac("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1675
1855
  return `${unprotected}&authCode=${authCode}`;
1676
1856
  }
1857
+ function deviceLinkInput(s, deviceLinkType, lang) {
1858
+ const base = {
1859
+ deviceLinkBase: s.deviceLinkBase ?? "",
1860
+ deviceLinkType,
1861
+ sessionToken: s.sessionToken ?? "",
1862
+ sessionSecret: s.sessionSecret ?? "",
1863
+ lang,
1864
+ receivedAt: s.receivedAt
1865
+ };
1866
+ if (s.sessionType === "sign" && s.sign) {
1867
+ return {
1868
+ ...base,
1869
+ sessionType: "sign",
1870
+ digest: s.sign.digest,
1871
+ relyingPartyName: s.sign.relyingPartyName,
1872
+ interactions: s.sign.interactions,
1873
+ initialCallbackUrl: s.sign.initialCallbackUrl
1874
+ };
1875
+ }
1876
+ if (s.sessionType === "cert") {
1877
+ return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
1878
+ }
1879
+ return {
1880
+ ...base,
1881
+ sessionType: "auth",
1882
+ rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
1883
+ relyingPartyName: s.acsp?.relyingPartyName ?? "",
1884
+ interactions: s.acsp?.interactions ?? "",
1885
+ initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
1886
+ };
1887
+ }
1677
1888
  function qrDeviceLinkTicker(input, onLink, opts) {
1678
1889
  const clock = opts?.clock ?? Date.now;
1679
1890
  const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
@@ -1710,6 +1921,16 @@ function memoryLoginStore() {
1710
1921
  };
1711
1922
  }
1712
1923
  var CALLBACK_PATH = "/auth/eid/callback";
1924
+ function forwardedClientIp(headers, trustProxy) {
1925
+ if (trustProxy === false || trustProxy === 0) return void 0;
1926
+ const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
1927
+ if (xff.length) {
1928
+ if (trustProxy === true) return xff[0];
1929
+ const n = Math.floor(trustProxy);
1930
+ return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
1931
+ }
1932
+ return header(headers, "x-real-ip").trim() || void 0;
1933
+ }
1713
1934
  var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
1714
1935
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1715
1936
  var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
@@ -1761,11 +1982,12 @@ function safeEqualHex(a, b) {
1761
1982
  const y = Buffer.from(b, "hex");
1762
1983
  return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
1763
1984
  }
1764
- function toIdentity(r, v, flowType) {
1985
+ function toIdentity(sessionId, r, v, flowType) {
1765
1986
  const s = v.parsedSubject;
1766
1987
  const etsi = s.serialNumber ?? "";
1767
1988
  const m = /^PNOMN-([0-9]+)$/.exec(etsi);
1768
1989
  return {
1990
+ sessionId,
1769
1991
  etsi,
1770
1992
  civilId: m ? m[1] : null,
1771
1993
  givenName: s.givenName ?? null,
@@ -1933,7 +2155,8 @@ function createEidLogin(opts) {
1933
2155
  const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
1934
2156
  if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
1935
2157
  const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
1936
- if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
2158
+ const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2159
+ if (ip && !await rateLimit(`start:${ip}`)) return limited();
1937
2160
  if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
1938
2161
  let r;
1939
2162
  try {
@@ -1955,7 +2178,7 @@ function createEidLogin(opts) {
1955
2178
  async function finish(req, tx, r, flow, verifier) {
1956
2179
  const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
1957
2180
  if (!await store.delete(tx.sessionId)) return void 0;
1958
- const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
2181
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
1959
2182
  return out;
1960
2183
  }
1961
2184
  async function status(req) {
@@ -2038,11 +2261,20 @@ function createEidLogin(opts) {
2038
2261
  }
2039
2262
  return json(404, { error: "not_found" });
2040
2263
  }
2264
+ let warnedNoIp = false;
2041
2265
  async function handle(req) {
2042
2266
  const headers = {};
2043
2267
  req.headers.forEach((v, k) => headers[k] = v);
2044
2268
  const body = req.method === "POST" ? await req.text() : void 0;
2045
- const r = await handleRequest({ method: req.method, url: req.url, headers, body });
2269
+ const ip = opts.getClientIp?.(req) || void 0;
2270
+ if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
2271
+ warnedNoIp = true;
2272
+ 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"), {
2273
+ request: { method: req.method, url: req.url, headers },
2274
+ mode: "qr"
2275
+ });
2276
+ }
2277
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
2046
2278
  const h = new Headers();
2047
2279
  for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2048
2280
  return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
@@ -2054,20 +2286,24 @@ export {
2054
2286
  AuthApi,
2055
2287
  AuthenticationError,
2056
2288
  CITIZEN_ISSUING_CA_SKI,
2289
+ CertChoiceInvalidError,
2057
2290
  DEFAULT_BASE_URL,
2058
2291
  DEFAULT_PDF_MAX_BYTES,
2059
2292
  DEFAULT_PDF_TIMEOUT_MS,
2293
+ DEFAULT_SIGN_SESSION_TTL_MS,
2060
2294
  DeviceLinkError,
2061
2295
  EidClient,
2062
2296
  EidError,
2063
2297
  ForbiddenError,
2064
2298
  Http,
2065
2299
  NetworkError,
2300
+ NoSigningCertificateError,
2066
2301
  ON_BEHALF_ERROR_CODES,
2067
2302
  OrganizationApi,
2068
2303
  PdfApi,
2069
2304
  PersonApi,
2070
2305
  RETRYABLE_ERROR_CODES,
2306
+ RequestInvalidError,
2071
2307
  ResponseTooLargeError,
2072
2308
  ResponseValidator,
2073
2309
  SessionApi,
@@ -2080,10 +2316,13 @@ export {
2080
2316
  buildDeviceLink,
2081
2317
  createEidLogin,
2082
2318
  deviceLink,
2319
+ deviceLinkInput,
2320
+ forwardedClientIp,
2083
2321
  interactionsToBase64,
2084
2322
  isMobileUserAgent,
2085
2323
  memoryLoginStore,
2086
2324
  openOrShowQR,
2325
+ parseExpiresAt,
2087
2326
  parseRetryAfter,
2088
2327
  parseSessionResult,
2089
2328
  parseSubject,
@@ -2091,7 +2330,9 @@ export {
2091
2330
  qrDeviceLinkTicker,
2092
2331
  randomChallenge,
2093
2332
  resolveBaseUrl,
2333
+ sessionExpiry,
2094
2334
  sha256Base64,
2335
+ startExpiry,
2095
2336
  tokenAuthDigest,
2096
2337
  tokenLimitOf,
2097
2338
  userChallengeOf,
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.2",
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 test/session-start.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
  },