@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 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,171 @@ 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
+
40
+ **Endpoint-ууд** (`/auth/eid` дор — eID сервер Web2App буцах замыг үргэлж `/auth/eid/callback` болгож нормчилдог):
39
41
 
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 мөрдөгдөнө.
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=<код>` |
44
48
 
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 флаг тавихгүй.
49
+ **Backend** (нэг instance-ийг модулийн түвшинд):
50
50
 
51
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
52
+ import { EidClient, createEidLogin } from "@gerege-systems/eid-mongolia-sdk";
53
+
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!] },
57
+ });
58
+
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
+ // id.sessionId, id.documentNumber, id.certificateDerB64 — дараагийн алхамд (гарын үсэг, гэрчилгээ холбох)
65
+ const user = await users.findOrCreateByEtsi(id.etsi);
66
+ const sid = await sessions.create(user.id); // ШИНЭ session (session fixation)
67
+ return {
68
+ redirect: "/dashboard",
69
+ headers: { "Set-Cookie": `app_session=${sid}; Path=/; HttpOnly; Secure; SameSite=Lax` },
70
+ };
71
+ },
72
+ // store: redisStore, // олон instance / serverless бол (анхдагч — процессын санах ой)
73
+ // rateLimit: (key) => redisLimiter.allow(key), // "start:<ip>", "push:<idCode>"; анхдагч push 3/мин нэг дугаарт
74
+ trustProxy: 1, // handle(Request)-д клиент IP: nginx/LB нэг hop бол 1 (доор «Клиент IP»)
59
75
  });
60
76
  ```
61
77
 
78
+ Next.js App Router — `app/auth/eid/[action]/route.ts`:
79
+
62
80
  ```ts
63
- import { EidOidcClient, EidOidcError } from "@gerege-systems/eid-mongolia-sdk";
81
+ import { eidLogin } from "@/lib/eid";
82
+ export const dynamic = "force-dynamic";
83
+ export const GET = (req: Request) => eidLogin.handle(req);
84
+ export const POST = (req: Request) => eidLogin.handle(req);
85
+ ```
64
86
 
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
- });
87
+ **Клиент IP** (`start:<ip>` хязгаар). Web-standard `Request`-д IP байдаггүй — `handle(req)` ашиглаж байвал аль нэгийг өг:
71
88
 
72
- const TX = "eid_oidc_tx";
73
- const cookieOpts = { httpOnly: true, secure: true, sameSite: "lax", path: "/auth/eid", maxAge: 600_000 } as const;
89
+ - `trustProxy: n` — итгэмжит reverse proxy-ийн ард: `X-Forwarded-For`-ийн **баруунаас n** дахь утга (n = таны proxy-ийн тоо),
90
+ XFF байхгүй бол `X-Real-IP`. `true` = XFF-ийн зүүн утга (proxy нь клиентийн XFF-ийг дарж бичдэг үед л). Proxy-гүй
91
+ үед бүү асаа — header-ийг клиент хуурамчаар илгээнэ.
92
+ - `getClientIp: (req) => …` — платформын IP (Bun `server.requestIP(req)?.address`, Deno `info.remoteAddr.hostname`,
93
+ Vercel `req.headers.get("x-real-ip")`). `trustProxy`-оос давуу.
74
94
 
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);
80
- });
95
+ Аль нь ч үгүй бол IP-ээр хязгаарлахгүй (эхний хүсэлтэд `onError`-оор нэг удаа анхааруулна). `handleRequest`-д `req.ip`
96
+ (Express `req.ip`, `app.set("trust proxy", …)`-тэй) шууд хэрэглэгдэнэ. Анхдагч `rateLimit` зөвхөн `push:`-ийг хязгаарладаг —
97
+ `start:<ip>`-ийг өөрийн `rateLimit`-ээр (Redis г.м.) хязгаарлана. Go: `eidlogin.Config.ClientIP` (`eidlogin.ForwardedClientIP(n)`) — ижил дүрэм.
81
98
 
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
- }
99
+ Express:
100
+
101
+ ```ts
102
+ app.use("/auth/eid", express.json(), async (req, res) => {
103
+ const r = await eidLogin.handleRequest({ method: req.method, url: req.originalUrl, headers: req.headers, body: req.body, ip: req.ip });
104
+ res.status(r.status).set(r.headers).send(r.body);
96
105
  });
97
106
  ```
