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

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
@@ -5,9 +5,9 @@
5
5
  RP-API v3, Smart-ID нийцтэй. Бүрэн баримт: <https://developer.eidmongolia.mn/>.
6
6
 
7
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).
8
+ > хэзээ ч задлахгүй. Браузерт зөвхөн таны backend-ийн өгсөн холбоос/QR/товч харагдана — «eID-ээр нэвтрэх» товч
9
+ > (`/button`) ч таны backend-ийг л дуудна (§1a). `@gerege-systems/eid-mongolia-sdk/browser`-ийн legacy device-link
10
+ > туслахууд 2026-10-07-оос ажиллахгүй (§4).
11
11
 
12
12
  ## Суулгах
13
13
 
@@ -22,125 +22,104 @@ Node.js ≥ 22 (global `fetch`, `node:crypto`).
22
22
  RP-г eID Mongolia-гийн оператор бүртгэнэ — хүсэлтээ <eIDsupport@gerege.com> эсвэл 7777-3773-аар гаргана
23
23
  (нийтийн `POST /v3/rp-applications` 2026-10-14-нөөс засвартай: `503 rp_applications_maintenance`). Оператор
24
24
  баталсны дараа **relyingPartyUUID** ба **API secret** (`rp_sk_…`) олгогдоно; secret **зөвхөн нэг удаа** харагдана.
25
- Шаардлагатай мэдээлэл (байгууллага, зорилго `AUTH`/`SIGN`, `callbackHosts`, OIDC redirect URI…):
25
+ Шаардлагатай мэдээлэл (байгууллага, зорилго `AUTH`/`SIGN`, `callbackHosts`…):
26
26
  <https://developer.eidmongolia.mn/#registration>.
27
27
 
28
- ## 1a. «eID-ээр нэвтрэх» (OpenID Connect)
28
+ ## 1a. «eID-ээр нэвтрэх» — нэг товч
29
29
 
30
- Хамгийн хялбар нэвтрэлт: таны вэбэд нэг товч, нэвтрэх хуудсыг (QR · утсан дээрх аппаар нээх · регистрээр
31
- мэдэгдэл) eID сервер өөрөө харуулж, гарын үсэг ба гэрчилгээний гинжийг шалгаад танд стандарт OpenID Connect
32
- (Authorization Code + PKCE `S256`) `id_token` өгнө — Google/Apple нэвтрэлттэй ижил. Утасны хөтчөөс дарахад
33
- хуудасгүйгээр шууд eID апп нээгдэнэ. Гэрээ: <https://developer.eidmongolia.mn/#oidc>.
30
+ Smart-ID-тэй RP-үүдийн хэв маяг: хөтөчид **нэг товч**, бүх eID дуудлага таны **backend**-д (API secret-ээр). SDK хоёр
31
+ хэсэг өгнө — backend-ийн бэлэн урсгал `createEidLogin` ба хөтчийн товч `@gerege-systems/eid-mongolia-sdk/button`.
32
+ Доорх нь §2/§4-ийн RP-API (device link v3, push, `ResponseValidator`)-ийг л ашиглана; тусдаа протокол биш.
34
33
 
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-ээр солино (доор).
34
+ | Хаана | Юу болно |
35
+ |---|---|
36
+ | Desktop хөтөч | Цонх: QR (секунд тутам шинэчлэгдэнэ) + «регистрийн дугаараар утас руу мэдэгдэл» (push), үлдсэн хугацаа, шууд төлөв |
37
+ | Утасны хөтөч | Товч → `start?mode=web2app` → сервер 302 → eID апп (Web2App). Апп баталгаажуулаад `/auth/eid/callback` руу буцаана |
38
+ | Backend | session эхлүүлэх, QR угсрах (`sessionSecret` серверт), poll, `validateAuth` (гинж + ACSP_V2 + `flowType` + Web2App-д `userChallengeVerifier`) → таны `onSuccess` |
39
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 мөрдөгдөнө.
40
+ **Endpoint-ууд** (`/auth/eid` дор — eID сервер Web2App буцах замыг үргэлж `/auth/eid/callback` болгож нормчилдог):
44
41
 
