@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/README.md +176 -99
- package/dist/{browser-C6JHm0lR.d.cts → browser-CuMH5IVG.d.cts} +40 -1
- package/dist/{browser-C6JHm0lR.d.ts → browser-CuMH5IVG.d.ts} +40 -1
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/button.cjs +1005 -0
- package/dist/button.d.cts +93 -0
- package/dist/button.d.ts +93 -0
- package/dist/button.js +975 -0
- package/dist/index.cjs +591 -305
- package/dist/index.d.cts +254 -146
- package/dist/index.d.ts +254 -146
- package/dist/index.js +586 -301
- package/package.json +12 -4
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:
|
|
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:
|
|
508
|
-
signatureAlgorithmParameters: { hashAlgorithm:
|
|
525
|
+
signatureAlgorithm: sign.signatureAlgorithm,
|
|
526
|
+
signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
|
|
509
527
|
},
|
|
510
|
-
interactions:
|
|
528
|
+
interactions: sign.interactions
|
|
511
529
|
};
|
|
512
|
-
if (
|
|
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
|
-
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
return {
|
|
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
|
|
528
|
-
return this.
|
|
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
|
|
572
|
+
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
|
|
531
573
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
532
|
-
const
|
|
533
|
-
return this.
|
|
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
|
|
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
|
-
|
|
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/
|
|
2101
|
+
// src/login.ts
|
|
1916
2102
|
var import_node_crypto7 = require("crypto");
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
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
|
|
2028
|
-
if (
|
|
2029
|
-
const
|
|
2030
|
-
const
|
|
2031
|
-
return {
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
-
|
|
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 (
|
|
2057
|
-
|
|
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
|
-
|
|
2065
|
-
|
|
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
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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
|
|
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
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
);
|
|
2109
|
-
|
|
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
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
-
|
|
2149
|
-
} catch {
|
|
2150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
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
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
});
|