@gerege-systems/eid-mongolia-sdk 0.7.0 → 0.8.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 CHANGED
@@ -4,8 +4,10 @@
4
4
  (authentication) ба хууль ёсны цахим гарын үсэг (qualified signature) хийлгэх Node.js/TypeScript сан.
5
5
  RP-API v3, Smart-ID нийцтэй. Бүрэн баримт: <https://developer.eidmongolia.mn/>.
6
6
 
7
- > ⚠️ Зөвхөн **backend**-д ажиллана (`@gerege-systems/eid-mongolia-sdk/browser`-ийн legacy device-link туслахаас бусад).
8
- > API secret (`rp_sk_…`) браузер/гар утсанд хэзээ ч задлахгүй.
7
+ > ⚠️ SDK **backend**-д ажиллана: бүх дуудлага API secret (`rp_sk_…`)-ээр баталгаажна, secret-ийг браузер/гар утсанд
8
+ > хэзээ ч задлахгүй. Браузерт зөвхөн таны backend-ийн өгсөн холбоос/QR/товч харагдана — «eID-ээр нэвтрэх» товч ч
9
+ > энгийн холбоос, `code` → `id_token` солих нь backend-д (§1a). `@gerege-systems/eid-mongolia-sdk/browser`-ийн
10
+ > legacy device-link туслахууд 2026-10-07-оос ажиллахгүй (§4).
9
11
 
10
12
  ## Суулгах
11
13
 
@@ -17,9 +19,111 @@ Node.js ≥ 22 (global `fetch`, `node:crypto`).
17
19
 
18
20
  ## 1. RP бүртгүүлэх
19
21
 