45
- **mTLS-тэй RP (`require_mtls`).** `login.` хост mTLS хийдэггүй тул ийм RP-ийн code солих хүсэлт
46
- `login.eidmongolia.mn/oauth2/token` дээр `401 invalid_client` авна. Discovery-ийн `mtls_endpoint_aliases.token_endpoint`
47
- (RFC 8705 §5, `https://rp.eidmongolia.mn/oauth2/token`) руу RP-API-д хэрэглэдэг **ижил client cert**-ээр явуулна:
48
- `useMtlsEndpointAliases: true` + `dispatcher` (RP-API client-ийнхтэй ижил undici `Agent`). Discovery-д alias байхгүй
49
- бол `token_endpoint` руу буцна; alias https биш бол `discovery_failed`. mTLS-гүй RP флаг тавихгүй.
42
+ | | Хүсэлт | Хариу |
43
+ |---|---|---|
44
+ | `POST /auth/eid/start` | `{"mode":"qr"}` эсвэл `{"mode":"push","idCode":"УБ12345678"}` (JSON заавал) | `{mode, vc, qr?, expiresIn}` + `Set-Cookie: eid_login` |
45
+ | `GET /auth/eid/start?mode=web2app` | утасны хөтөч (товчны навигаци) | `302` → eID апп |
46
+ | `GET /auth/eid/status` | cookie | `PENDING` (+шинэ `qr`, `expiresIn`) · `WAIT_CALLBACK` · `OK` (`redirect`, `data`) · `FAILED` (`error`) · `EXPIRED` |
47
+ | `GET /auth/eid/callback` | апп буцаана: `?sessionId&userChallengeVerifier` | `303` → `onSuccess`-ийн `redirect` / `successRedirect`, алдаа бол `failureRedirect?eid_error=<код>` |
50
48
 
51
- ```ts
52
- import { Agent } from "undici";
53
- const oidc = new EidOidcClient({
54
- clientId: process.env.EID_RP_UUID!,
55
- clientSecret: process.env.EID_SECRET!,
56
- redirectUri: "https://bank.example.mn/auth/eid/callback",
57
- dispatcher: new Agent({ connect: { cert: clientCertPem, key: clientKeyPem } }), // RP-API-ийн cert
58
- useMtlsEndpointAliases: true, // token → https://rp.eidmongolia.mn/oauth2/token
59
- });
60
- ```
49
+ **Backend** (нэг instance-ийг модулийн түвшинд):
61
50
 
