@gerege-systems/eid-mongolia-sdk 0.7.0 → 0.8.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
@@ -19,7 +19,94 @@ Node.js ≥ 22 (global `fetch`, `node:crypto`).
19
19
 
20
20
  `POST https://ca.eidmongolia.mn/v3/rp-applications` (эсвэл [developer.eidmongolia.mn](https://developer.eidmongolia.mn)) —
21
21
  оператор баталсны дараа **relyingPartyUUID** ба **API secret** (`rp_sk_…`) олгогдоно; secret **зөвхөн нэг удаа**
22
- харагдана. Дэлгэрэнгүй: <https://developer.eidmongolia.mn/#burtgel>.
22
+ харагдана. Дэлгэрэнгүй: <https://developer.eidmongolia.mn/#registration>.
23
+
24
+ ## 1a. «eID-ээр нэвтрэх» (OpenID Connect)
25
+
26
+ Хамгийн хялбар нэвтрэлт: таны вэбэд нэг товч, нэвтрэх хуудсыг (QR · утсан дээрх аппаар нээх · регистрээр
27
+ мэдэгдэл) eID сервер өөрөө харуулж, гарын үсэг ба гэрчилгээний гинжийг шалгаад танд стандарт OpenID Connect
28
+ (Authorization Code + PKCE `S256`) `id_token` өгнө — Google/Apple нэвтрэлттэй ижил. Утасны хөтчөөс дарахад
29
+ хуудасгүйгээр шууд eID апп нээгдэнэ. Гэрээ: <https://developer.eidmongolia.mn/#oidc>.
30
+
31
+ **Бүртгэл.** `client_id` = таны RP UUID (`relyingPartyUUID`), `client_secret` = RP API secret (`rp_sk_…`).
32
+ RP-д `AUTH` эрх, OIDC асаалттай, `redirect_uri` (https, ≤ 10) бүртгэлтэй байна — authorize/token-ий `redirect_uri`
33
+ бүртгэлтэйтэй **тэмдэгт бүрээрээ** таарах ёстой. Иргэний ETSI дугаар (`pnomn` claim) хэрэгтэй бол `PNOMN` эрх
34
+ хүсэж, `scope: "openid pnomn"`. Token endpoint-д RP-ийн IP allowlist мөрдөгдөнө; `requireMtls` RP OIDC ашиглахгүй.
35
+
36
+ ```ts
37
+ import { EidOidcClient, EidOidcError } from "@gerege-systems/eid-mongolia-sdk";
38
+
39
+ const oidc = new EidOidcClient({
40
+ clientId: process.env.EID_RP_UUID!,
41
+ clientSecret: process.env.EID_SECRET!, // rp_sk_… — зөвхөн backend
42
+ redirectUri: "https://bank.example.mn/auth/eid/callback",
43
+ // issuer анхдагч https://login.eidmongolia.mn, scope анхдагч "openid"
44
+ });
45
+
46
+ const TX = "eid_oidc_tx";
47
+ const cookieOpts = { httpOnly: true, secure: true, sameSite: "lax", path: "/auth/eid", maxAge: 600_000 } as const;
48
+
49
+ // GET /auth/eid/login — товчны href
50
+ app.get("/auth/eid/login", async (_req, res) => {
51
+ const a = await oidc.createAuthorization(); // { url, state, nonce, codeVerifier }
52
+ res.cookie(TX, JSON.stringify({ state: a.state, nonce: a.nonce, codeVerifier: a.codeVerifier }), cookieOpts);
53
+ res.set("Cache-Control", "no-store").redirect(302, a.url);
54
+ });
55
+
56
+ // GET /auth/eid/callback?code&state (эсвэл ?error) — бүртгэлтэй redirect_uri
57
+ app.get("/auth/eid/callback", async (req, res) => {
58
+ const tx = JSON.parse(req.cookies[TX] ?? "null");
59
+ res.clearCookie(TX, { path: "/auth/eid" });
60
+ if (!tx) return res.redirect(303, "/login?error=state");
61
+ try {
62
+ const { claims } = await oidc.handleCallback(req.originalUrl, tx); // state → token → id_token (ES256)
63
+ const user = await users.findOrCreateBySub(claims.sub); // pairwise sub-аар таньна (нэрээр биш)
64
+ await startSession(res, user); // таны session-ийг шинээр үүсгэ (session fixation)
65
+ res.redirect(303, "/");
66
+ } catch (e) {
67
+ const code = e instanceof EidOidcError ? e.code : "server_error"; // access_denied → «дахин оролдоно уу»
68
+ res.redirect(303, `/login?error=${code}`);
69
+ }
70
+ });
71
+ ```
72
+
73
+ Next.js App Router-д ижил: `app/auth/eid/login/route.ts` → `NextResponse.redirect(a.url, 302)` + `res.cookies.set(…)`;
74
+ callback → `oidc.handleCallback(request.nextUrl.searchParams, tx)` (зөвхөн query уншина — прокси ард host хамаагүй).
75
+
76
+ **Товч** — энгийн холбоос (`<a href>` эсвэл form GET). `fetch`/XHR/JS redirect биш: iOS Universal Link зөвхөн
77
+ хэрэглэгчийн tap-аас эхэлсэн redirect гинжид eID аппыг нээнэ.
78
+
79
+ ```html
80
+ <a class="eid-login" href="/auth/eid/login">eID-ээр нэвтрэх</a>
81
+ <style>
82
+ .eid-login { display: inline-flex; align-items: center; justify-content: center; min-height: 44px;
83
+ padding: 0 1.25rem; border-radius: 8px; background: #0b3d91; color: #fff; font: 600 1rem/1 system-ui, sans-serif;
84
+ text-decoration: none; }
85
+ .eid-login:focus-visible { outline: 2px solid #0b3d91; outline-offset: 2px; }
86
+ </style>
87
+ ```
88
+
89
+ `createAuthorization({ display: "page" })` — утсан дээр ч нэвтрэх хуудас (QR / push) харуулна (ж: таблет киоск).
90
+
91
+ **`id_token` claims** (`EidIdTokenClaims`): `sub` (pairwise — RP тус бүрд өөр, иргэнд тогтмол), `acr`
92
+ (`urn:eidmongolia:loa:qualified` | `…:advanced`), `auth_time`, `given_name` / `family_name` (латин, гэрчилгээнээс),
93
+ `given_name_mn` / `family_name_mn` (кирилл, байвал), `pnomn` (`pnomn` scope + эрхтэй үед). `id_token` 5 минут
94
+ хүчинтэй; access_token / refresh_token / userinfo байхгүй — нэвтрэлтийн дараа өөрийн session-оо хэрэглэнэ.
95
+
96
+ **Аюулгүй байдал.**
97
+
98
+ - `state`, `nonce`, `codeVerifier`-ийг **хөтчийн session-тэй** (httpOnly, Secure, SameSite=Lax cookie эсвэл server
99
+ session) холбож хадгална; `handleCallback` state-ийг timing-safe тулгаж, таарахгүй бол code-ийг солихгүй
100
+ (`state_mismatch`). Үүнгүйгээр «өөрийн бүртгэлд хохирогчийн eID-ийг холбуулах» (login CSRF) нээлттэй.
101
+ - `handleCallback` / `verifyIdToken` нь ES256 гарын үсэг (JWKS `kid`, cache 1 цаг, танихгүй `kid` → нэг удаа дахин
102
+ татна), `iss`, `aud` (= `clientId`; олон aud бол `azp`), `exp` / `iat` (±60 сек), `nonce`-ийг шалгана.
103
+ `EidOidcError` (`invalid_id_token`) гарвал **итгэхгүй**.
104
+ - Code нэг удаагийн, 60 секунд — callback-ийг дахин ачаалбал `invalid_grant`; хэрэглэгчийг дахин нэвтрүүлнэ.
105
+ - `error_description` (`EidOidcError.description`)-ийг логт бичиж болно, хэрэглэгчид шууд харуулахгүй.
106
+ - **Native апп (RFC 8252):** authorize-ийг системийн хөтчөөр нээнэ — iOS `ASWebAuthenticationSession`
107
+ (`prefersEphemeralWebBrowserSession = false`), Android Custom Tabs. **WebView хориотой.** `redirect_uri` = таны
108
+ https Universal Link / App Link (custom scheme дэмжихгүй). `state` / `nonce` / `codeVerifier` апп-ын санах ойд;
109
+ code-ийг таны backend `client_secret`-ээр солино (`client_secret`-ийг аппад хадгалахгүй).
23
110
 
24
111
  ## 2. Нэвтрэлт (push)
25
112
 
@@ -222,6 +309,7 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
222
309
  | `ValidationError` | cert chain/signature/level шалгалт бүтэлгүйтсэн |
223
310
  | `ApiError` | бусад HTTP алдаа (`.status`, `.body`, серверийн `.code` — ж: 404 `ORG_NOT_FOUND`) |
224
311
  | `NetworkError` | timeout/сүлжээ |
312
+ | `EidOidcError` | «eID-ээр нэвтрэх» (OIDC) — `.code`: `access_denied`, `login_required`, `state_mismatch`, `invalid_grant`, `invalid_id_token`, `server_error` … |
225
313
  | `DeviceLinkError` | device-link холбоос угсрах боломжгүй (`deviceLinkBase` https биш, token/secret/lang хэлбэр, төрөлд хэрэгтэй талбар дутуу) |
226
314
 
227
315
  ## mTLS (eIDAS qualified орчин)
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-Db82DlYN.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-Db82DlYN.cjs';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -457,4 +457,141 @@ interface QrDeviceLinkTickerOptions {
457
457
  */
458
458
  declare function qrDeviceLinkTicker(input: Omit<BuildDeviceLinkInput, "deviceLinkType" | "now">, onLink: (link: string) => void, opts?: QrDeviceLinkTickerOptions): () => void;
459
459
 
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 };
460
+ /** eID-ийн OIDC issuer (төгсгөлд `/` байхгүй). */
461
+ declare const EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
462
+ /** `acr` утгууд — иргэний гэрчилгээний бодит түвшин. */
463
+ declare const EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
464
+ declare const EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
465
+ /**
466
+ * OIDC алдааны код. Authorize redirect (RFC 6749 §4.1.2.1) ба token endpoint (§5.2)-ийн стандарт кодууд, мөн
467
+ * SDK-ийн өөрийн шалгалтын кодууд:
468
+ * - `state_mismatch` — callback-ийн `state` хадгалсантай таарсангүй (CSRF / өөр tab / cookie дууссан) → дахин эхлүүл;
469
+ * - `invalid_callback` — `code` алга / хэт урт, эсвэл `iss` параметр өөр;
470
+ * - `invalid_id_token` — id_token-ий гарын үсэг / iss / aud / exp / iat / nonce шалгалт унасан (ИТГЭХГҮЙ);
471
+ * - `discovery_failed` — discovery/JWKS буруу эсвэл issuer таарахгүй.
472
+ *
473
+ * `access_denied` = иргэн татгалзсан, нэвтрэлтийн хугацаа дууссан, эсвэл гэрчилгээний шалгалт унасан → «дахин оролд».
474
+ */
475
+ 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";
476
+ /**
477
+ * OIDC урсгалын алдаа. `code`-оор салгана. `description` нь серверийн `error_description` — логт л,
478
+ * хэрэглэгчид шууд харуулахгүй.
479
+ */
480
+ declare class EidOidcError extends EidError {
481
+ readonly code: EidOidcErrorCode;
482
+ readonly description?: string | undefined;
483
+ constructor(code: EidOidcErrorCode, message: string, description?: string | undefined);
484
+ }
485
+ interface EidOidcClientConfig {
486
+ /** RP UUID (`relyingPartyUUID`). */
487
+ clientId: string;
488
+ /** RP API secret (`rp_sk_…`) — зөвхөн backend-д. */
489
+ clientSecret: string;
490
+ /** Бүртгэлтэй redirect URI — тэмдэгт бүрээрээ таарах ёстой. */
491
+ redirectUri: string;
492
+ /** Анхдагч {@link EID_OIDC_ISSUER}. */
493
+ issuer?: string;
494
+ /** Анхдагч `"openid"`. Иргэний ETSI дугаар хэрэгтэй бол `"openid pnomn"` (RP-д `PNOMN` эрх). */
495
+ scope?: string;
496
+ /** HTTP timeout (мс), анхдагч 10000. */
497
+ timeoutMs?: number;
498
+ /** fetch орлуулагч (тест). */
499
+ fetchImpl?: typeof fetch;
500
+ }
501
+ interface EidOidcDiscovery {
502
+ issuer: string;
503
+ authorization_endpoint: string;
504
+ token_endpoint: string;
505
+ jwks_uri: string;
506
+ [k: string]: unknown;
507
+ }
508
+ interface CreateAuthorizationOptions {
509
+ /** Энэ хүсэлтэд scope-ыг дарна. */
510
+ scope?: string;
511
+ /**
512
+ * `touch` (анхдагч) — утасны хөтчөөс шууд eID апп; `page` — үргэлж нэвтрэх хуудас (QR / push), ж: таблет киоск.
513
+ */
514
+ display?: "page" | "touch";
515
+ }
516
+ /** RP-ийн хадгалах ёстой гүйлгээ — httpOnly cookie / server session-д (браузерийн JS-д биш). */
517
+ interface EidAuthorization {
518
+ /** Хөтчийг энэ рүү 302. */
519
+ url: string;
520
+ state: string;
521
+ nonce: string;
522
+ codeVerifier: string;
523
+ }
524
+ /** createAuthorization-аас хадгалсан утгууд. */
525
+ interface EidCallbackChecks {
526
+ state: string;
527
+ nonce: string;
528
+ codeVerifier: string;
529
+ }
530
+ /** id_token-ий claim-ууд (`docs/OIDC.md` §4). */
531
+ interface EidIdTokenClaims {
532
+ iss: string;
533
+ aud: string | string[];
534
+ azp?: string;
535
+ /** Pairwise — RP тус бүрд өөр, иргэнд тогтмол. Хэрэглэгчийг ҮҮГЭЭР таньна. */
536
+ sub: string;
537
+ exp: number;
538
+ iat: number;
539
+ nonce: string;
540
+ /** Иргэн баталгаажуулсан мөч (Unix сек). */
541
+ auth_time: number;
542
+ /** {@link EID_ACR_QUALIFIED} эсвэл {@link EID_ACR_ADVANCED}. */
543
+ acr: string;
544
+ /** Гэрчилгээний GIVENNAME — латин (ICAO, том үсэг). */
545
+ given_name: string;
546
+ /** Гэрчилгээний SURNAME (эцэг/эхийн нэр) — латин. */
547
+ family_name: string;
548
+ /** Нэр — кирилл (бүртгэлд байхгүй бол алга). */
549
+ given_name_mn?: string;
550
+ /** Эцэг/эхийн нэр — кирилл (бүртгэлд байхгүй бол алга). */
551
+ family_name_mn?: string;
552
+ /** `PNOMN-<иргэний дугаар>` — зөвхөн `pnomn` scope + `PNOMN` эрхтэй үед. */
553
+ pnomn?: string;
554
+ [k: string]: unknown;
555
+ }
556
+ interface EidCallbackResult {
557
+ claims: EidIdTokenClaims;
558
+ idToken: string;
559
+ }
560
+ declare class EidOidcClient {
561
+ private readonly cfg;
562
+ readonly issuer: string;
563
+ private readonly scope;
564
+ private discovery?;
565
+ private jwks?;
566
+ constructor(cfg: EidOidcClientConfig);
567
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
568
+ discover(): Promise<EidOidcDiscovery>;
569
+ private fetchDiscovery;
570
+ /**
571
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
572
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
573
+ */
574
+ createAuthorization(opts?: CreateAuthorizationOptions): Promise<EidAuthorization>;
575
+ /**
576
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
577
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
578
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
579
+ */
580
+ handleCallback(input: string | URL | URLSearchParams, checks: EidCallbackChecks): Promise<EidCallbackResult>;
581
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
582
+ private exchangeCode;
583
+ /**
584
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
585
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
586
+ */
587
+ verifyIdToken(idToken: string, opts: {
588
+ nonce: string;
589
+ }): Promise<EidIdTokenClaims>;
590
+ private keys;
591
+ private getJson;
592
+ private fetch;
593
+ }
594
+ /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
595
+ declare function pkceChallenge(codeVerifier: string): Promise<string>;
596
+
597
+ 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-Db82DlYN.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-Db82DlYN.js';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -457,4 +457,141 @@ interface QrDeviceLinkTickerOptions {
457
457
  */
458
458
  declare function qrDeviceLinkTicker(input: Omit<BuildDeviceLinkInput, "deviceLinkType" | "now">, onLink: (link: string) => void, opts?: QrDeviceLinkTickerOptions): () => void;
459
459
 
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 };
460
+ /** eID-ийн OIDC issuer (төгсгөлд `/` байхгүй). */
461
+ declare const EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
462
+ /** `acr` утгууд — иргэний гэрчилгээний бодит түвшин. */
463
+ declare const EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
464
+ declare const EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
465
+ /**
466
+ * OIDC алдааны код. Authorize redirect (RFC 6749 §4.1.2.1) ба token endpoint (§5.2)-ийн стандарт кодууд, мөн
467
+ * SDK-ийн өөрийн шалгалтын кодууд:
468
+ * - `state_mismatch` — callback-ийн `state` хадгалсантай таарсангүй (CSRF / өөр tab / cookie дууссан) → дахин эхлүүл;
469
+ * - `invalid_callback` — `code` алга / хэт урт, эсвэл `iss` параметр өөр;
470
+ * - `invalid_id_token` — id_token-ий гарын үсэг / iss / aud / exp / iat / nonce шалгалт унасан (ИТГЭХГҮЙ);
471
+ * - `discovery_failed` — discovery/JWKS буруу эсвэл issuer таарахгүй.
472
+ *
473
+ * `access_denied` = иргэн татгалзсан, нэвтрэлтийн хугацаа дууссан, эсвэл гэрчилгээний шалгалт унасан → «дахин оролд».
474
+ */
475
+ 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";
476
+ /**
477
+ * OIDC урсгалын алдаа. `code`-оор салгана. `description` нь серверийн `error_description` — логт л,
478
+ * хэрэглэгчид шууд харуулахгүй.
479
+ */
480
+ declare class EidOidcError extends EidError {
481
+ readonly code: EidOidcErrorCode;
482
+ readonly description?: string | undefined;
483
+ constructor(code: EidOidcErrorCode, message: string, description?: string | undefined);
484
+ }
485
+ interface EidOidcClientConfig {
486
+ /** RP UUID (`relyingPartyUUID`). */
487
+ clientId: string;
488
+ /** RP API secret (`rp_sk_…`) — зөвхөн backend-д. */
489
+ clientSecret: string;
490
+ /** Бүртгэлтэй redirect URI — тэмдэгт бүрээрээ таарах ёстой. */
491
+ redirectUri: string;
492
+ /** Анхдагч {@link EID_OIDC_ISSUER}. */
493
+ issuer?: string;
494
+ /** Анхдагч `"openid"`. Иргэний ETSI дугаар хэрэгтэй бол `"openid pnomn"` (RP-д `PNOMN` эрх). */
495
+ scope?: string;
496
+ /** HTTP timeout (мс), анхдагч 10000. */
497
+ timeoutMs?: number;
498
+ /** fetch орлуулагч (тест). */
499
+ fetchImpl?: typeof fetch;
500
+ }
501
+ interface EidOidcDiscovery {
502
+ issuer: string;
503
+ authorization_endpoint: string;
504
+ token_endpoint: string;
505
+ jwks_uri: string;
506
+ [k: string]: unknown;
507
+ }
508
+ interface CreateAuthorizationOptions {
509
+ /** Энэ хүсэлтэд scope-ыг дарна. */
510
+ scope?: string;
511
+ /**
512
+ * `touch` (анхдагч) — утасны хөтчөөс шууд eID апп; `page` — үргэлж нэвтрэх хуудас (QR / push), ж: таблет киоск.
513
+ */
514
+ display?: "page" | "touch";
515
+ }
516
+ /** RP-ийн хадгалах ёстой гүйлгээ — httpOnly cookie / server session-д (браузерийн JS-д биш). */
517
+ interface EidAuthorization {
518
+ /** Хөтчийг энэ рүү 302. */
519
+ url: string;
520
+ state: string;
521
+ nonce: string;
522
+ codeVerifier: string;
523
+ }
524
+ /** createAuthorization-аас хадгалсан утгууд. */
525
+ interface EidCallbackChecks {
526
+ state: string;
527
+ nonce: string;
528
+ codeVerifier: string;
529
+ }
530
+ /** id_token-ий claim-ууд (`docs/OIDC.md` §4). */
531
+ interface EidIdTokenClaims {
532
+ iss: string;
533
+ aud: string | string[];
534
+ azp?: string;
535
+ /** Pairwise — RP тус бүрд өөр, иргэнд тогтмол. Хэрэглэгчийг ҮҮГЭЭР таньна. */
536
+ sub: string;
537
+ exp: number;
538
+ iat: number;
539
+ nonce: string;
540
+ /** Иргэн баталгаажуулсан мөч (Unix сек). */
541
+ auth_time: number;
542
+ /** {@link EID_ACR_QUALIFIED} эсвэл {@link EID_ACR_ADVANCED}. */
543
+ acr: string;
544
+ /** Гэрчилгээний GIVENNAME — латин (ICAO, том үсэг). */
545
+ given_name: string;
546
+ /** Гэрчилгээний SURNAME (эцэг/эхийн нэр) — латин. */
547
+ family_name: string;
548
+ /** Нэр — кирилл (бүртгэлд байхгүй бол алга). */
549
+ given_name_mn?: string;
550
+ /** Эцэг/эхийн нэр — кирилл (бүртгэлд байхгүй бол алга). */
551
+ family_name_mn?: string;
552
+ /** `PNOMN-<иргэний дугаар>` — зөвхөн `pnomn` scope + `PNOMN` эрхтэй үед. */
553
+ pnomn?: string;
554
+ [k: string]: unknown;
555
+ }
556
+ interface EidCallbackResult {
557
+ claims: EidIdTokenClaims;
558
+ idToken: string;
559
+ }
560
+ declare class EidOidcClient {
561
+ private readonly cfg;
562
+ readonly issuer: string;
563
+ private readonly scope;
564
+ private discovery?;
565
+ private jwks?;
566
+ constructor(cfg: EidOidcClientConfig);
567
+ /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
568
+ discover(): Promise<EidOidcDiscovery>;
569
+ private fetchDiscovery;
570
+ /**
571
+ * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
572
+ * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
573
+ */
574
+ createAuthorization(opts?: CreateAuthorizationOptions): Promise<EidAuthorization>;
575
+ /**
576
+ * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
577
+ * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
578
+ * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
579
+ */
580
+ handleCallback(input: string | URL | URLSearchParams, checks: EidCallbackChecks): Promise<EidCallbackResult>;
581
+ /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
582
+ private exchangeCode;
583
+ /**
584
+ * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
585
+ * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
586
+ */
587
+ verifyIdToken(idToken: string, opts: {
588
+ nonce: string;
589
+ }): Promise<EidIdTokenClaims>;
590
+ private keys;
591
+ private getJson;
592
+ private fetch;
593
+ }
594
+ /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
595
+ declare function pkceChallenge(codeVerifier: string): Promise<string>;
596
+
597
+ 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.0",
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": [