98
107
 
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 хамаагүй).
101
-
102
- **Товч** — энгийн холбоос (`<a href>` эсвэл form GET). `fetch`/XHR/JS redirect биш: iOS Universal Link зөвхөн
103
- хэрэглэгчийн tap-аас эхэлсэн redirect гинжид eID аппыг нээнэ.
108
+ **Хөтөч** — Web Component (React/Vue/энгийн HTML аль нь ч):
104
109
 
105
110
  ```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>
111
+ <script type="module">import "@gerege-systems/eid-mongolia-sdk/button";</script>
112
+ <eid-login-button start-url="/auth/eid/start" status-url="/auth/eid/status" lang="mn"></eid-login-button>
113
+ <script type="module">
114
+ document.querySelector("eid-login-button").addEventListener("eid-login-success", (e) => {
115
+ // e.detail = { state: "OK", redirect, data } — preventDefault() хийвэл redirect хийхгүй
116
+ });
117
+ </script>
117
118
  ```
118
119
 
119
- Бичвэр «eID Mongolia-аар нэвтрэх», зүүн талд eID Mongolia лого (<https://developer.eidmongolia.mn/logo-96.png>-ийг
120
- татаж өөрийн хостоос үйлчил). Google/Apple-ийн товч шиг саармаг (цагаан, хүрээтэй) — таны сайтын бусад нэвтрэх
121
- товчтой зэрэгцэж болно. Жишээ: <https://demo.eidmongolia.mn>.
122
-
123
- `createAuthorization({ display: "page" })` — утсан дээр ч нэвтрэх хуудас (QR / push) харуулна (ж: таблет киоск).
120
+ эсвэл `mountEidLoginButton(el, { startUrl, statusUrl, lang: "en", theme: "auto", successUrl, onSuccess, onError })`.
121
+ Атрибут: `lang` (`mn` | `en`), `mode` (`auto` анхдагч — утас → Web2App, бусад → QR цонх; `qr`; `app`),
122
+ `theme` (`auto` анхдагч | `light` | `dark`), `success-url`.
123
+ Үйл явдал: `eid-login-success`, `eid-login-error` (`detail.error`: `USER_REFUSED…`, `VALIDATION`, `EXPIRED` …).
124
+ Гадны хамааралгүй (QR encoder дотроо), цонх нь focus trap, Esc, `aria-live` төлөвтэй.
125
+
126
+ **Дизайн, theme.** Дизайн нь eID нэвтрэх хуудсынх (лого, өнгө). Фонт нь хуудаснаас өвлөгдөнө (товч, цонх, input бүгд).
127
+ `theme="auto"` үед дараалал: хуудасны `html[data-theme="dark|light"]` → `html.dark` / `html.light` → OS
128
+ (`prefers-color-scheme`); хуудас theme солиход (MutationObserver) шууд дагана. `theme="light|dark"` нь хуудсыг давна.
129
+ Өнгө, радиусыг CSS хувьсагчаар (элемент дээр — shadow-ийн `:host`-оос давуу) дарна:
130
+
131
+ ```css
132
+ eid-login-button {
133
+ --eid-accent: #0b5cff; --eid-accent-fg: #fff; /* үндсэн товч, QR-ийн алхмын тоо (--eid-accent-subtle, --eid-accent-subtle-fg) */
134
+ --eid-bg: #fff; --eid-bg-subtle: …; --eid-bg-muted: …; /* цонх, hover */
135
+ --eid-fg: …; --eid-fg-muted: …; --eid-fg-subtle: …; /* текст */
136
+ --eid-border: …; --eid-border-strong: …; --eid-ring: …; /* хүрээ, focus */
137
+ --eid-warning-bg: …; --eid-warning-border: …; --eid-warning-fg: …; --eid-danger-fg: …;
138
+ --eid-radius: 6px; --eid-radius-lg: 8px;
139
+ }
140
+ html[data-theme="dark"] eid-login-button { --eid-accent: …; } /* dark-д өөр өнгө бол */
141
+ eid-login-button::part(button) { width: 100%; } /* part: button, backdrop, dialog */
142
+ ```
124
143
 
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-оо хэрэглэнэ.
144
+ **eID аппын доод хувилбар.** `flowType` бэхлэлт (device link v3) ба Web2App-ийн `userChallengeVerifier` нь
145
+ **iOS 2.2.3 (build 59)**, **Android 2.2.3 (versionCode 60)**-аас эхэлсэн. eID сервер 2026-10-07-оос legacy холбоосыг
146
+ бүрэн хассан (v3-гүй session-д утас `DEVICE_LINK_REQUIRED`) бөгөөд доод хувилбарыг `/mobile/app-config`-оор албадан
147
+ шинэчлүүлдэг: Android 2026-10-07-оос ≥ 70 (төхөөрөмжийн баталгаатай), 2026-10-14-ний байдлаар iOS ≥ 2.2.7,
148
+ Android ≥ 83 (`curl https://ca.eidmongolia.mn/mobile/app-config`). Иймд гарын үсэг зурж чадах бүх апп эдгээрийг дэмжинэ —
149
+ RP талд нэмэлт шалгалт хэрэггүй. Эх сурвалж: eID CA-ийн `docs/DEVICE_LINK_V3.md` §5, §7; changelog
150
+ `2026-10-02-ios-min-version-2-2-3`, `2026-10-07-android-min-70`.
129
151
 