62
51
  ```ts
63
- import { EidOidcClient, EidOidcError } from "@gerege-systems/eid-mongolia-sdk";
52
+ import { EidClient, createEidLogin } from "@gerege-systems/eid-mongolia-sdk";
64
53
 
65
- const oidc = new EidOidcClient({
66
- clientId: process.env.EID_RP_UUID!,
67
- clientSecret: process.env.EID_SECRET!, // rp_sk_… — зөвхөн backend
68
- redirectUri: "https://bank.example.mn/auth/eid/callback",
69
- // issuer анхдагч https://login.eidmongolia.mn, scope анхдагч "openid"
70
- });
71
-
72
- const TX = "eid_oidc_tx";
73
- const cookieOpts = { httpOnly: true, secure: true, sameSite: "lax", path: "/auth/eid", maxAge: 600_000 } as const;
74
-
75
- // GET /auth/eid/login — товчны href
76
- app.get("/auth/eid/login", async (_req, res) => {
77
- const a = await oidc.createAuthorization(); // { url, state, nonce, codeVerifier }
78
- res.cookie(TX, JSON.stringify({ state: a.state, nonce: a.nonce, codeVerifier: a.codeVerifier }), cookieOpts);
79
- res.set("Cache-Control", "no-store").redirect(302, a.url);
54
+ const eid = new EidClient({
55
+ credentials: { rpUUID: process.env.EID_RP_UUID!, rpName: "Интернэт банк", apiSecret: process.env.EID_SECRET! },
56
+ trust: { trustAnchorsPem: [process.env.EID_NATIONAL_ROOT_CA_PEM!] },
80
57
  });
81
58
 
82
- // GET /auth/eid/callback?code&state (эсвэл ?error) — бүртгэлтэй redirect_uri
83
- app.get("/auth/eid/callback", async (req, res) => {
84
- const tx = JSON.parse(req.cookies[TX] ?? "null");
85
- res.clearCookie(TX, { path: "/auth/eid" });
86
- if (!tx) return res.redirect(303, "/login?error=state");
87
- try {
88
- const { claims } = await oidc.handleCallback(req.originalUrl, tx); // state → token → id_token (ES256)
89
- const user = await users.findOrCreateBySub(claims.sub); // pairwise sub-аар таньна (нэрээр биш)
90
- await startSession(res, user); // таны session-ийг шинээр үүсгэ (session fixation)
91
- res.redirect(303, "/");
92
- } catch (e) {
93
- const code = e instanceof EidOidcError ? e.code : "server_error"; // access_denied → «дахин оролдоно уу»
94
- res.redirect(303, `/login?error=${code}`);
95
- }
59
+ export const eidLogin = createEidLogin({
60
+ client: eid,
61
+ callbackUrl: "https://bank.example.mn/auth/eid/callback", // host нь RP-ийн callbackHosts-д; утасны урсгалд заавал
62
+ async onSuccess(id) {
63
+ // id.etsi (PNOMN-…) — хэрэглэгчийг ҮҮГЭЭР таньна; id.givenName/surname (латин), id.person (кирилл, байвал)
64
+ const user = await users.findOrCreateByEtsi(id.etsi);
65
+ const sid = await sessions.create(user.id); // ШИНЭ session (session fixation)
66
+ return {
67
+ redirect: "/dashboard",
68
+ headers: { "Set-Cookie": `app_session=${sid}; Path=/; HttpOnly; Secure; SameSite=Lax` },
69
+ };
70
+ },
71
+ // store: redisStore, // олон instance / serverless бол (анхдагч — процессын санах ой)
72
+ // rateLimit: (key) => redisLimiter.allow(key), // "start:<ip>", "push:<idCode>"; анхдагч push 3/мин нэг дугаарт
96
73
  });
97
74
  ```
98
75
 
99
- Next.js App Router-д ижил: `app/auth/eid/login/route.ts` → `NextResponse.redirect(a.url, 302)` + `res.cookies.set(…)`;
100
- callback → `oidc.handleCallback(request.nextUrl.searchParams, tx)` (зөвхөн query уншина — прокси ард host хамаагүй).
76
+ Next.js App Router — `app/auth/eid/[action]/route.ts`:
101
77
 
102
- **Товч** — энгийн холбоос (`<a href>` эсвэл form GET). `fetch`/XHR/JS redirect биш: iOS Universal Link зөвхөн
103
- хэрэглэгчийн tap-аас эхэлсэн redirect гинжид eID аппыг нээнэ.
78
+ ```ts
79
+ import { eidLogin } from "@/lib/eid";
80
+ export const dynamic = "force-dynamic";
81
+ export const GET = (req: Request) => eidLogin.handle(req);
82
+ export const POST = (req: Request) => eidLogin.handle(req);
83
+ ```
104
84
 
