@gerege-systems/eid-mongolia-sdk 0.6.1 → 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/dist/index.js CHANGED
@@ -5,12 +5,13 @@ import {
5
5
  EidError,
6
6
  ForbiddenError,
7
7
  NetworkError,
8
+ ON_BEHALF_ERROR_CODES,
8
9
  SessionFailedError,
9
10
  ValidationError,
10
11
  deviceLink,
11
12
  isMobileUserAgent,
12
13
  openOrShowQR
13
- } from "./chunk-Q5TQMCJC.js";
14
+ } from "./chunk-NXDKQZIW.js";
14
15
 
15
16
  // src/http.ts
16
17
  var DEFAULT_TIMEOUT_MS = 15e3;
@@ -20,21 +21,35 @@ var Http = class {
20
21
  }
21
22
  cfg;
22
23
  post(path, body, timeoutMs) {
23
- return this.request("POST", path, body, timeoutMs);
24
+ return this.json("POST", path, JSON.stringify(body), "application/json", timeoutMs);
24
25
  }
25
26
  get(path, timeoutMs) {
26
- return this.request("GET", path, void 0, timeoutMs);
27
+ return this.json("GET", path, void 0, void 0, timeoutMs);
27
28
  }
28
- async request(method, path, body, timeoutMs) {
29
+ /** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
30
+ postMultipart(path, form, timeoutMs) {
31
+ return this.json("POST", path, form, void 0, timeoutMs);
32
+ }
33
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
34
+ async getBytes(path, timeoutMs) {
35
+ const res = await this.send("GET", path, void 0, void 0, timeoutMs);
36
+ return new Uint8Array(await res.arrayBuffer());
37
+ }
38
+ async json(method, path, body, contentType, timeoutMs) {
39
+ const res = await this.send(method, path, body, contentType, timeoutMs);
40
+ const text = await res.text();
41
+ return text ? JSON.parse(text) : {};
42
+ }
43
+ async send(method, path, body, contentType, timeoutMs) {
29
44
  const f = this.cfg.fetchImpl ?? fetch;
30
45
  const url = this.cfg.baseUrl + path;
31
46
  const init = {
32
47
  method,
33
48
  headers: {
34
49
  Authorization: `Bearer ${this.cfg.apiSecret}`,
35
- ...body !== void 0 ? { "Content-Type": "application/json" } : {}
50
+ ...contentType ? { "Content-Type": contentType } : {}
36
51
  },
37
- body: body !== void 0 ? JSON.stringify(body) : void 0,
52
+ body,
38
53
  signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
39
54
  };
40
55
  if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
@@ -48,13 +63,32 @@ var Http = class {
48
63
  }
49
64
  throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${method} ${path}): ${err.message}`);
50
65
  }
66
+ if (res.status < 300) return res;
51
67
  const text = await res.text();
68
+ const { error, code } = errorBody(text);
52
69
  if (res.status === 401) throw new AuthenticationError("API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401)");
53
- if (res.status === 403) throw new ForbiddenError("IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)");
54
- if (res.status >= 300) throw new ApiError(res.status, text);
55
- return text ? JSON.parse(text) : {};
70
+ if (res.status === 403) {
71
+ throw new ForbiddenError(
72
+ error ? `RP-API 403${code ? ` ${code}` : ""}: ${error}` : "IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)",
73
+ code
74
+ );
75
+ }
76
+ throw new ApiError(res.status, text, code);
56
77
  }
57
78
  };
79
+ function errorBody(text) {
80
+ try {
81
+ const v = JSON.parse(text);
82
+ if (!v || typeof v !== "object") return {};
83
+ const o = v;
84
+ return {
85
+ error: typeof o.error === "string" && o.error ? o.error : void 0,
86
+ code: typeof o.code === "string" && o.code ? o.code : void 0
87
+ };
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
58
92
 
59
93
  // src/crypto.ts
60
94
  import { createHash, randomBytes } from "crypto";
@@ -266,9 +300,32 @@ function parseSessionResult(raw) {
266
300
  initialCallbackUrl: typeof sig.initialCallbackUrl === "string" ? sig.initialCallbackUrl : null,
267
301
  signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
268
302
  interactionTypeUsed: str(r.interactionTypeUsed),
269
- onBehalfOf: parseOrg(r.onBehalfOf)
303
+ onBehalfOf: parseOrg(r.onBehalfOf),
304
+ pdf: parsePdf(r.pdf)
270
305
  };
271
306
  }
307
+ function parsePdf(raw) {
308
+ const p = asRecord(raw);
309
+ const docID = str(p.docID);
310
+ if (!docID) return null;
311
+ const out = { docID, documentStatus: String(p.documentStatus ?? "") };
312
+ if (p.errorCode != null) out.errorCode = String(p.errorCode);
313
+ if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
314
+ if (p.size != null) out.size = Number(p.size);
315
+ if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
316
+ if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
317
+ const sg = asRecord(p.signer);
318
+ if (sg.etsi != null) {
319
+ out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
320
+ if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
321
+ if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
322
+ }
323
+ const v = asRecord(p.validation);
324
+ if (v.indication != null) {
325
+ out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
326
+ }
327
+ return out;
328
+ }
272
329
  function parseSigParams(raw) {
273
330
  const p = asRecord(raw);
274
331
  const hash = str(p.hashAlgorithm);
@@ -323,6 +380,76 @@ var SessionApi = class {
323
380
  }
324
381
  };
325
382
 
383
+ // src/pdf.ts
384
+ function rec3(v) {
385
+ return v && typeof v === "object" ? v : {};
386
+ }
387
+ var PdfApi = class {
388
+ constructor(http, creds, defaults) {
389
+ this.http = http;
390
+ this.creds = creds;
391
+ this.defaults = defaults;
392
+ }
393
+ http;
394
+ creds;
395
+ defaults;
396
+ /** POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. */
397
+ async prepare(input) {
398
+ const request = {
399
+ relyingPartyUUID: this.creds.rpUUID,
400
+ relyingPartyName: this.creds.rpName,
401
+ certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
402
+ signer: input.signer,
403
+ flow: input.flow ?? "notification",
404
+ fileName: input.fileName
405
+ };
406
+ if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
407
+ if (input.docID?.trim()) request.docID = input.docID.trim();
408
+ if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
409
+ const form = new FormData();
410
+ form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
411
+ form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
412
+ const r = rec3(await this.http.postMultipart("/pdf/sign/prepare", form, 6e4));
413
+ const receivedAt = Date.now();
414
+ const vc = rec3(r.vc).value;
415
+ const dl = rec3(r.deviceLink);
416
+ return {
417
+ sessionId: String(r.sessionID),
418
+ docId: String(r.docID),
419
+ vc: vc != null ? String(vc) : null,
420
+ deviceLink: dl.sessionToken ? {
421
+ sessionToken: String(dl.sessionToken),
422
+ sessionSecret: String(dl.sessionSecret),
423
+ deviceLinkBase: String(dl.deviceLinkBase),
424
+ digest: String(dl.digest),
425
+ interactions: String(dl.interactions)
426
+ } : null,
427
+ receivedAt
428
+ };
429
+ }
430
+ /**
431
+ * GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
432
+ * session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
433
+ */
434
+ document(sessionId) {
435
+ return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
436
+ }
437
+ };
438
+
439
+ // src/organization.ts
440
+ var OrganizationApi = class {
441
+ constructor(http) {
442
+ this.http = http;
443
+ }
444
+ http;
445
+ /** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
446
+ async getRepresentations(personEtsi) {
447
+ const raw = await this.http.get(`/organization/representations/etsi/${encodeURIComponent(personEtsi)}`);
448
+ const list = Array.isArray(raw.representations) ? raw.representations : [];
449
+ return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
450
+ }
451
+ };
452
+
326
453
  // src/validator.ts
327
454
  import { X509Certificate, constants, createHash as createHash2, publicDecrypt, verify } from "crypto";
328
455
  var ResponseValidator = class {
@@ -677,6 +804,8 @@ function resolveBaseUrl(baseUrl) {
677
804
  var EidClient = class {
678
805
  auth;
679
806
  sign;
807
+ pdf;
808
+ organization;
680
809
  session;
681
810
  validator;
682
811
  constructor(cfg) {
@@ -692,6 +821,8 @@ var EidClient = class {
692
821
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
693
822
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
694
823
  this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
824
+ this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level });
825
+ this.organization = new OrganizationApi(http);
695
826
  this.session = new SessionApi(http);
696
827
  this.validator = new ResponseValidator(cfg.trust ?? {});
697
828
  }
@@ -766,17 +897,302 @@ function qrDeviceLinkTicker(input, onLink, opts) {
766
897
  t.unref?.();
767
898
  return () => clearInterval(t);
768
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
+ }
769
1177
  export {
770
1178
  ApiError,
771
1179
  AuthApi,
772
1180
  AuthenticationError,
773
1181
  DEFAULT_BASE_URL,
774
1182
  DeviceLinkError,
1183
+ EID_ACR_ADVANCED,
1184
+ EID_ACR_QUALIFIED,
1185
+ EID_OIDC_ISSUER,
775
1186
  EidClient,
776
1187
  EidError,
1188
+ EidOidcClient,
1189
+ EidOidcError,
777
1190
  ForbiddenError,
778
1191
  Http,
779
1192
  NetworkError,
1193
+ ON_BEHALF_ERROR_CODES,
1194
+ OrganizationApi,
1195
+ PdfApi,
780
1196
  ResponseValidator,
781
1197
  SessionApi,
782
1198
  SessionFailedError,
@@ -790,6 +1206,7 @@ export {
790
1206
  isMobileUserAgent,
791
1207
  openOrShowQR,
792
1208
  parseSessionResult,
1209
+ pkceChallenge,
793
1210
  qrDeviceLinkTicker,
794
1211
  randomChallenge,
795
1212
  resolveBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.6.1",
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": [