130
152
  **Аюулгүй байдал.**
131
153
 
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`-ийг аппад хадгалахгүй).
154
+ - `sessionToken`, `sessionSecret`, `rpChallenge` серверт (store) үлдэнэ; хөтөчид зөвхөн HttpOnly, Secure, SameSite=Lax cookie
155
+ (`<sessionId>.<binding>`, store-д binding-ийн SHA-256). Status ба callback зөвхөн нэвтрэлтийг **эхлүүлсэн хөтөчид** хариулна —
156
+ өөр хөтчид нээгдсэн callback URL (verifier-тэй ч) хүлээн авагдахгүй (login CSRF).
157
+ - `start` POST нь JSON заавал (simple request биш → cross-origin preflight), `Sec-Fetch-Site: cross-site` → 403.
158
+ - `OK` нь зөвхөн `validateAuth` амжилттай үед: QR → `flowType=QR`, push → `Notification`, утас → `Web2App` + callback-ийн
159
+ `userChallengeVerifier`. Web2App session-ийг poll дуусгахгүй (`WAIT_CALLBACK`). `onSuccess` нэг session-д нэг л удаа
160
+ (store-ийн `delete` атомар байх ёстой — Redis `DEL`).
161
+ - `onSuccess`-ийн `data` хөтөчид очно — нууц бүү оруул. Иргэний бүртгэлтэй эсэхийг push алдаагаар задлахгүй (`eid_unavailable`).
162
+ - Олон instance / serverless (Vercel г.м.) дээр анхдагч санах ойн store ажиллахгүй — `EidLoginStore` (`get`/`set`/`delete`)-ийг Redis-ээр.
163
+ - `onSuccess` дуудагдахаас ӨМНӨ tx store-оос атомар устгагдана (нэг удаагийн баталгаа) — `onSuccess` дотор store-оос бүү
164
+ унш; хэрэгтэй бүхэн `identity`-д (`sessionId`, `documentNumber`, `certificateDerB64`, …).
165
+
166
+ **Redis store** (SDK нэмэлт хамааралгүй — жишээ нь `ioredis`):
167
+
168
+ ```ts
169
+ import Redis from "ioredis";
170
+ import type { EidLoginStore, EidLoginTx } from "@gerege-systems/eid-mongolia-sdk";
171
+
172
+ const redis = new Redis(process.env.REDIS_URL!);
173
+ const key = (sid: string) => `eid:login:${sid}`;
174
+
175
+ export const redisStore: EidLoginStore = {
176
+ async get(sid) {
177
+ const v = await redis.get(key(sid));
178
+ return v ? (JSON.parse(v) as EidLoginTx) : undefined;
179
+ },
180
+ async set(sid, tx, ttlMs) {
181
+ await redis.set(key(sid), JSON.stringify(tx), "PX", ttlMs);
182
+ },
183
+ async delete(sid) {
184
+ return (await redis.del(key(sid))) === 1; // DEL атомар — зэрэг хоёр status-ийн зөвхөн нэг нь onSuccess дуудна
185
+ },
186
+ };
187
+ ```
188
+
189
+ Tx-д `sessionSecret` (QR угсрах материал) байдаг — Redis-ийг сүлжээнээс хаалттай, TLS/AUTH-тай ашигла.
144
190
 
145
191
  ## 2. Нэвтрэлт (push)
146
192
 
@@ -222,6 +268,40 @@ eid.validator.validateSign(result, digest384, { hashAlgorithm: "SHA-384", expect
222
268
 
223
269
  RAW_DIGEST_SIGNATURE-д `flowType` гарын үсэгт ордоггүй (зөвхөн digest зурагдана) — энэ нь RP-API хариуны тулгалт.
224
270
 
271
+ **QR / Web2App / App2App** — `eid.sign.deviceLinkByEtsi` / `deviceLinkByDocument` (`callbackUrl`-тай бол Web2App/App2App);
272
+ холбоосыг backend-д `buildDeviceLink(deviceLinkInput(s, "QR", "mon"))` (auth/cert session-д ч мөн). `s.sign`
273
+ (`SignContext`: digest, hash, алгоритм, interactions, callback)-ийг session-тэй хамт серверт хадгал.
274
+
275
+ **`expiresAt`** (ms epoch) — sign / certificate-choice session-ий *тооцоолсон* дуусах мөч: CA session start хариунд
276
+ хугацаа буцаадаггүй (зөвхөн `sessionID`), CA-ийн анхдагч 600с (`DEFAULT_SIGN_SESSION_TTL_MS`; CA админ 60–1800с болгож
277
+ болно → `signSessionTtlMs`). Хүсэлтээс өмнөх цагаас тооцдог тул CA-ийнхаас хойно хэзээ ч биш; эцсийн үнэн нь
278
+ `GET /session` (`TIMEOUT`).
279
+
280
+ ### 3.1. Гэрчилгээг урьдчилж мэдэх (client-side PAdES / CMS)
281
+
282
+ Гэрчилгээ DTBS-д (`signingCertificateV2`) ордог тул эхлээд certificate-choice, дараа нь digest-ийг **яг тэр төхөөрөмж
283
+ рүү (`documentNumber`)** зуруулна. Олон утастай иргэнд etsi-ээр эхлүүлбэл өөр утас зурж, гарын үсэг гэрчилгээтэйгээ
284
+ таарахгүй.
285
+
286
+ ```ts
287
+ const cc = await eid.sign.certificateChoiceByEtsi("PNOMN-12345678"); // эсвэл certificateChoiceDeviceLink() (зөвхөн QR)
288
+ const signer = eid.validator.validateCertificateChoice(await eid.session.waitForResult(cc.sessionId), cc.sessionId);
289
+ const dtbs = buildDtbs(signer.certificate); // 32 байт SHA-256, стандарт base64
290
+ const s = await eid.sign.digestForSigner(signer, dtbs, ix, { fileName: "Гэрээ.pdf" }); // linked/{documentNumber}
291
+ // QR / Web2App: eid.sign.deviceLinkForSigner(signer, dtbs, ix, { callbackUrl })
292
+ const r = await eid.session.waitForResult(s.sessionId);
293
+ const sig = eid.validator.validateSign(r, s.sign!.digest, {
294
+ hashAlgorithm: s.sign!.hashAlgorithm,
295
+ expectedFlowType: "Notification",
296
+ expectedAlgorithm: s.sign!.signatureAlgorithm, // sha256WithRSAEncryption — PSS хариуг татгалзана
297
+ expectedCertificate: signer.certificate, // өөр гэрчилгээгээр зурсан бол татгалзана
298
+ });
299
+ ```
300
+
301
+ `digestForSigner` / `deviceLinkForSigner`-ийн анхдагч: `sha256WithRSAEncryption` + `SHA-256` (CA SplitKey RSA нь хүссэн
302
+ алгоритмаар, PKCS#1 v1.5; хуучин threshold-ECDSA credential нь үргэлж `ecdsa-with-SHA256` — `expectedAlgorithm` үүнийг
303
+ зөвхөн EC P-256 гэрчилгээтэй үед зөвшөөрнө). Digest-ийн урт hash-тай таарахгүй бол хүсэлт явахгүй.
304
+
225
305
  ## 3a. Байгууллагын нэрийн өмнөөс гарын үсэг (onBehalfOf)
226
306
 
227
307
  Иргэн **өөрийн PIN2 гэрчилгээгээр** зурна; тухайн мөчид байгууллагыг төлөөлөх эрхтэйг eID **өөрийн бүртгэлээр**
@@ -445,7 +525,6 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
445
525
  | `ApiError` | бусад HTTP алдаа (`.status`, `.body`, серверийн `.code`, `.retryAfterMs`, `.headers`, `.retryable` — ж: 404 `ORG_NOT_FOUND`) |
446
526
  | `ResponseTooLargeError` | хариу SDK-ийн хязгаараас их (JSON 4 MiB, PDF `pdfMaxBytes`) — `.op`, `.limit` |
447
527
  | `NetworkError` | timeout/сүлжээ (`.op` — таних тэмдэггүй зам, `.timeout`) |
448
- | `EidOidcError` | «eID-ээр нэвтрэх» (OIDC) — `.code`: `access_denied`, `login_required`, `state_mismatch`, `invalid_grant`, `invalid_id_token`, `server_error` … |
449
528
  | `DeviceLinkError` | device-link холбоос угсрах боломжгүй (`deviceLinkBase` https биш, token/secret/lang хэлбэр, төрөлд хэрэгтэй талбар дутуу) |
450
529
 
451
530
  ## mTLS (eIDAS qualified орчин)
@@ -469,9 +548,7 @@ npm test # build + node --test
469
548
  ### Гэрээний шалгалт
470
549
 
471
550
  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`-д ордоггүй:
551
+ `endpoints`-д байх ёстой. Discovery-д байгаа ч SDK хамраагүй замууд WARN (fail биш). Сүлжээ шаарддаг тул `npm test`-д ордоггүй:
475
552
 
476
553
  ```bash
477
554
  npm run test:contract
@@ -72,7 +72,38 @@ interface NotificationSession {
72
72
  rpChallenge: string;
73
73
  /** Auth session-д validator-т өгөх контекст (sign session-д undefined). */
74
74
  acsp?: AcspContext;
75
+ /** Sign session-ий хүсэлтэд илгээсэн утгууд ({@link SignContext}); auth / certificate-choice-д undefined. */
76
+ sign?: SignContext;
77
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch) — {@link sessionExpiry}; auth-д undefined. */
78
+ expiresAt?: number;
75
79
  }
80
+ /**
81
+ * Sign session-ий хүсэлтэд илгээсэн утгууд ЯГ ХЭВЭЭР: `validateSign` (digest, hashAlgorithm, signatureAlgorithm) ба
82
+ * device link (`buildDeviceLink`: digest, relyingPartyName, interactions, initialCallbackUrl)-д. RP session-тэй хамт
83
+ * серверт хадгална.
84
+ */
85
+ interface SignContext {
86
+ /** Илгээсэн digest (стандарт base64, ЯГ ХЭВЭЭР — device link-ийн authCode-д ордог). */
87
+ digest: string;
88
+ hashAlgorithm: HashAlgorithm;
89
+ signatureAlgorithm: SignatureAlgorithm;
90
+ relyingPartyName: string;
91
+ /** Илгээсэн interactions-ийн base64 МӨР. */
92
+ interactions: string;
93
+ initialCallbackUrl: string;
94
+ }
95
+ /**
96
+ * CA-ийн SIGNATURE ба CERTIFICATE_CHOICE session-ий анхдагч хүчинтэй хугацаа (мс; `EID_SIGN_SESSION_TTL_SECONDS` = 600).
97
+ * CA-ийн админ «Системийн тохиргоо»-нд 60–1800 секундэд өөрчилж болно; session start хариунд CA хугацааг буцаадаггүй
98
+ * (зөвхөн `sessionID`) тул SDK үүнийг `ClientConfig.signSessionTtlMs`-ээр тооцоолно.
99
+ */
100
+ declare const DEFAULT_SIGN_SESSION_TTL_MS = 600000;
101
+ /**
102
+ * Session start-ын хариунаас тооцоолох дуусах мөч (ms epoch): хүсэлт илгээхээс ӨМНӨХ цаг + ttl. CA ExpiresAt-ийг өөрийн
103
+ * хүлээн авсан мөчөөс тооцдог тул энэ утга CA-ийнхаас ХЭЗЭЭ Ч хойно биш (консерватив). RP UI-ийн тоолуур / «дахин
104
+ * эхлүүлэх» шийдвэрт л — эцсийн үнэн нь `GET /session` (`TIMEOUT`).
105
+ */
106
+ declare function sessionExpiry(sentAtMs: number, ttlMs?: number): number;
76
107
  /**
77
108
  * Device-link (QR/Web2App/App2App) session. Иргэн QR уншиж эсвэл товч дарж нэгдэнэ.
78
109
  * `sessionToken`, `sessionSecret`-ийг ЗӨВХӨН backend-д хадгална — браузер руу бүү гарга.
@@ -99,6 +130,14 @@ interface DeviceLinkSession {
99
130
  * ЗӨВХӨН backend-д хадгална.
100
131
  */
101
132
  receivedAt: number;
133
+ /** auth | sign | cert ({@link deviceLinkInput}-д). Хуучин хадгалсан session-д undefined = auth. */
134
+ sessionType?: "auth" | "sign" | "cert";
135
+ /** Sign session-д ({@link deviceLinkInput}, `validateSign`); бусад үед undefined. */
136
+ sign?: SignContext;
137
+ /** Certificate-choice session-ий хүсэлтэд илгээсэн нэр (cert холбоосын authCode-д). */
138
+ relyingPartyName?: string;
139
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch, {@link sessionExpiry}); auth-д undefined. */
140
+ expiresAt?: number;
102
141
  }
103
142
  /** Session poll-ийн нэгтгэсэн үр дүн (validator-т орох түүхий материал). */
104
143
  interface SessionResult {
@@ -444,4 +483,4 @@ declare class ValidationError extends EidError {
444
483
  declare class DeviceLinkError extends EidError {
445
484
  }
446
485
 
447
- export { type AcspContext as A, deviceLink as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, isMobileUserAgent as G, type HashAlgorithm as H, type Interaction as I, openOrShowQR as J, parseRetryAfter as K, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DeviceLinkError as f, type DeviceLinkInput as g, type EndResult as h, ForbiddenError as i, type HttpErrorInit as j, type NavigatorLike as k, NetworkError as l, type OnBehalfErrorCode as m, type OpenOrShowQROptions as n, type OrgResult as o, type PdfDocumentStatus as p, type PdfSigner as q, RETRYABLE_ERROR_CODES as r, type Representation as s, ResponseTooLargeError as t, type RetryableErrorCode as u, SessionFailedError as v, type SessionState as w, type SignatureAlgorithmParameters as x, type SignatureProtocol as y, type TokenCertStatus as z };
486
+ export { type AcspContext as A, type SignatureProtocol as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type TokenCertStatus as G, type HashAlgorithm as H, type Interaction as I, deviceLink as J, isMobileUserAgent as K, openOrShowQR as L, parseRetryAfter as M, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, sessionExpiry as Q, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DEFAULT_SIGN_SESSION_TTL_MS as f, DeviceLinkError as g, type DeviceLinkInput as h, type EndResult as i, ForbiddenError as j, type HttpErrorInit as k, type NavigatorLike as l, NetworkError as m, type OnBehalfErrorCode as n, type OpenOrShowQROptions as o, type OrgResult as p, type PdfDocumentStatus as q, type PdfSigner as r, RETRYABLE_ERROR_CODES as s, type Representation as t, ResponseTooLargeError as u, type RetryableErrorCode as v, SessionFailedError as w, type SessionState as x, type SignContext as y, type SignatureAlgorithmParameters as z };
@@ -72,7 +72,38 @@ interface NotificationSession {
72
72
  rpChallenge: string;
73
73
  /** Auth session-д validator-т өгөх контекст (sign session-д undefined). */
74
74
  acsp?: AcspContext;
75
+ /** Sign session-ий хүсэлтэд илгээсэн утгууд ({@link SignContext}); auth / certificate-choice-д undefined. */
76
+ sign?: SignContext;
77
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch) — {@link sessionExpiry}; auth-д undefined. */
78
+ expiresAt?: number;
75
79
  }
80
+ /**
81
+ * Sign session-ий хүсэлтэд илгээсэн утгууд ЯГ ХЭВЭЭР: `validateSign` (digest, hashAlgorithm, signatureAlgorithm) ба
82
+ * device link (`buildDeviceLink`: digest, relyingPartyName, interactions, initialCallbackUrl)-д. RP session-тэй хамт
83
+ * серверт хадгална.
84
+ */
85
+ interface SignContext {
86
+ /** Илгээсэн digest (стандарт base64, ЯГ ХЭВЭЭР — device link-ийн authCode-д ордог). */
87
+ digest: string;
88
+ hashAlgorithm: HashAlgorithm;
89
+ signatureAlgorithm: SignatureAlgorithm;
90
+ relyingPartyName: string;
91
+ /** Илгээсэн interactions-ийн base64 МӨР. */
92
+ interactions: string;
93
+ initialCallbackUrl: string;
94
+ }
95
+ /**
96
+ * CA-ийн SIGNATURE ба CERTIFICATE_CHOICE session-ий анхдагч хүчинтэй хугацаа (мс; `EID_SIGN_SESSION_TTL_SECONDS` = 600).
97
+ * CA-ийн админ «Системийн тохиргоо»-нд 60–1800 секундэд өөрчилж болно; session start хариунд CA хугацааг буцаадаггүй
98
+ * (зөвхөн `sessionID`) тул SDK үүнийг `ClientConfig.signSessionTtlMs`-ээр тооцоолно.
99
+ */
100
+ declare const DEFAULT_SIGN_SESSION_TTL_MS = 600000;
101
+ /**
102
+ * Session start-ын хариунаас тооцоолох дуусах мөч (ms epoch): хүсэлт илгээхээс ӨМНӨХ цаг + ttl. CA ExpiresAt-ийг өөрийн
103
+ * хүлээн авсан мөчөөс тооцдог тул энэ утга CA-ийнхаас ХЭЗЭЭ Ч хойно биш (консерватив). RP UI-ийн тоолуур / «дахин
104
+ * эхлүүлэх» шийдвэрт л — эцсийн үнэн нь `GET /session` (`TIMEOUT`).
105
+ */
106
+ declare function sessionExpiry(sentAtMs: number, ttlMs?: number): number;
76
107
  /**
77
108
  * Device-link (QR/Web2App/App2App) session. Иргэн QR уншиж эсвэл товч дарж нэгдэнэ.
78
109
  * `sessionToken`, `sessionSecret`-ийг ЗӨВХӨН backend-д хадгална — браузер руу бүү гарга.
@@ -99,6 +130,14 @@ interface DeviceLinkSession {
99
130
  * ЗӨВХӨН backend-д хадгална.
100
131
  */
101
132
  receivedAt: number;
133
+ /** auth | sign | cert ({@link deviceLinkInput}-д). Хуучин хадгалсан session-д undefined = auth. */
134
+ sessionType?: "auth" | "sign" | "cert";
135
+ /** Sign session-д ({@link deviceLinkInput}, `validateSign`); бусад үед undefined. */
136
+ sign?: SignContext;
137
+ /** Certificate-choice session-ий хүсэлтэд илгээсэн нэр (cert холбоосын authCode-д). */
138
+ relyingPartyName?: string;
139
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch, {@link sessionExpiry}); auth-д undefined. */
140
+ expiresAt?: number;
102
141
  }
103
142
  /** Session poll-ийн нэгтгэсэн үр дүн (validator-т орох түүхий материал). */
104
143
  interface SessionResult {
@@ -444,4 +483,4 @@ declare class ValidationError extends EidError {
444
483
  declare class DeviceLinkError extends EidError {
445
484
  }
446
485
 
447
- export { type AcspContext as A, deviceLink as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, isMobileUserAgent as G, type HashAlgorithm as H, type Interaction as I, openOrShowQR as J, parseRetryAfter as K, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DeviceLinkError as f, type DeviceLinkInput as g, type EndResult as h, ForbiddenError as i, type HttpErrorInit as j, type NavigatorLike as k, NetworkError as l, type OnBehalfErrorCode as m, type OpenOrShowQROptions as n, type OrgResult as o, type PdfDocumentStatus as p, type PdfSigner as q, RETRYABLE_ERROR_CODES as r, type Representation as s, ResponseTooLargeError as t, type RetryableErrorCode as u, SessionFailedError as v, type SessionState as w, type SignatureAlgorithmParameters as x, type SignatureProtocol as y, type TokenCertStatus as z };
486
+ export { type AcspContext as A, type SignatureProtocol as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type TokenCertStatus as G, type HashAlgorithm as H, type Interaction as I, deviceLink as J, isMobileUserAgent as K, openOrShowQR as L, parseRetryAfter as M, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, sessionExpiry as Q, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DEFAULT_SIGN_SESSION_TTL_MS as f, DeviceLinkError as g, type DeviceLinkInput as h, type EndResult as i, ForbiddenError as j, type HttpErrorInit as k, type NavigatorLike as l, NetworkError as m, type OnBehalfErrorCode as n, type OpenOrShowQROptions as o, type OrgResult as p, type PdfDocumentStatus as q, type PdfSigner as r, RETRYABLE_ERROR_CODES as s, type Representation as t, ResponseTooLargeError as u, type RetryableErrorCode as v, SessionFailedError as w, type SessionState as x, type SignContext as y, type SignatureAlgorithmParameters as z };
@@ -1 +1 @@
1
- export { f as DeviceLinkError, g as DeviceLinkInput, D as DeviceLinkSession, E as EidError, k as NavigatorLike, n as OpenOrShowQROptions, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR } from './browser-C6JHm0lR.cjs';
1
+ export { g as DeviceLinkError, h as DeviceLinkInput, D as DeviceLinkSession, E as EidError, l as NavigatorLike, o as OpenOrShowQROptions, J as deviceLink, K as isMobileUserAgent, L as openOrShowQR } from './browser-CuMH5IVG.cjs';
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { f as DeviceLinkError, g as DeviceLinkInput, D as DeviceLinkSession, E as EidError, k as NavigatorLike, n as OpenOrShowQROptions, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR } from './browser-C6JHm0lR.js';
1
+ export { g as DeviceLinkError, h as DeviceLinkInput, D as DeviceLinkSession, E as EidError, l as NavigatorLike, o as OpenOrShowQROptions, J as deviceLink, K as isMobileUserAgent, L as openOrShowQR } from './browser-CuMH5IVG.js';