105
- ```html
106
- <a class="eid-login" href="/auth/eid/login">
107
- <img src="/img/eid-mongolia.png" alt="" width="20" height="20">
108
- eID Mongolia-аар нэвтрэх
109
- </a>
110
- <style>
111
- .eid-login { display: inline-flex; align-items: center; justify-content: center; gap: .5rem; min-height: 44px;
112
- padding: 0 1.25rem; border: 1px solid #d0d5dd; border-radius: 6px; background: #fff; color: #111827;
113
- font: 500 1rem/1 system-ui, sans-serif; text-decoration: none; }
114
- .eid-login:hover { background: #f5f7fa; }
115
- .eid-login:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
116
- </style>
85
+ Express:
86
+
87
+ ```ts
88
+ app.use("/auth/eid", express.json(), async (req, res) => {
89
+ const r = await eidLogin.handleRequest({ method: req.method, url: req.originalUrl, headers: req.headers, body: req.body, ip: req.ip });
90
+ res.status(r.status).set(r.headers).send(r.body);
91
+ });
117
92
  ```
118
93
 
119
- Бичвэр «eID Mongolia-аар нэвтрэх», зүүн талд eID Mongolia лого (<https://developer.eidmongolia.mn/logo-96.png>-ийг
120
- татаж өөрийн хостоос үйлчил). Google/Apple-ийн товч шиг саармаг (цагаан, хүрээтэй) — таны сайтын бусад нэвтрэх
121
- товчтой зэрэгцэж болно. Жишээ: <https://demo.eidmongolia.mn>.
94
+ **Хөтөч** — Web Component (React/Vue/энгийн HTML аль нь ч):
122
95
 
123
- `createAuthorization({ display: "page" })` — утсан дээр ч нэвтрэх хуудас (QR / push) харуулна (ж: таблет киоск).
96
+ ```html
97
+ <script type="module">import "@gerege-systems/eid-mongolia-sdk/button";</script>
98
+ <eid-login-button start-url="/auth/eid/start" status-url="/auth/eid/status" lang="mn"></eid-login-button>
99
+ <script type="module">
100
+ document.querySelector("eid-login-button").addEventListener("eid-login-success", (e) => {
101
+ // e.detail = { state: "OK", redirect, data } — preventDefault() хийвэл redirect хийхгүй
102
+ });
103
+ </script>
104
+ ```
124
105
 
125
- **`id_token` claims** (`EidIdTokenClaims`): `sub` (pairwise — RP тус бүрд өөр, иргэнд тогтмол), `acr`
126
- (`urn:eidmongolia:loa:qualified` | `…:advanced`), `auth_time`, `given_name` / `family_name` (латин, гэрчилгээнээс),
127
- `given_name_mn` / `family_name_mn` (кирилл, байвал), `pnomn` (`pnomn` scope + эрхтэй үед). `id_token` 5 минут
128
- хүчинтэй; access_token / refresh_token / userinfo байхгүй — нэвтрэлтийн дараа өөрийн session-оо хэрэглэнэ.
106
+ эсвэл `mountEidLoginButton(el, { startUrl, statusUrl, lang: "en", successUrl, onSuccess, onError })`.
107
+ Атрибут: `lang` (`mn` | `en`), `mode` (`auto` анхдагч — утас → Web2App, бусад → QR цонх; `qr`; `app`), `success-url`.
108
+ Үйл явдал: `eid-login-success`, `eid-login-error` (`detail.error`: `USER_REFUSED…`, `VALIDATION`, `EXPIRED` …).
109
+ Дизайн нь eID нэвтрэх хуудсынх (лого, өнгө, light/dark); `--eid-accent`, `--eid-bg` г.м. CSS хувьсагчаар, `::part(button)`-оор
110
+ тохируулна. Гадны хамааралгүй (QR encoder дотроо), цонх нь focus trap, Esc, `aria-live` төлөвтэй.
129
111
 
130
112
  **Аюулгүй байдал.**
131
113
 
132
- - `state`, `nonce`, `codeVerifier`-ийг **хөтчийн session-тэй** (httpOnly, Secure, SameSite=Lax cookie эсвэл server
133
- session) холбож хадгална; `handleCallback` state-ийг timing-safe тулгаж, таарахгүй бол code-ийг солихгүй
134
- (`state_mismatch`). Үүнгүйгээр «өөрийн бүртгэлд хохирогчийн eID-ийг холбуулах» (login CSRF) нээлттэй.
135
- - `handleCallback` / `verifyIdToken` нь ES256 гарын үсэг (JWKS `kid`, cache 1 цаг, танихгүй `kid` → нэг удаа дахин
136
- татна), `iss`, `aud` (= `clientId`; олон aud бол `azp`), `exp` / `iat` (±60 сек), `nonce`-ийг шалгана.
137
- `EidOidcError` (`invalid_id_token`) гарвал **итгэхгүй**.
138
- - Code нэг удаагийн, 60 секунд — callback-ийг дахин ачаалбал `invalid_grant`; хэрэглэгчийг дахин нэвтрүүлнэ.
139
- - `error_description` (`EidOidcError.description`)-ийг логт бичиж болно, хэрэглэгчид шууд харуулахгүй.
140
- - **Native апп (RFC 8252):** authorize-ийг системийн хөтчөөр нээнэ — iOS `ASWebAuthenticationSession`
141
- (`prefersEphemeralWebBrowserSession = false`), Android Custom Tabs. **WebView хориотой.** `redirect_uri` = таны
142
- https Universal Link / App Link (custom scheme дэмжихгүй). `state` / `nonce` / `codeVerifier` апп-ын санах ойд;
143
- code-ийг таны backend `client_secret`-ээр солино (`client_secret`-ийг аппад хадгалахгүй).
114
+ - `sessionToken`, `sessionSecret`, `rpChallenge` серверт (store) үлдэнэ; хөтөчид зөвхөн HttpOnly, Secure, SameSite=Lax cookie
115
+ (`<sessionId>.<binding>`, store-д binding-ийн SHA-256). Status ба callback зөвхөн нэвтрэлтийг **эхлүүлсэн хөтөчид** хариулна —
116
+ өөр хөтчид нээгдсэн callback URL (verifier-тэй ч) хүлээн авагдахгүй (login CSRF).
117
+ - `start` POST нь JSON заавал (simple request биш → cross-origin preflight), `Sec-Fetch-Site: cross-site` → 403.
118
+ - `OK` нь зөвхөн `validateAuth` амжилттай үед: QR → `flowType=QR`, push → `Notification`, утас → `Web2App` + callback-ийн
119
+ `userChallengeVerifier`. Web2App session-ийг poll дуусгахгүй (`WAIT_CALLBACK`). `onSuccess` нэг session-д нэг л удаа
120
+ (store-ийн `delete` атомар байх ёстой — Redis `DEL`).
121
+ - `onSuccess`-ийн `data` хөтөчид очно — нууц бүү оруул. Иргэний бүртгэлтэй эсэхийг push алдаагаар задлахгүй (`eid_unavailable`).
122
+ - Олон instance / serverless (Vercel г.м.) дээр анхдагч санах ойн store ажиллахгүй — `EidLoginStore` (`get`/`set`/`delete`)-ийг Redis-ээр.
144
123
 
145
124
  ## 2. Нэвтрэлт (push)
146
125
 
@@ -445,7 +424,6 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
445
424
  | `ApiError` | бусад HTTP алдаа (`.status`, `.body`, серверийн `.code`, `.retryAfterMs`, `.headers`, `.retryable` — ж: 404 `ORG_NOT_FOUND`) |
446
425
  | `ResponseTooLargeError` | хариу SDK-ийн хязгаараас их (JSON 4 MiB, PDF `pdfMaxBytes`) — `.op`, `.limit` |
447
426
  | `NetworkError` | timeout/сүлжээ (`.op` — таних тэмдэггүй зам, `.timeout`) |
448
- | `EidOidcError` | «eID-ээр нэвтрэх» (OIDC) — `.code`: `access_denied`, `login_required`, `state_mismatch`, `invalid_grant`, `invalid_id_token`, `server_error` … |
449
427
  | `DeviceLinkError` | device-link холбоос угсрах боломжгүй (`deviceLinkBase` https биш, token/secret/lang хэлбэр, төрөлд хэрэгтэй талбар дутуу) |
450
428
 
451
429
  ## mTLS (eIDAS qualified орчин)
@@ -469,9 +447,7 @@ npm test # build + node --test
469
447
  ### Гэрээний шалгалт
470
448
 
471
449
  SDK-ийн дууддаг RP-API зам бүр (`src/contract.ts`) live [`/.well-known/eid`](https://rp.eidmongolia.mn/.well-known/eid)-ийн
472
- `endpoints`-д байх ёстой; OIDC-ийн хүлээдэг утгууд (issuer, `ES256`, PKCE `S256`, `client_secret_basic`, `EidIdTokenClaims`-ийн
473
- талбарууд) [discovery](https://login.eidmongolia.mn/.well-known/openid-configuration)-тэй таарах ёстой. Discovery-д
474
- байгаа ч SDK хамраагүй замууд WARN (fail биш). Сүлжээ шаарддаг тул `npm test`-д ордоггүй:
450
+ `endpoints`-д байх ёстой. Discovery-д байгаа ч SDK хамраагүй замууд WARN (fail биш). Сүлжээ шаарддаг тул `npm test`-д ордоггүй:
475
451
 
476
452
  ```bash
477
453
  npm run test:contract