20
- `POST https://ca.eidmongolia.mn/v3/rp-applications` (эсвэл [developer.eidmongolia.mn](https://developer.eidmongolia.mn)) —
21
- оператор баталсны дараа **relyingPartyUUID** ба **API secret** (`rp_sk_…`) олгогдоно; secret **зөвхөн нэг удаа**
22
- харагдана. Дэлгэрэнгүй: <https://developer.eidmongolia.mn/#burtgel>.
22
+ RP-г eID Mongolia-гийн оператор бүртгэнэ — хүсэлтээ <eIDsupport@gerege.com> эсвэл 7777-3773-аар гаргана
23
+ (нийтийн `POST /v3/rp-applications` 2026-10-14-нөөс засвартай: `503 rp_applications_maintenance`). Оператор
24
+ баталсны дараа **relyingPartyUUID** ба **API secret** (`rp_sk_…`) олгогдоно; secret **зөвхөн нэг удаа** харагдана.
25
+ Шаардлагатай мэдээлэл (байгууллага, зорилго `AUTH`/`SIGN`, `callbackHosts`, OIDC redirect URI…):
26
+ <https://developer.eidmongolia.mn/#registration>.
27
+
28
+ ## 1a. «eID-ээр нэвтрэх» (OpenID Connect)
29
+
30
+ Хамгийн хялбар нэвтрэлт: таны вэбэд нэг товч, нэвтрэх хуудсыг (QR · утсан дээрх аппаар нээх · регистрээр
31
+ мэдэгдэл) eID сервер өөрөө харуулж, гарын үсэг ба гэрчилгээний гинжийг шалгаад танд стандарт OpenID Connect
32
+ (Authorization Code + PKCE `S256`) `id_token` өгнө — Google/Apple нэвтрэлттэй ижил. Утасны хөтчөөс дарахад
33
+ хуудасгүйгээр шууд eID апп нээгдэнэ. Гэрээ: <https://developer.eidmongolia.mn/#oidc>.
34
+
35
+ **Хаана юу ажиллах.** Товч нь браузерт (энгийн `<a href>` таны backend-ийн `/auth/eid/login` руу); `EidOidcClient`
36
+ нь backend-д — authorize URL угсрах, callback-д `code`-ийг `client_secret`-ээр `id_token` болгон солих, гарын үсгийг
37
+ шалгах. eID-ийн OIDC client нь одоогоор зөвхөн **confidential client** (`client_secret_basic`): backend-гүй SPA эсвэл
38
+ native апп дангаараа token endpoint-ийг дуудаж чадахгүй — native апп ч `code`-ийг өөрийн backend-ээр солино (доор).
39
+
40
+ **Бүртгэл.** `client_id` = таны RP UUID (`relyingPartyUUID`), `client_secret` = RP API secret (`rp_sk_…`).
41
+ RP-д `AUTH` эрх, OIDC асаалттай, `redirect_uri` (https, ≤ 10) бүртгэлтэй байна — authorize/token-ий `redirect_uri`
42
+ бүртгэлтэйтэй **тэмдэгт бүрээрээ** таарах ёстой. Иргэний ETSI дугаар (`pnomn` claim) хэрэгтэй бол `PNOMN` эрх
43
+ хүсэж, `scope: "openid pnomn"`. Token endpoint-д RP-ийн IP allowlist мөрдөгдөнө; `requireMtls` RP OIDC ашиглахгүй.
44
+
45
+ ```ts
46
+ import { EidOidcClient, EidOidcError } from "@gerege-systems/eid-mongolia-sdk";
47
+
48
+ const oidc = new EidOidcClient({
49
+ clientId: process.env.EID_RP_UUID!,
50
+ clientSecret: process.env.EID_SECRET!, // rp_sk_… — зөвхөн backend
51
+ redirectUri: "https://bank.example.mn/auth/eid/callback",
52
+ // issuer анхдагч https://login.eidmongolia.mn, scope анхдагч "openid"
53
+ });
54
+
55
+ const TX = "eid_oidc_tx";
56
+ const cookieOpts = { httpOnly: true, secure: true, sameSite: "lax", path: "/auth/eid", maxAge: 600_000 } as const;
57
+
58
+ // GET /auth/eid/login — товчны href
59
+ app.get("/auth/eid/login", async (_req, res) => {
60
+ const a = await oidc.createAuthorization(); // { url, state, nonce, codeVerifier }
61
+ res.cookie(TX, JSON.stringify({ state: a.state, nonce: a.nonce, codeVerifier: a.codeVerifier }), cookieOpts);
62
+ res.set("Cache-Control", "no-store").redirect(302, a.url);
63
+ });
64
+
65
+ // GET /auth/eid/callback?code&state (эсвэл ?error) — бүртгэлтэй redirect_uri
66
+ app.get("/auth/eid/callback", async (req, res) => {
67
+ const tx = JSON.parse(req.cookies[TX] ?? "null");
68
+ res.clearCookie(TX, { path: "/auth/eid" });
69
+ if (!tx) return res.redirect(303, "/login?error=state");
70
+ try {
71
+ const { claims } = await oidc.handleCallback(req.originalUrl, tx); // state → token → id_token (ES256)
72
+ const user = await users.findOrCreateBySub(claims.sub); // pairwise sub-аар таньна (нэрээр биш)
73
+ await startSession(res, user); // таны session-ийг шинээр үүсгэ (session fixation)
74
+ res.redirect(303, "/");
75
+ } catch (e) {
76
+ const code = e instanceof EidOidcError ? e.code : "server_error"; // access_denied → «дахин оролдоно уу»
77
+ res.redirect(303, `/login?error=${code}`);
78
+ }
79
+ });
80
+ ```
81
+
82
+ Next.js App Router-д ижил: `app/auth/eid/login/route.ts` → `NextResponse.redirect(a.url, 302)` + `res.cookies.set(…)`;
83
+ callback → `oidc.handleCallback(request.nextUrl.searchParams, tx)` (зөвхөн query уншина — прокси ард host хамаагүй).
84
+
85
+ **Товч** — энгийн холбоос (`<a href>` эсвэл form GET). `fetch`/XHR/JS redirect биш: iOS Universal Link зөвхөн
86
+ хэрэглэгчийн tap-аас эхэлсэн redirect гинжид eID аппыг нээнэ.
87
+
88
+ ```html
89
+ <a class="eid-login" href="/auth/eid/login">
90
+ <img src="/img/eid-mongolia.png" alt="" width="20" height="20">
91
+ eID Mongolia-аар нэвтрэх
92
+ </a>
93
+ <style>
94
+ .eid-login { display: inline-flex; align-items: center; justify-content: center; gap: .5rem; min-height: 44px;
95
+ padding: 0 1.25rem; border: 1px solid #d0d5dd; border-radius: 6px; background: #fff; color: #111827;
96
+ font: 500 1rem/1 system-ui, sans-serif; text-decoration: none; }
97
+ .eid-login:hover { background: #f5f7fa; }
98
+ .eid-login:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
99
+ </style>
100
+ ```
101
+
102
+ Бичвэр «eID Mongolia-аар нэвтрэх», зүүн талд eID Mongolia лого (<https://developer.eidmongolia.mn/logo-96.png>-ийг
103
+ татаж өөрийн хостоос үйлчил). Google/Apple-ийн товч шиг саармаг (цагаан, хүрээтэй) — таны сайтын бусад нэвтрэх
104
+ товчтой зэрэгцэж болно. Жишээ: <https://demo.eidmongolia.mn>.
105
+
106
+ `createAuthorization({ display: "page" })` — утсан дээр ч нэвтрэх хуудас (QR / push) харуулна (ж: таблет киоск).
107
+
108
+ **`id_token` claims** (`EidIdTokenClaims`): `sub` (pairwise — RP тус бүрд өөр, иргэнд тогтмол), `acr`
109
+ (`urn:eidmongolia:loa:qualified` | `…:advanced`), `auth_time`, `given_name` / `family_name` (латин, гэрчилгээнээс),
110
+ `given_name_mn` / `family_name_mn` (кирилл, байвал), `pnomn` (`pnomn` scope + эрхтэй үед). `id_token` 5 минут
111
+ хүчинтэй; access_token / refresh_token / userinfo байхгүй — нэвтрэлтийн дараа өөрийн session-оо хэрэглэнэ.
112
+
113
+ **Аюулгүй байдал.**
114
+
115
+ - `state`, `nonce`, `codeVerifier`-ийг **хөтчийн session-тэй** (httpOnly, Secure, SameSite=Lax cookie эсвэл server
116
+ session) холбож хадгална; `handleCallback` state-ийг timing-safe тулгаж, таарахгүй бол code-ийг солихгүй
117
+ (`state_mismatch`). Үүнгүйгээр «өөрийн бүртгэлд хохирогчийн eID-ийг холбуулах» (login CSRF) нээлттэй.
118
+ - `handleCallback` / `verifyIdToken` нь ES256 гарын үсэг (JWKS `kid`, cache 1 цаг, танихгүй `kid` → нэг удаа дахин
119
+ татна), `iss`, `aud` (= `clientId`; олон aud бол `azp`), `exp` / `iat` (±60 сек), `nonce`-ийг шалгана.
120
+ `EidOidcError` (`invalid_id_token`) гарвал **итгэхгүй**.
121
+ - Code нэг удаагийн, 60 секунд — callback-ийг дахин ачаалбал `invalid_grant`; хэрэглэгчийг дахин нэвтрүүлнэ.
122
+ - `error_description` (`EidOidcError.description`)-ийг логт бичиж болно, хэрэглэгчид шууд харуулахгүй.
123
+ - **Native апп (RFC 8252):** authorize-ийг системийн хөтчөөр нээнэ — iOS `ASWebAuthenticationSession`
124
+ (`prefersEphemeralWebBrowserSession = false`), Android Custom Tabs. **WebView хориотой.** `redirect_uri` = таны
125
+ https Universal Link / App Link (custom scheme дэмжихгүй). `state` / `nonce` / `codeVerifier` апп-ын санах ойд;
126
+ code-ийг таны backend `client_secret`-ээр солино (`client_secret`-ийг аппад хадгалахгүй).
23
127
 
24
128
  ## 2. Нэвтрэлт (push)
25
129
 
@@ -36,7 +140,7 @@ const eid = new EidClient({
36
140
  trust: { trustAnchorsPem: [process.env.EID_NATIONAL_ROOT_CA_PEM!] }, // ← Mongolian National Root CA, production-д ЗААВАЛ
37
141
  });
38
142
 
39
- // ETSI (PNOMN-<иргэний дугаар>) / регистр / иргэний дугаар — аль нь ч болно (сервер таьна)
143
+ // ETSI (PNOMN-<иргэний дугаар>) / регистр / иргэний дугаар — аль нь ч болно (сервер танина)
40
144
  const s = await eid.auth.notificationByEtsi("PNOMN-12345678", [
41
145
  { type: "displayTextAndPIN", displayText60: "Хаан Банк-д нэвтрэх" },
42
146
  ]);
@@ -139,7 +243,8 @@ import { buildDeviceLink, type DeviceLinkSession } from "@gerege-systems/eid-mon
139
243
  // backend — session эхлүүлээд бүх хариуг СЕРВЕРТ хадгална (sessionToken/sessionSecret/receivedAt/acsp)
140
244
  const s = await eid.auth.deviceLinkAnonymous(
141
245
  [{ type: "displayTextAndPIN", displayText60: "Хаан Банк-д нэвтрэх" }],
142
- { callbackUrl: "https://bank.example.mn/eid/callback?state=…" }, // товч харуулах бол
246
+ { callbackUrl: "https://bank.example.mn/auth/eid/callback?state=…" }, // товч харуулах бол; host нь callbackHosts-д,
247
+ // зам нь үргэлж /auth/eid/callback болж нормчлогдоно (зөвхөн query хадгалагдана), https заавал
143
248
  );
144
249
  store.put(s.sessionId, s);
145
250
  res.json({ sessionId: s.sessionId, vc: s.vc }); // браузерт ЗӨВХӨН эдгээр
@@ -188,10 +293,9 @@ eid.validator.validateAuth(result, s.acsp, {
188
293
  QR ба товч хоёуланг харуулсан хуудсанд: callback-аар ирсэн үр дүнг `Web2App`/`App2App`-аар, poll-оор ирснийг
189
294
  `QR`-аар шалга (товчоор нээгдсэн session зөвхөн callback-аар дуусна).
190
295
 
191
- > ⚠️ **Шилжилт.** eID Mongolia 2.2.3 (build 59)-өөс өмнөх апп v3 холбоосыг танихгүй. RP-ийн v3 рүү шилжилтийг
192
- > тохиргооны унтраалгаар (анхдагч legacy) хийж, build 59 store-д гарч `min_version=59` тавигдах мөчид асаана.
193
- > Legacy `deviceLink(session)` (`{deviceLinkBase}?sessionId=…&vc=…`) болон `/browser` entry-ийн
194
- > `openOrShowQR` **deprecated** — тэр мөчөөс сервер статик холбоосыг хүлээн авахгүй.
296
+ > ⚠️ **Legacy холбоос хасагдсан (2026-10-07).** Статик `deviceLink(session)` (`{deviceLinkBase}?sessionId=…&vc=…`)
297
+ > болон `/browser` entry-ийн `openOrShowQR`-ээр нээгдсэн session-д сервер утасны үйлдлийг `DEVICE_LINK_REQUIRED`-аар
298
+ > татгалзана — зөвхөн v3 (`buildDeviceLink`) ажиллана. Аппын доод хувилбар: iOS 2.2.3 (59), Android 70.
195
299
 
196
300
  ## Аюулгүй байдал — яагаад `validator` заавал
197
301
 
@@ -206,7 +310,7 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
206
310
  - **auth** → ACSP_V2 payload (`smart-id|ACSP_V2|serverRandom|rpChallenge|userChallenge|B64(rpName)|B64(brokeredRpName)|B64(SHA-256(interactions))|interactionTypeUsed|initialCallbackUrl|flowType`, hash = хүсэлтийн `hashAlgorithm`) дээр — `flowType`, `userChallenge`, `serverRandom` ба алгоритмын downgrade-ийг мөн шалгана;
207
311
  `expectedFlowType` өгвөл `flowType` эхлүүлсэн урсгалтай ЯГ таарах, Web2App/App2App-д `userChallengeVerifier` заавал
208
312
  `initialCallbackUrl`-ийг хариуны `signature.initialCallbackUrl`-аас авна (CA урсгалаар шийднэ: QR/Notification →
209
- `""`, Web2App/App2App → таны callback; шилжилтийн үед legacy `/dl` ба push-д таны callback) — зөвхөн `""` эсвэл
313
+ `""`, Web2App/App2App → таны callback) — зөвхөн `""` эсвэл
210
314
  таны илгээсэн callback байхад, Web2App/App2App-д заавал таны callback; өөр утга → татгалзана
211
315
  - **sign** → RP-ийн өгсөн digest дээр
212
316
 
@@ -222,6 +326,7 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
222
326
  | `ValidationError` | cert chain/signature/level шалгалт бүтэлгүйтсэн |
223
327
  | `ApiError` | бусад HTTP алдаа (`.status`, `.body`, серверийн `.code` — ж: 404 `ORG_NOT_FOUND`) |
224
328
  | `NetworkError` | timeout/сүлжээ |
329
+ | `EidOidcError` | «eID-ээр нэвтрэх» (OIDC) — `.code`: `access_denied`, `login_required`, `state_mismatch`, `invalid_grant`, `invalid_id_token`, `server_error` … |
225
330
  | `DeviceLinkError` | device-link холбоос угсрах боломжгүй (`deviceLinkBase` https биш, token/secret/lang хэлбэр, төрөлд хэрэгтэй талбар дутуу) |
226
331
 
227
332
  ## mTLS (eIDAS qualified орчин)
@@ -216,9 +216,8 @@ type DeviceLinkInput = Pick<DeviceLinkSession, "sessionId" | "vc" | "deviceLinkB
216
216
  * QR-д эсвэл «eID аппаар нээх» товчинд тавина.
217
217
  *
218
218
  * @deprecated Legacy статик холбоос — холбоосын төрөл (QR/Web2App) хамгаалагдаагүй, relay-д нээлттэй.
219
- * eID Mongolia 2.2.3 (build 59) гармагц сервер унтраана (`EID_DEVICE_LINK_LEGACY=false`). Шинэ код
220
- * backend-д {@link buildDeviceLink} (v3) ашиглана; build 59-өөс өмнөх апп v3 холбоосыг танихгүй тул
221
- * RP v3 рүү шилжих нь тохиргооны унтраалгаар (анхдагч legacy) хийгдэнэ.
219
+ * Сервер 2026-10-07-нд хассан: ийм холбоосоор нээгдсэн session-д утасны үйлдлийг `DEVICE_LINK_REQUIRED`-аар
220
+ * татгалзана. Backend-д {@link buildDeviceLink} (v3) ашиглана.
222
221
  *
223
222
  * @throws {DeviceLinkError} deviceLinkBase байхгүй / http(s) биш, sessionId UUID биш, vc 5 орон биш.
224
223
  */
@@ -251,6 +250,8 @@ interface OpenOrShowQROptions {
251
250
  * Link-ийг алгасаж /dl хуудсыг нээдэг; тэр хуудас «Аппаар нээх» товч ба store холбоосыг санал болгоно.
252
251
  *
253
252
  * @throws {DeviceLinkError} холбоос угсрах боломжгүй бол ({@link deviceLink}).
253
+ * @deprecated Legacy {@link deviceLink} холбоос дээр тулгуурладаг — сервер 2026-10-07-нд хассан тул ажиллахгүй.
254
+ * QR-ийг backend-ийн `buildDeviceLink` (v3)-ээр, утсан дээр Web2App холбоосыг энгийн `<a href>`-ээр харуулна.
254
255
  */
255
256
  declare function openOrShowQR(session: DeviceLinkInput, opts: OpenOrShowQROptions): "opened" | "qr";
256
257
 
@@ -216,9 +216,8 @@ type DeviceLinkInput = Pick<DeviceLinkSession, "sessionId" | "vc" | "deviceLinkB
216
216
  * QR-д эсвэл «eID аппаар нээх» товчинд тавина.
217
217
  *
218
218
  * @deprecated Legacy статик холбоос — холбоосын төрөл (QR/Web2App) хамгаалагдаагүй, relay-д нээлттэй.
219
- * eID Mongolia 2.2.3 (build 59) гармагц сервер унтраана (`EID_DEVICE_LINK_LEGACY=false`). Шинэ код
220
- * backend-д {@link buildDeviceLink} (v3) ашиглана; build 59-өөс өмнөх апп v3 холбоосыг танихгүй тул
221
- * RP v3 рүү шилжих нь тохиргооны унтраалгаар (анхдагч legacy) хийгдэнэ.
219
+ * Сервер 2026-10-07-нд хассан: ийм холбоосоор нээгдсэн session-д утасны үйлдлийг `DEVICE_LINK_REQUIRED`-аар
220
+ * татгалзана. Backend-д {@link buildDeviceLink} (v3) ашиглана.
222
221
  *
223
222
  * @throws {DeviceLinkError} deviceLinkBase байхгүй / http(s) биш, sessionId UUID биш, vc 5 орон биш.
224
223
  */
@@ -251,6 +250,8 @@ interface OpenOrShowQROptions {
251
250
  * Link-ийг алгасаж /dl хуудсыг нээдэг; тэр хуудас «Аппаар нээх» товч ба store холбоосыг санал болгоно.
252
251
  *
253
252
  * @throws {DeviceLinkError} холбоос угсрах боломжгүй бол ({@link deviceLink}).
253
+ * @deprecated Legacy {@link deviceLink} холбоос дээр тулгуурладаг — сервер 2026-10-07-нд хассан тул ажиллахгүй.
254
+ * QR-ийг backend-ийн `buildDeviceLink` (v3)-ээр, утсан дээр Web2App холбоосыг энгийн `<a href>`-ээр харуулна.
254
255
  */
255
256
  declare function openOrShowQR(session: DeviceLinkInput, opts: OpenOrShowQROptions): "opened" | "qr";
256
257
 
@@ -1 +1 @@
1
- export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-Db82DlYN.cjs';
1
+ export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.cjs';
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-Db82DlYN.js';
1
+ export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.js';
package/dist/index.cjs CHANGED
@@ -25,8 +25,13 @@ __export(index_exports, {
25
25
  AuthenticationError: () => AuthenticationError,
26
26
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
27
27
  DeviceLinkError: () => DeviceLinkError,
28
+ EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
29
+ EID_ACR_QUALIFIED: () => EID_ACR_QUALIFIED,
30
+ EID_OIDC_ISSUER: () => EID_OIDC_ISSUER,
28
31
  EidClient: () => EidClient,
29
32
  EidError: () => EidError,
33
+ EidOidcClient: () => EidOidcClient,
34
+ EidOidcError: () => EidOidcError,
30
35
  ForbiddenError: () => ForbiddenError,
31
36
  Http: () => Http,
32
37
  NetworkError: () => NetworkError,
@@ -46,6 +51,7 @@ __export(index_exports, {
46
51
  isMobileUserAgent: () => isMobileUserAgent,
47
52
  openOrShowQR: () => openOrShowQR,
48
53
  parseSessionResult: () => parseSessionResult,
54
+ pkceChallenge: () => pkceChallenge,
49
55
  qrDeviceLinkTicker: () => qrDeviceLinkTicker,
50
56
  randomChallenge: () => randomChallenge,
51
57
  resolveBaseUrl: () => resolveBaseUrl,
@@ -1032,6 +1038,283 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1032
1038
  t.unref?.();
1033
1039
  return () => clearInterval(t);
1034
1040
  }
1041
+
1042
+ // src/oidc.ts
1043
+ var import_node_crypto4 = require("crypto");
1044
+ var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1045
+ var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1046
+ var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
1047
+ var CLOCK_SKEW_S = 60;
1048
+ var CACHE_MS = 60 * 60 * 1e3;
1049
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
1050
+ var KNOWN_CODES = /* @__PURE__ */ new Set([
1051
+ "access_denied",
1052
+ "login_required",
1053
+ "invalid_request",
1054
+ "invalid_scope",
1055
+ "unauthorized_client",
1056
+ "unsupported_response_type",
1057
+ "request_not_supported",
1058
+ "temporarily_unavailable",
1059
+ "server_error",
1060
+ "invalid_client",
1061
+ "invalid_grant",
1062
+ "unsupported_grant_type"
1063
+ ]);
1064
+ var EidOidcError = class extends EidError {
1065
+ constructor(code, message, description) {
1066
+ super(message);
1067
+ this.code = code;
1068
+ this.description = description;
1069
+ }
1070
+ code;
1071
+ description;
1072
+ };
1073
+ var EidOidcClient = class {
1074
+ constructor(cfg) {
1075
+ this.cfg = cfg;
1076
+ if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
1077
+ if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
1078
+ if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
1079
+ this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
1080
+ this.scope = cfg.scope ?? "openid";
1081
+ }
1082
+ cfg;
1083
+ issuer;
1084
+ scope;
1085
+ discovery;
1086
+ jwks;
1087
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
1088
+ discover() {
1089
+ const c = this.discovery;
1090
+ if (c && Date.now() - c.at < CACHE_MS) return c.d;
1091
+ const d = this.fetchDiscovery();
1092
+ this.discovery = { at: Date.now(), d };
1093
+ d.catch(() => {
1094
+ if (this.discovery?.d === d) this.discovery = void 0;
1095
+ });
1096
+ return d;
1097
+ }
1098
+ async fetchDiscovery() {
1099
+ const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
1100
+ if (!d || d.issuer !== this.issuer) {
1101
+ throw new EidOidcError("discovery_failed", `OIDC discovery-\u0438\u0439\u043D issuer \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (\u0445\u04AF\u043B\u044D\u044D\u0441\u044D\u043D ${this.issuer})`);
1102
+ }
1103
+ for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
1104
+ if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
1105
+ throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
1106
+ }
1107
+ }
1108
+ return d;
1109
+ }
1110
+ /**
1111
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
1112
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
1113
+ */
1114
+ async createAuthorization(opts = {}) {
1115
+ const d = await this.discover();
1116
+ const state = randomB64url(32);
1117
+ const nonce = randomB64url(32);
1118
+ const codeVerifier = randomB64url(32);
1119
+ const u = new URL(d.authorization_endpoint);
1120
+ const p = new URLSearchParams({
1121
+ response_type: "code",
1122
+ client_id: this.cfg.clientId,
1123
+ redirect_uri: this.cfg.redirectUri,
1124
+ scope: opts.scope ?? this.scope,
1125
+ state,
1126
+ nonce,
1127
+ code_challenge: await pkceChallenge(codeVerifier),
1128
+ code_challenge_method: "S256"
1129
+ });
1130
+ if (opts.display) p.set("display", opts.display);
1131
+ u.search = p.toString();
1132
+ return { url: u.toString(), state, nonce, codeVerifier };
1133
+ }
1134
+ /**
1135
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
1136
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
1137
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
1138
+ */
1139
+ async handleCallback(input, checks) {
1140
+ const q = callbackParams(input);
1141
+ const state = q.get("state") ?? "";
1142
+ if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
1143
+ throw new EidOidcError("state_mismatch", "OIDC state \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439 \u2014 \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u0433 \u0434\u0430\u0445\u0438\u043D \u044D\u0445\u043B\u04AF\u04AF\u043B\u043D\u044D \u04AF\u04AF");
1144
+ }
1145
+ const iss = q.get("iss");
1146
+ if (iss !== null && iss !== this.issuer) {
1147
+ throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1148
+ }
1149
+ const err = q.get("error");
1150
+ if (err) {
1151
+ const code2 = KNOWN_CODES.has(err) ? err : "server_error";
1152
+ throw new EidOidcError(code2, `eID \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442 \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${code2}`, q.get("error_description") ?? void 0);
1153
+ }
1154
+ const code = q.get("code") ?? "";
1155
+ if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
1156
+ const idToken = await this.exchangeCode(code, checks.codeVerifier);
1157
+ const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
1158
+ return { claims, idToken };
1159
+ }
1160
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
1161
+ async exchangeCode(code, codeVerifier) {
1162
+ const d = await this.discover();
1163
+ const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
1164
+ const res = await this.fetch(d.token_endpoint, {
1165
+ method: "POST",
1166
+ headers: {
1167
+ Authorization: "Basic " + basic,
1168
+ "Content-Type": "application/x-www-form-urlencoded",
1169
+ Accept: "application/json"
1170
+ },
1171
+ body: new URLSearchParams({
1172
+ grant_type: "authorization_code",
1173
+ code,
1174
+ redirect_uri: this.cfg.redirectUri,
1175
+ code_verifier: codeVerifier
1176
+ }).toString()
1177
+ });
1178
+ let j = {};
1179
+ try {
1180
+ j = await res.json();
1181
+ } catch {
1182
+ }
1183
+ if (!res.ok) {
1184
+ const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
1185
+ throw new EidOidcError(
1186
+ e,
1187
+ `OIDC token ${res.status}: ${e}`,
1188
+ typeof j.error_description === "string" ? j.error_description : void 0
1189
+ );
1190
+ }
1191
+ if (typeof j.id_token !== "string" || j.id_token === "") {
1192
+ throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
1193
+ }
1194
+ return j.id_token;
1195
+ }
1196
+ /**
1197
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
1198
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
1199
+ */
1200
+ async verifyIdToken(idToken, opts) {
1201
+ const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
1202
+ const parts = idToken.split(".");
1203
+ if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1204
+ const header = decodeJsonPart(parts[0]);
1205
+ const payload = decodeJsonPart(parts[1]);
1206
+ if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1207
+ if (header.alg !== "ES256") throw bad("alg ES256 \u0431\u0438\u0448");
1208
+ const kid = typeof header.kid === "string" ? header.kid : "";
1209
+ if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
1210
+ const pick = (ks) => ks.find(
1211
+ (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")
1212
+ );
1213
+ const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
1214
+ if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
1215
+ const sig = Buffer.from(parts[2], "base64url");
1216
+ 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");
1217
+ let ok = false;
1218
+ try {
1219
+ const key = await globalThis.crypto.subtle.importKey(
1220
+ "jwk",
1221
+ { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
1222
+ { name: "ECDSA", namedCurve: "P-256" },
1223
+ false,
1224
+ ["verify"]
1225
+ );
1226
+ ok = await globalThis.crypto.subtle.verify(
1227
+ { name: "ECDSA", hash: "SHA-256" },
1228
+ key,
1229
+ sig,
1230
+ new TextEncoder().encode(parts[0] + "." + parts[1])
1231
+ );
1232
+ } catch {
1233
+ ok = false;
1234
+ }
1235
+ if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
1236
+ const c = payload;
1237
+ const now = Math.floor(Date.now() / 1e3);
1238
+ if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1239
+ const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
1240
+ if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1241
+ if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1242
+ 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");
1243
+ if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
1244
+ if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1245
+ if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
1246
+ return c;
1247
+ }
1248
+ keys(force) {
1249
+ const c = this.jwks;
1250
+ if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
1251
+ const keys = this.discover().then(async (d) => {
1252
+ const j = await this.getJson(d.jwks_uri);
1253
+ return Array.isArray(j?.keys) ? j.keys : [];
1254
+ });
1255
+ this.jwks = { at: Date.now(), keys };
1256
+ keys.catch(() => {
1257
+ if (this.jwks?.keys === keys) this.jwks = void 0;
1258
+ });
1259
+ return keys;
1260
+ }
1261
+ async getJson(url) {
1262
+ const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1263
+ if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
1264
+ try {
1265
+ return await res.json();
1266
+ } catch {
1267
+ throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
1268
+ }
1269
+ }
1270
+ async fetch(url, init) {
1271
+ const f = this.cfg.fetchImpl ?? fetch;
1272
+ try {
1273
+ return await f(url, {
1274
+ ...init,
1275
+ redirect: "error",
1276
+ signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
1277
+ });
1278
+ } catch (e) {
1279
+ const err = e;
1280
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
1281
+ throw new NetworkError(`OIDC \u0445\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${url}`);
1282
+ }
1283
+ throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
1284
+ }
1285
+ }
1286
+ };
1287
+ async function pkceChallenge(codeVerifier) {
1288
+ const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
1289
+ return Buffer.from(h).toString("base64url");
1290
+ }
1291
+ function randomB64url(bytes) {
1292
+ return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
1293
+ }
1294
+ function formEncode(s) {
1295
+ return new URLSearchParams({ v: s }).toString().slice(2);
1296
+ }
1297
+ function timingSafeEqualStr(a, b) {
1298
+ const x = Buffer.from(a, "utf8");
1299
+ const y = Buffer.from(b, "utf8");
1300
+ return x.length === y.length && (0, import_node_crypto4.timingSafeEqual)(x, y);
1301
+ }
1302
+ function decodeJsonPart(s) {
1303
+ try {
1304
+ const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
1305
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
1306
+ } catch {
1307
+ return null;
1308
+ }
1309
+ }
1310
+ function callbackParams(input) {
1311
+ if (input instanceof URLSearchParams) return input;
1312
+ if (input instanceof URL) return input.searchParams;
1313
+ const s = input.trim();
1314
+ if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
1315
+ const i = s.indexOf("?");
1316
+ return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
1317
+ }
1035
1318
  // Annotate the CommonJS export names for ESM import in node:
1036
1319
  0 && (module.exports = {
1037
1320
  ApiError,
@@ -1039,8 +1322,13 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1039
1322
  AuthenticationError,
1040
1323
  DEFAULT_BASE_URL,
1041
1324
  DeviceLinkError,
1325
+ EID_ACR_ADVANCED,
1326
+ EID_ACR_QUALIFIED,
1327
+ EID_OIDC_ISSUER,
1042
1328
  EidClient,
1043
1329
  EidError,
1330
+ EidOidcClient,
1331
+ EidOidcError,
1044
1332
  ForbiddenError,
1045
1333
  Http,
1046
1334
  NetworkError,
@@ -1060,6 +1348,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1060
1348
  isMobileUserAgent,
1061
1349
  openOrShowQR,
1062
1350
  parseSessionResult,
1351
+ pkceChallenge,
1063
1352
  qrDeviceLinkTicker,
1064
1353
  randomChallenge,
1065
1354
  resolveBaseUrl,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType } from './browser-Db82DlYN.cjs';
2
- export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, E as EidError, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-Db82DlYN.cjs';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-DqKDj2nB.cjs';
2
+ export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.cjs';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -218,8 +218,7 @@ interface ValidateAuthOptions {
218
218
  * (verifier-тэй) ирсэн үр дүнг "Web2App"/"App2App"-аар, poll-оор ирснийг "QR"-аар шалга — товчоор
219
219
  * нээгдсэн session poll замаар OK болохгүй, callback-аар л дуусна.
220
220
  *
221
- * ⚠️ Legacy `/dl?sessionId&vc` холбоосонд төрөл хамгаалагдаагүй — build 59 + `min_version=59` хүртэл
222
- * энэ шалгалт relay-ээс бүрэн хамгаалахгүй.
221
+ * Legacy `/dl?sessionId&vc` холбоос 2026-10-07-нд серверээс хасагдсан — төрөл үргэлж authCode-оор хамгаалагдана.
223
222
  */
224
223
  expectedFlowType?: FlowType;
225
224
  /**
@@ -457,4 +456,141 @@ interface QrDeviceLinkTickerOptions {
457
456
  */
458
457
  declare function qrDeviceLinkTicker(input: Omit<BuildDeviceLinkInput, "deviceLinkType" | "now">, onLink: (link: string) => void, opts?: QrDeviceLinkTickerOptions): () => void;
459
458
 
460
- export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
459
+ /** eID-ийн OIDC issuer (төгсгөлд `/` байхгүй). */
460
+ declare const EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
461
+ /** `acr` утгууд — иргэний гэрчилгээний бодит түвшин. */
462
+ declare const EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
463
+ declare const EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
464
+ /**
465
+ * OIDC алдааны код. Authorize redirect (RFC 6749 §4.1.2.1) ба token endpoint (§5.2)-ийн стандарт кодууд, мөн
466
+ * SDK-ийн өөрийн шалгалтын кодууд:
467
+ * - `state_mismatch` — callback-ийн `state` хадгалсантай таарсангүй (CSRF / өөр tab / cookie дууссан) → дахин эхлүүл;
468
+ * - `invalid_callback` — `code` алга / хэт урт, эсвэл `iss` параметр өөр;
469
+ * - `invalid_id_token` — id_token-ий гарын үсэг / iss / aud / exp / iat / nonce шалгалт унасан (ИТГЭХГҮЙ);
470
+ * - `discovery_failed` — discovery/JWKS буруу эсвэл issuer таарахгүй.
471
+ *
472
+ * `access_denied` = иргэн татгалзсан, нэвтрэлтийн хугацаа дууссан, эсвэл гэрчилгээний шалгалт унасан → «дахин оролд».
473
+ */
474
+ type EidOidcErrorCode = "access_denied" | "login_required" | "invalid_request" | "invalid_scope" | "unauthorized_client" | "unsupported_response_type" | "request_not_supported" | "temporarily_unavailable" | "server_error" | "invalid_client" | "invalid_grant" | "unsupported_grant_type" | "state_mismatch" | "invalid_callback" | "invalid_id_token" | "discovery_failed";
475
+ /**
476
+ * OIDC урсгалын алдаа. `code`-оор салгана. `description` нь серверийн `error_description` — логт л,
477
+ * хэрэглэгчид шууд харуулахгүй.
478
+ */
479
+ declare class EidOidcError extends EidError {
480
+ readonly code: EidOidcErrorCode;
481
+ readonly description?: string | undefined;
482
+ constructor(code: EidOidcErrorCode, message: string, description?: string | undefined);
483
+ }
484
+ interface EidOidcClientConfig {
485
+ /** RP UUID (`relyingPartyUUID`). */
486
+ clientId: string;
487
+ /** RP API secret (`rp_sk_…`) — зөвхөн backend-д. */
488
+ clientSecret: string;
489
+ /** Бүртгэлтэй redirect URI — тэмдэгт бүрээрээ таарах ёстой. */
490
+ redirectUri: string;
491
+ /** Анхдагч {@link EID_OIDC_ISSUER}. */
492
+ issuer?: string;
493
+ /** Анхдагч `"openid"`. Иргэний ETSI дугаар хэрэгтэй бол `"openid pnomn"` (RP-д `PNOMN` эрх). */
494
+ scope?: string;
495
+ /** HTTP timeout (мс), анхдагч 10000. */
496
+ timeoutMs?: number;
497
+ /** fetch орлуулагч (тест). */
498
+ fetchImpl?: typeof fetch;
499
+ }
500
+ interface EidOidcDiscovery {
501
+ issuer: string;
502
+ authorization_endpoint: string;
503
+ token_endpoint: string;
504
+ jwks_uri: string;
505
+ [k: string]: unknown;
506
+ }
507
+ interface CreateAuthorizationOptions {
508
+ /** Энэ хүсэлтэд scope-ыг дарна. */
509
+ scope?: string;
510
+ /**
511
+ * `touch` (анхдагч) — утасны хөтчөөс шууд eID апп; `page` — үргэлж нэвтрэх хуудас (QR / push), ж: таблет киоск.
512
+ */
513
+ display?: "page" | "touch";
514
+ }
515
+ /** RP-ийн хадгалах ёстой гүйлгээ — httpOnly cookie / server session-д (браузерийн JS-д биш). */
516
+ interface EidAuthorization {
517
+ /** Хөтчийг энэ рүү 302. */
518
+ url: string;
519
+ state: string;
520
+ nonce: string;
521
+ codeVerifier: string;
522
+ }
523
+ /** createAuthorization-аас хадгалсан утгууд. */
524
+ interface EidCallbackChecks {
525
+ state: string;
526
+ nonce: string;
527
+ codeVerifier: string;
528
+ }
529
+ /** id_token-ий claim-ууд (`docs/OIDC.md` §4). */
530
+ interface EidIdTokenClaims {
531
+ iss: string;
532
+ aud: string | string[];
533
+ azp?: string;
534
+ /** Pairwise — RP тус бүрд өөр, иргэнд тогтмол. Хэрэглэгчийг ҮҮГЭЭР таньна. */
535
+ sub: string;
536
+ exp: number;
537
+ iat: number;
538
+ nonce: string;
539
+ /** Иргэн баталгаажуулсан мөч (Unix сек). */
540
+ auth_time: number;
541
+ /** {@link EID_ACR_QUALIFIED} эсвэл {@link EID_ACR_ADVANCED}. */
542
+ acr: string;
543
+ /** Гэрчилгээний GIVENNAME — латин (ICAO, том үсэг). */
544
+ given_name: string;
545
+ /** Гэрчилгээний SURNAME (эцэг/эхийн нэр) — латин. */
546
+ family_name: string;
547
+ /** Нэр — кирилл (бүртгэлд байхгүй бол алга). */
548
+ given_name_mn?: string;
549
+ /** Эцэг/эхийн нэр — кирилл (бүртгэлд байхгүй бол алга). */
550
+ family_name_mn?: string;
551
+ /** `PNOMN-<иргэний дугаар>` — зөвхөн `pnomn` scope + `PNOMN` эрхтэй үед. */
552
+ pnomn?: string;
553
+ [k: string]: unknown;
554
+ }
555
+ interface EidCallbackResult {
556
+ claims: EidIdTokenClaims;
557
+ idToken: string;
558
+ }
559
+ declare class EidOidcClient {
560
+ private readonly cfg;
561
+ readonly issuer: string;
562
+ private readonly scope;
563
+ private discovery?;
564
+ private jwks?;
565
+ constructor(cfg: EidOidcClientConfig);
566
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
567
+ discover(): Promise<EidOidcDiscovery>;
568
+ private fetchDiscovery;
569
+ /**
570
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
571
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
572
+ */
573
+ createAuthorization(opts?: CreateAuthorizationOptions): Promise<EidAuthorization>;
574
+ /**
575
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
576
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
577
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
578
+ */
579
+ handleCallback(input: string | URL | URLSearchParams, checks: EidCallbackChecks): Promise<EidCallbackResult>;
580
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
581
+ private exchangeCode;
582
+ /**
583
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
584
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
585
+ */
586
+ verifyIdToken(idToken: string, opts: {
587
+ nonce: string;
588
+ }): Promise<EidIdTokenClaims>;
589
+ private keys;
590
+ private getJson;
591
+ private fetch;
592
+ }
593
+ /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
+ declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
+
596
+ export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType } from './browser-Db82DlYN.js';
2
- export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, E as EidError, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-Db82DlYN.js';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-DqKDj2nB.js';
2
+ export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.js';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -218,8 +218,7 @@ interface ValidateAuthOptions {
218
218
  * (verifier-тэй) ирсэн үр дүнг "Web2App"/"App2App"-аар, poll-оор ирснийг "QR"-аар шалга — товчоор
219
219
  * нээгдсэн session poll замаар OK болохгүй, callback-аар л дуусна.
220
220
  *
221
- * ⚠️ Legacy `/dl?sessionId&vc` холбоосонд төрөл хамгаалагдаагүй — build 59 + `min_version=59` хүртэл
222
- * энэ шалгалт relay-ээс бүрэн хамгаалахгүй.
221
+ * Legacy `/dl?sessionId&vc` холбоос 2026-10-07-нд серверээс хасагдсан — төрөл үргэлж authCode-оор хамгаалагдана.
223
222
  */
224
223
  expectedFlowType?: FlowType;
225
224
  /**
@@ -457,4 +456,141 @@ interface QrDeviceLinkTickerOptions {
457
456
  */
458
457
  declare function qrDeviceLinkTicker(input: Omit<BuildDeviceLinkInput, "deviceLinkType" | "now">, onLink: (link: string) => void, opts?: QrDeviceLinkTickerOptions): () => void;
459
458
 
460
- export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
459
+ /** eID-ийн OIDC issuer (төгсгөлд `/` байхгүй). */
460
+ declare const EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
461
+ /** `acr` утгууд — иргэний гэрчилгээний бодит түвшин. */
462
+ declare const EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
463
+ declare const EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
464
+ /**
465
+ * OIDC алдааны код. Authorize redirect (RFC 6749 §4.1.2.1) ба token endpoint (§5.2)-ийн стандарт кодууд, мөн
466
+ * SDK-ийн өөрийн шалгалтын кодууд:
467
+ * - `state_mismatch` — callback-ийн `state` хадгалсантай таарсангүй (CSRF / өөр tab / cookie дууссан) → дахин эхлүүл;
468
+ * - `invalid_callback` — `code` алга / хэт урт, эсвэл `iss` параметр өөр;
469
+ * - `invalid_id_token` — id_token-ий гарын үсэг / iss / aud / exp / iat / nonce шалгалт унасан (ИТГЭХГҮЙ);
470
+ * - `discovery_failed` — discovery/JWKS буруу эсвэл issuer таарахгүй.
471
+ *
472
+ * `access_denied` = иргэн татгалзсан, нэвтрэлтийн хугацаа дууссан, эсвэл гэрчилгээний шалгалт унасан → «дахин оролд».
473
+ */
474
+ type EidOidcErrorCode = "access_denied" | "login_required" | "invalid_request" | "invalid_scope" | "unauthorized_client" | "unsupported_response_type" | "request_not_supported" | "temporarily_unavailable" | "server_error" | "invalid_client" | "invalid_grant" | "unsupported_grant_type" | "state_mismatch" | "invalid_callback" | "invalid_id_token" | "discovery_failed";
475
+ /**
476
+ * OIDC урсгалын алдаа. `code`-оор салгана. `description` нь серверийн `error_description` — логт л,
477
+ * хэрэглэгчид шууд харуулахгүй.
478
+ */
479
+ declare class EidOidcError extends EidError {
480
+ readonly code: EidOidcErrorCode;
481
+ readonly description?: string | undefined;
482
+ constructor(code: EidOidcErrorCode, message: string, description?: string | undefined);
483
+ }
484
+ interface EidOidcClientConfig {
485
+ /** RP UUID (`relyingPartyUUID`). */
486
+ clientId: string;
487
+ /** RP API secret (`rp_sk_…`) — зөвхөн backend-д. */
488
+ clientSecret: string;
489
+ /** Бүртгэлтэй redirect URI — тэмдэгт бүрээрээ таарах ёстой. */
490
+ redirectUri: string;
491
+ /** Анхдагч {@link EID_OIDC_ISSUER}. */
492
+ issuer?: string;
493
+ /** Анхдагч `"openid"`. Иргэний ETSI дугаар хэрэгтэй бол `"openid pnomn"` (RP-д `PNOMN` эрх). */
494
+ scope?: string;
495
+ /** HTTP timeout (мс), анхдагч 10000. */
496
+ timeoutMs?: number;
497
+ /** fetch орлуулагч (тест). */
498
+ fetchImpl?: typeof fetch;
499
+ }
500
+ interface EidOidcDiscovery {
501
+ issuer: string;
502
+ authorization_endpoint: string;
503
+ token_endpoint: string;
504
+ jwks_uri: string;
505
+ [k: string]: unknown;
506
+ }
507
+ interface CreateAuthorizationOptions {
508
+ /** Энэ хүсэлтэд scope-ыг дарна. */
509
+ scope?: string;
510
+ /**
511
+ * `touch` (анхдагч) — утасны хөтчөөс шууд eID апп; `page` — үргэлж нэвтрэх хуудас (QR / push), ж: таблет киоск.
512
+ */
513
+ display?: "page" | "touch";
514
+ }
515
+ /** RP-ийн хадгалах ёстой гүйлгээ — httpOnly cookie / server session-д (браузерийн JS-д биш). */
516
+ interface EidAuthorization {
517
+ /** Хөтчийг энэ рүү 302. */
518
+ url: string;
519
+ state: string;
520
+ nonce: string;
521
+ codeVerifier: string;
522
+ }
523
+ /** createAuthorization-аас хадгалсан утгууд. */
524
+ interface EidCallbackChecks {
525
+ state: string;
526
+ nonce: string;
527
+ codeVerifier: string;
528
+ }
529
+ /** id_token-ий claim-ууд (`docs/OIDC.md` §4). */
530
+ interface EidIdTokenClaims {
531
+ iss: string;
532
+ aud: string | string[];
533
+ azp?: string;
534
+ /** Pairwise — RP тус бүрд өөр, иргэнд тогтмол. Хэрэглэгчийг ҮҮГЭЭР таньна. */
535
+ sub: string;
536
+ exp: number;
537
+ iat: number;
538
+ nonce: string;
539
+ /** Иргэн баталгаажуулсан мөч (Unix сек). */
540
+ auth_time: number;
541
+ /** {@link EID_ACR_QUALIFIED} эсвэл {@link EID_ACR_ADVANCED}. */
542
+ acr: string;
543
+ /** Гэрчилгээний GIVENNAME — латин (ICAO, том үсэг). */
544
+ given_name: string;
545
+ /** Гэрчилгээний SURNAME (эцэг/эхийн нэр) — латин. */
546
+ family_name: string;
547
+ /** Нэр — кирилл (бүртгэлд байхгүй бол алга). */
548
+ given_name_mn?: string;
549
+ /** Эцэг/эхийн нэр — кирилл (бүртгэлд байхгүй бол алга). */
550
+ family_name_mn?: string;
551
+ /** `PNOMN-<иргэний дугаар>` — зөвхөн `pnomn` scope + `PNOMN` эрхтэй үед. */
552
+ pnomn?: string;
553
+ [k: string]: unknown;
554
+ }
555
+ interface EidCallbackResult {
556
+ claims: EidIdTokenClaims;
557
+ idToken: string;
558
+ }
559
+ declare class EidOidcClient {
560
+ private readonly cfg;
561
+ readonly issuer: string;
562
+ private readonly scope;
563
+ private discovery?;
564
+ private jwks?;
565
+ constructor(cfg: EidOidcClientConfig);
566
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
567
+ discover(): Promise<EidOidcDiscovery>;
568
+ private fetchDiscovery;
569
+ /**
570
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
571
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
572
+ */
573
+ createAuthorization(opts?: CreateAuthorizationOptions): Promise<EidAuthorization>;
574
+ /**
575
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
576
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
577
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
578
+ */
579
+ handleCallback(input: string | URL | URLSearchParams, checks: EidCallbackChecks): Promise<EidCallbackResult>;
580
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
581
+ private exchangeCode;
582
+ /**
583
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
584
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
585
+ */
586
+ verifyIdToken(idToken: string, opts: {
587
+ nonce: string;
588
+ }): Promise<EidIdTokenClaims>;
589
+ private keys;
590
+ private getJson;
591
+ private fetch;
592
+ }
593
+ /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
+ declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
+
596
+ export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
package/dist/index.js CHANGED
@@ -897,14 +897,296 @@ function qrDeviceLinkTicker(input, onLink, opts) {
897
897
  t.unref?.();
898
898
  return () => clearInterval(t);
899
899
  }
900
+
901
+ // src/oidc.ts
902
+ import { timingSafeEqual } from "crypto";
903
+ var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
904
+ var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
905
+ var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
906
+ var CLOCK_SKEW_S = 60;
907
+ var CACHE_MS = 60 * 60 * 1e3;
908
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
909
+ var KNOWN_CODES = /* @__PURE__ */ new Set([
910
+ "access_denied",
911
+ "login_required",
912
+ "invalid_request",
913
+ "invalid_scope",
914
+ "unauthorized_client",
915
+ "unsupported_response_type",
916
+ "request_not_supported",
917
+ "temporarily_unavailable",
918
+ "server_error",
919
+ "invalid_client",
920
+ "invalid_grant",
921
+ "unsupported_grant_type"
922
+ ]);
923
+ var EidOidcError = class extends EidError {
924
+ constructor(code, message, description) {
925
+ super(message);
926
+ this.code = code;
927
+ this.description = description;
928
+ }
929
+ code;
930
+ description;
931
+ };
932
+ var EidOidcClient = class {
933
+ constructor(cfg) {
934
+ this.cfg = cfg;
935
+ if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
936
+ if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
937
+ if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
938
+ this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
939
+ this.scope = cfg.scope ?? "openid";
940
+ }
941
+ cfg;
942
+ issuer;
943
+ scope;
944
+ discovery;
945
+ jwks;
946
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
947
+ discover() {
948
+ const c = this.discovery;
949
+ if (c && Date.now() - c.at < CACHE_MS) return c.d;
950
+ const d = this.fetchDiscovery();
951
+ this.discovery = { at: Date.now(), d };
952
+ d.catch(() => {
953
+ if (this.discovery?.d === d) this.discovery = void 0;
954
+ });
955
+ return d;
956
+ }
957
+ async fetchDiscovery() {
958
+ const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
959
+ if (!d || d.issuer !== this.issuer) {
960
+ throw new EidOidcError("discovery_failed", `OIDC discovery-\u0438\u0439\u043D issuer \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (\u0445\u04AF\u043B\u044D\u044D\u0441\u044D\u043D ${this.issuer})`);
961
+ }
962
+ for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
963
+ if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
964
+ throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
965
+ }
966
+ }
967
+ return d;
968
+ }
969
+ /**
970
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
971
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
972
+ */
973
+ async createAuthorization(opts = {}) {
974
+ const d = await this.discover();
975
+ const state = randomB64url(32);
976
+ const nonce = randomB64url(32);
977
+ const codeVerifier = randomB64url(32);
978
+ const u = new URL(d.authorization_endpoint);
979
+ const p = new URLSearchParams({
980
+ response_type: "code",
981
+ client_id: this.cfg.clientId,
982
+ redirect_uri: this.cfg.redirectUri,
983
+ scope: opts.scope ?? this.scope,
984
+ state,
985
+ nonce,
986
+ code_challenge: await pkceChallenge(codeVerifier),
987
+ code_challenge_method: "S256"
988
+ });
989
+ if (opts.display) p.set("display", opts.display);
990
+ u.search = p.toString();
991
+ return { url: u.toString(), state, nonce, codeVerifier };
992
+ }
993
+ /**
994
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
995
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
996
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
997
+ */
998
+ async handleCallback(input, checks) {
999
+ const q = callbackParams(input);
1000
+ const state = q.get("state") ?? "";
1001
+ if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
1002
+ throw new EidOidcError("state_mismatch", "OIDC state \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439 \u2014 \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u0433 \u0434\u0430\u0445\u0438\u043D \u044D\u0445\u043B\u04AF\u04AF\u043B\u043D\u044D \u04AF\u04AF");
1003
+ }
1004
+ const iss = q.get("iss");
1005
+ if (iss !== null && iss !== this.issuer) {
1006
+ throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1007
+ }
1008
+ const err = q.get("error");
1009
+ if (err) {
1010
+ const code2 = KNOWN_CODES.has(err) ? err : "server_error";
1011
+ throw new EidOidcError(code2, `eID \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442 \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${code2}`, q.get("error_description") ?? void 0);
1012
+ }
1013
+ const code = q.get("code") ?? "";
1014
+ if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
1015
+ const idToken = await this.exchangeCode(code, checks.codeVerifier);
1016
+ const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
1017
+ return { claims, idToken };
1018
+ }
1019
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
1020
+ async exchangeCode(code, codeVerifier) {
1021
+ const d = await this.discover();
1022
+ const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
1023
+ const res = await this.fetch(d.token_endpoint, {
1024
+ method: "POST",
1025
+ headers: {
1026
+ Authorization: "Basic " + basic,
1027
+ "Content-Type": "application/x-www-form-urlencoded",
1028
+ Accept: "application/json"
1029
+ },
1030
+ body: new URLSearchParams({
1031
+ grant_type: "authorization_code",
1032
+ code,
1033
+ redirect_uri: this.cfg.redirectUri,
1034
+ code_verifier: codeVerifier
1035
+ }).toString()
1036
+ });
1037
+ let j = {};
1038
+ try {
1039
+ j = await res.json();
1040
+ } catch {
1041
+ }
1042
+ if (!res.ok) {
1043
+ const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
1044
+ throw new EidOidcError(
1045
+ e,
1046
+ `OIDC token ${res.status}: ${e}`,
1047
+ typeof j.error_description === "string" ? j.error_description : void 0
1048
+ );
1049
+ }
1050
+ if (typeof j.id_token !== "string" || j.id_token === "") {
1051
+ throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
1052
+ }
1053
+ return j.id_token;
1054
+ }
1055
+ /**
1056
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
1057
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
1058
+ */
1059
+ async verifyIdToken(idToken, opts) {
1060
+ const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
1061
+ const parts = idToken.split(".");
1062
+ if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1063
+ const header = decodeJsonPart(parts[0]);
1064
+ const payload = decodeJsonPart(parts[1]);
1065
+ if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1066
+ if (header.alg !== "ES256") throw bad("alg ES256 \u0431\u0438\u0448");
1067
+ const kid = typeof header.kid === "string" ? header.kid : "";
1068
+ if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
1069
+ const pick = (ks) => ks.find(
1070
+ (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")
1071
+ );
1072
+ const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
1073
+ if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
1074
+ const sig = Buffer.from(parts[2], "base64url");
1075
+ 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");
1076
+ let ok = false;
1077
+ try {
1078
+ const key = await globalThis.crypto.subtle.importKey(
1079
+ "jwk",
1080
+ { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
1081
+ { name: "ECDSA", namedCurve: "P-256" },
1082
+ false,
1083
+ ["verify"]
1084
+ );
1085
+ ok = await globalThis.crypto.subtle.verify(
1086
+ { name: "ECDSA", hash: "SHA-256" },
1087
+ key,
1088
+ sig,
1089
+ new TextEncoder().encode(parts[0] + "." + parts[1])
1090
+ );
1091
+ } catch {
1092
+ ok = false;
1093
+ }
1094
+ if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
1095
+ const c = payload;
1096
+ const now = Math.floor(Date.now() / 1e3);
1097
+ if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1098
+ const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
1099
+ if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1100
+ if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1101
+ 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");
1102
+ if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
1103
+ if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1104
+ if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
1105
+ return c;
1106
+ }
1107
+ keys(force) {
1108
+ const c = this.jwks;
1109
+ if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
1110
+ const keys = this.discover().then(async (d) => {
1111
+ const j = await this.getJson(d.jwks_uri);
1112
+ return Array.isArray(j?.keys) ? j.keys : [];
1113
+ });
1114
+ this.jwks = { at: Date.now(), keys };
1115
+ keys.catch(() => {
1116
+ if (this.jwks?.keys === keys) this.jwks = void 0;
1117
+ });
1118
+ return keys;
1119
+ }
1120
+ async getJson(url) {
1121
+ const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1122
+ if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
1123
+ try {
1124
+ return await res.json();
1125
+ } catch {
1126
+ throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
1127
+ }
1128
+ }
1129
+ async fetch(url, init) {
1130
+ const f = this.cfg.fetchImpl ?? fetch;
1131
+ try {
1132
+ return await f(url, {
1133
+ ...init,
1134
+ redirect: "error",
1135
+ signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
1136
+ });
1137
+ } catch (e) {
1138
+ const err = e;
1139
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
1140
+ throw new NetworkError(`OIDC \u0445\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${url}`);
1141
+ }
1142
+ throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
1143
+ }
1144
+ }
1145
+ };
1146
+ async function pkceChallenge(codeVerifier) {
1147
+ const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
1148
+ return Buffer.from(h).toString("base64url");
1149
+ }
1150
+ function randomB64url(bytes) {
1151
+ return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
1152
+ }
1153
+ function formEncode(s) {
1154
+ return new URLSearchParams({ v: s }).toString().slice(2);
1155
+ }
1156
+ function timingSafeEqualStr(a, b) {
1157
+ const x = Buffer.from(a, "utf8");
1158
+ const y = Buffer.from(b, "utf8");
1159
+ return x.length === y.length && timingSafeEqual(x, y);
1160
+ }
1161
+ function decodeJsonPart(s) {
1162
+ try {
1163
+ const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
1164
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
1165
+ } catch {
1166
+ return null;
1167
+ }
1168
+ }
1169
+ function callbackParams(input) {
1170
+ if (input instanceof URLSearchParams) return input;
1171
+ if (input instanceof URL) return input.searchParams;
1172
+ const s = input.trim();
1173
+ if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
1174
+ const i = s.indexOf("?");
1175
+ return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
1176
+ }
900
1177
  export {
901
1178
  ApiError,
902
1179
  AuthApi,
903
1180
  AuthenticationError,
904
1181
  DEFAULT_BASE_URL,
905
1182
  DeviceLinkError,
1183
+ EID_ACR_ADVANCED,
1184
+ EID_ACR_QUALIFIED,
1185
+ EID_OIDC_ISSUER,
906
1186
  EidClient,
907
1187
  EidError,
1188
+ EidOidcClient,
1189
+ EidOidcError,
908
1190
  ForbiddenError,
909
1191
  Http,
910
1192
  NetworkError,
@@ -924,6 +1206,7 @@ export {
924
1206
  isMobileUserAgent,
925
1207
  openOrShowQR,
926
1208
  parseSessionResult,
1209
+ pkceChallenge,
927
1210
  qrDeviceLinkTicker,
928
1211
  randomChallenge,
929
1212
  resolveBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsup src/index.ts src/browser.ts --format esm,cjs --dts --clean",
32
- "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts",
32
+ "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/oidc.test.ts",
33
33
  "lint": "tsc --noEmit"
34
34
  },
35
35
  "keywords": [