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