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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -167,13 +167,6 @@ function routePath(route, ...args) {
167
167
  if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
168
168
  return out;
169
169
  }
170
- var OIDC_EXPECT = {
171
- defaultScope: "openid",
172
- pkceMethod: "S256",
173
- idTokenAlg: "ES256",
174
- /** `Authorization: Basic` (RFC 6749 §2.3.1). */
175
- tokenEndpointAuthMethod: "client_secret_basic"
176
- };
177
170
 
178
171
  // src/crypto.ts
179
172
  import { createHash, randomBytes } from "crypto";
@@ -1590,6 +1583,8 @@ var EidClient = class {
1590
1583
  token;
1591
1584
  session;
1592
1585
  validator;
1586
+ /** `credentials.rpName` — иргэний утсанд харагдах RP нэр. */
1587
+ rpName;
1593
1588
  constructor(cfg) {
1594
1589
  if (!cfg.credentials?.apiSecret) throw new Error("EidClient: credentials.apiSecret \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
1595
1590
  const base = resolveBaseUrl(cfg.baseUrl);
@@ -1614,6 +1609,7 @@ var EidClient = class {
1614
1609
  this.token = new TokenApi(http, cfg.credentials);
1615
1610
  this.session = new SessionApi(http);
1616
1611
  this.validator = new ResponseValidator(cfg.trust ?? {});
1612
+ this.rpName = cfg.credentials.rpName;
1617
1613
  }
1618
1614
  };
1619
1615
 
@@ -1687,281 +1683,371 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1687
1683
  return () => clearInterval(t);
1688
1684
  }
1689
1685
 
1690
- // src/oidc.ts
1691
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1692
- var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1693
- var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1694
- var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
1695
- var CLOCK_SKEW_S = 60;
1696
- var CACHE_MS = 60 * 60 * 1e3;
1697
- var DEFAULT_TIMEOUT_MS2 = 1e4;
1698
- var KNOWN_CODES = /* @__PURE__ */ new Set([
1699
- "access_denied",
1700
- "login_required",
1701
- "invalid_request",
1702
- "invalid_scope",
1703
- "unauthorized_client",
1704
- "unsupported_response_type",
1705
- "request_not_supported",
1706
- "temporarily_unavailable",
1707
- "server_error",
1708
- "invalid_client",
1709
- "invalid_grant",
1710
- "unsupported_grant_type"
1711
- ]);
1712
- var EidOidcError = class extends EidError {
1713
- constructor(code, message, description) {
1714
- super(message);
1715
- this.code = code;
1716
- this.description = description;
1717
- }
1718
- code;
1719
- description;
1720
- };
1721
- var EidOidcClient = class {
1722
- constructor(cfg) {
1723
- this.cfg = cfg;
1724
- if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
1725
- if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
1726
- if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
1727
- this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
1728
- this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
1729
- }
1730
- cfg;
1731
- issuer;
1732
- scope;
1733
- discovery;
1734
- jwks;
1735
- /** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
1736
- discover() {
1737
- const c = this.discovery;
1738
- if (c && Date.now() - c.at < CACHE_MS) return c.d;
1739
- const d = this.fetchDiscovery();
1740
- this.discovery = { at: Date.now(), d };
1741
- d.catch(() => {
1742
- if (this.discovery?.d === d) this.discovery = void 0;
1743
- });
1744
- return d;
1745
- }
1746
- async fetchDiscovery() {
1747
- const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
1748
- if (!d || d.issuer !== this.issuer) {
1749
- 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})`);
1750
- }
1751
- for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
1752
- if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
1753
- throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
1686
+ // src/login.ts
1687
+ import { createHash as createHash5, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1688
+ function memoryLoginStore() {
1689
+ const m = /* @__PURE__ */ new Map();
1690
+ return {
1691
+ get(k) {
1692
+ const e = m.get(k);
1693
+ if (!e) return void 0;
1694
+ if (e.exp <= Date.now()) {
1695
+ m.delete(k);
1696
+ return void 0;
1697
+ }
1698
+ return e.tx;
1699
+ },
1700
+ set(k, tx, ttl) {
1701
+ const now = Date.now();
1702
+ if (m.size > 1e3) {
1703
+ for (const [key, e] of m) if (e.exp <= now) m.delete(key);
1754
1704
  }
1705
+ m.set(k, { tx, exp: now + ttl });
1706
+ },
1707
+ delete(k) {
1708
+ return m.delete(k);
1709
+ }
1710
+ };
1711
+ }
1712
+ var CALLBACK_PATH = "/auth/eid/callback";
1713
+ var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
1714
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1715
+ var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
1716
+ var IOS_BROWSERS = [
1717
+ [/CriOS/i, "googlechromes"],
1718
+ [/EdgiOS/i, "microsoft-edge-https"],
1719
+ [/FxiOS/i, "firefox"],
1720
+ [/OPT\//i, "touch-https"],
1721
+ [/OPiOS|OPR\//i, "opera-https"],
1722
+ [/DuckDuckGo/i, "ddgQuickLink"]
1723
+ ];
1724
+ var ANDROID_BROWSERS = [
1725
+ [/EdgA\//i, "com.microsoft.emmx"],
1726
+ [/Opera Mini/i, "com.opera.mini.native"],
1727
+ [/OPR\//i, "com.opera.browser"],
1728
+ [/SamsungBrowser\//i, "com.sec.android.app.sbrowser"],
1729
+ [/YaBrowser\//i, "com.yandex.browser"],
1730
+ [/DuckDuckGo\//i, "com.duckduckgo.mobile.android"],
1731
+ [/Vivaldi\//i, "com.vivaldi.browser"],
1732
+ [/Firefox\//i, "org.mozilla.firefox"],
1733
+ [/Chrome\//i, "com.android.chrome"]
1734
+ ];
1735
+ function withReturnHint(callbackUrl, userAgent) {
1736
+ const u = new URL(callbackUrl);
1737
+ const pick = (t) => t.find(([re]) => re.test(userAgent))?.[1];
1738
+ if (/iPhone|iPad|iPod/i.test(userAgent)) {
1739
+ const v = pick(IOS_BROWSERS);
1740
+ if (v) u.searchParams.set("retScheme", v);
1741
+ } else if (/Android/i.test(userAgent)) {
1742
+ const v = pick(ANDROID_BROWSERS);
1743
+ if (v) u.searchParams.set("retPkg", v);
1744
+ }
1745
+ return u.toString();
1746
+ }
1747
+ function header(h, name) {
1748
+ const v = h[name] ?? h[name.toLowerCase()];
1749
+ return (Array.isArray(v) ? v[0] : v) ?? "";
1750
+ }
1751
+ function readCookie(h, name) {
1752
+ for (const part of header(h, "cookie").split(";")) {
1753
+ const i = part.indexOf("=");
1754
+ if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
1755
+ }
1756
+ return "";
1757
+ }
1758
+ var sha256hex = (s) => createHash5("sha256").update(s).digest("hex");
1759
+ function safeEqualHex(a, b) {
1760
+ const x = Buffer.from(a, "hex");
1761
+ const y = Buffer.from(b, "hex");
1762
+ return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
1763
+ }
1764
+ function toIdentity(r, v, flowType) {
1765
+ const s = v.parsedSubject;
1766
+ const etsi = s.serialNumber ?? "";
1767
+ const m = /^PNOMN-([0-9]+)$/.exec(etsi);
1768
+ return {
1769
+ etsi,
1770
+ civilId: m ? m[1] : null,
1771
+ givenName: s.givenName ?? null,
1772
+ surname: s.surname ?? null,
1773
+ name: s.name,
1774
+ country: s.country ?? null,
1775
+ documentNumber: v.documentNumber,
1776
+ certificateLevel: v.certificateLevel,
1777
+ certificateDerB64: r.certificateDerB64 ?? "",
1778
+ flowType,
1779
+ person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
1780
+ };
1781
+ }
1782
+ function createEidLogin(opts) {
1783
+ const client = opts.client;
1784
+ if (!opts.onSuccess) throw new Error("createEidLogin: onSuccess \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
1785
+ if (opts.callbackUrl) {
1786
+ const u = new URL(opts.callbackUrl);
1787
+ if (u.protocol !== "https:" || u.pathname !== CALLBACK_PATH) {
1788
+ throw new Error(`createEidLogin: callbackUrl \u043D\u044C https://<\u0442\u0430\u043D\u044B host>${CALLBACK_PATH} \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
1755
1789
  }
1756
- return d;
1757
1790
  }
1758
- /**
1759
- * Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
1760
- * хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
1761
- */
1762
- async createAuthorization(opts = {}) {
1763
- const d = await this.discover();
1764
- const state = randomB64url(32);
1765
- const nonce = randomB64url(32);
1766
- const codeVerifier = randomB64url(32);
1767
- const u = new URL(d.authorization_endpoint);
1768
- const p = new URLSearchParams({
1769
- response_type: "code",
1770
- client_id: this.cfg.clientId,
1771
- redirect_uri: this.cfg.redirectUri,
1772
- scope: opts.scope ?? this.scope,
1773
- state,
1774
- nonce,
1775
- code_challenge: await pkceChallenge(codeVerifier),
1776
- code_challenge_method: OIDC_EXPECT.pkceMethod
1791
+ const store = opts.store ?? memoryLoginStore();
1792
+ const ttl = opts.ttlMs ?? 12e4;
1793
+ const pollMs = opts.pollMs ?? 1e3;
1794
+ const cookieName = opts.cookie?.name ?? "eid_login";
1795
+ const cookiePath = opts.cookie?.path ?? "/";
1796
+ const secure = opts.cookie?.secure ?? true;
1797
+ const successRedirect = opts.successRedirect ?? "/";
1798
+ const failureRedirect = opts.failureRedirect ?? "/";
1799
+ const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
1800
+ const pushHits = /* @__PURE__ */ new Map();
1801
+ const rateLimit = opts.rateLimit ?? ((key) => {
1802
+ if (!key.startsWith("push:")) return true;
1803
+ const now = Date.now();
1804
+ const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
1805
+ if (pushHits.size > 1e4) pushHits.clear();
1806
+ if (hits.length >= 3) return false;
1807
+ hits.push(now);
1808
+ pushHits.set(key, hits);
1809
+ return true;
1810
+ });
1811
+ const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
1812
+ const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
1813
+ const json = (status2, body, extra = {}) => ({
1814
+ status: status2,
1815
+ headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...extra },
1816
+ body: JSON.stringify(body)
1817
+ });
1818
+ const cookieAttrs = (maxAge) => `Path=${cookiePath}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` + (opts.cookie?.domain ? `; Domain=${opts.cookie.domain}` : "");
1819
+ const clearCookie = `${cookieName}=; ${cookieAttrs(0)}`;
1820
+ const redirect = (to, extra = {}) => ({
1821
+ status: 303,
1822
+ headers: { Location: to, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", ...extra },
1823
+ body: ""
1824
+ });
1825
+ const failTo = (code) => {
1826
+ const u = new URL(failureRedirect, "http://x");
1827
+ u.searchParams.set("eid_error", code);
1828
+ return /^https?:/i.test(failureRedirect) ? u.toString() : u.pathname + u.search + u.hash;
1829
+ };
1830
+ const mergeHeaders = (a, b) => {
1831
+ const out = { ...a };
1832
+ for (const [k, v] of Object.entries(b ?? {})) {
1833
+ const key = Object.keys(out).find((x) => x.toLowerCase() === k.toLowerCase()) ?? k;
1834
+ const prev = out[key];
1835
+ out[key] = key.toLowerCase() === "set-cookie" && prev ? [...[prev].flat(), ...[v].flat()] : v;
1836
+ }
1837
+ return out;
1838
+ };
1839
+ async function loadTx(req) {
1840
+ const raw = readCookie(req.headers, cookieName);
1841
+ const dot = raw.indexOf(".");
1842
+ const sid = raw.slice(0, dot);
1843
+ const binding = raw.slice(dot + 1);
1844
+ if (dot < 0 || !UUID_RE.test(sid) || !B64URL_RE.test(binding)) return void 0;
1845
+ const tx = await store.get(sid);
1846
+ if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
1847
+ if (tx.expiresAt <= Date.now()) {
1848
+ await store.delete(sid);
1849
+ return void 0;
1850
+ }
1851
+ return tx;
1852
+ }
1853
+ function qrLink(tx) {
1854
+ if (tx.mode !== "qr" || !tx.link) return void 0;
1855
+ return buildDeviceLink({
1856
+ deviceLinkBase: tx.link.base,
1857
+ deviceLinkType: "QR",
1858
+ sessionToken: tx.link.token,
1859
+ sessionSecret: tx.link.secret,
1860
+ sessionType: "auth",
1861
+ lang: tx.link.lang,
1862
+ receivedAt: tx.link.receivedAt,
1863
+ rpChallenge: tx.acsp.rpChallenge,
1864
+ relyingPartyName: tx.acsp.relyingPartyName,
1865
+ interactions: tx.acsp.interactions
1777
1866
  });
1778
- if (opts.display) p.set("display", opts.display);
1779
- u.search = p.toString();
1780
- return { url: u.toString(), state, nonce, codeVerifier };
1781
1867
  }
1782
- /**
1783
- * redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
1784
- * id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
1785
- * Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
1786
- */
1787
- async handleCallback(input, checks) {
1788
- const q = callbackParams(input);
1789
- const state = q.get("state") ?? "";
1790
- if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
1791
- 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");
1868
+ async function begin(req, mode, idCode, lang) {
1869
+ const level = opts.certificateLevel ? { certificateLevel: opts.certificateLevel } : {};
1870
+ let tx;
1871
+ let appLink;
1872
+ if (mode === "push") {
1873
+ const s = await client.auth.notificationByEtsi(idCode, interactions, level);
1874
+ tx = { sessionId: s.sessionId, mode, vc: s.vc, acsp: s.acsp };
1875
+ } else {
1876
+ const callbackUrl = mode === "web2app" ? withReturnHint(opts.callbackUrl, header(req.headers, "user-agent")) : void 0;
1877
+ const s = await client.auth.deviceLinkAnonymous(interactions, { ...level, callbackUrl });
1878
+ if (!s.deviceLinkBase || !s.sessionToken || !s.sessionSecret || !s.acsp) throw new Error("eID \u0445\u0430\u0440\u0438\u0443\u043D\u0434 device link \u0442\u0430\u043B\u0431\u0430\u0440 \u0434\u0443\u0442\u0443\u0443");
1879
+ tx = {
1880
+ sessionId: s.sessionId,
1881
+ mode,
1882
+ vc: s.vc,
1883
+ acsp: s.acsp,
1884
+ link: { base: s.deviceLinkBase, token: s.sessionToken, secret: s.sessionSecret, receivedAt: s.receivedAt, lang }
1885
+ };
1886
+ if (mode === "web2app") {
1887
+ appLink = buildDeviceLink({
1888
+ deviceLinkBase: s.deviceLinkBase,
1889
+ deviceLinkType: "Web2App",
1890
+ sessionToken: s.sessionToken,
1891
+ sessionSecret: s.sessionSecret,
1892
+ sessionType: "auth",
1893
+ lang,
1894
+ rpChallenge: s.acsp.rpChallenge,
1895
+ relyingPartyName: s.acsp.relyingPartyName,
1896
+ interactions: s.acsp.interactions,
1897
+ initialCallbackUrl: s.acsp.initialCallbackUrl
1898
+ });
1899
+ }
1900
+ }
1901
+ const binding = randomBytes2(32).toString("base64url");
1902
+ const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
1903
+ await store.set(full.sessionId, full, ttl);
1904
+ const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
1905
+ return { tx: full, setCookie, appLink };
1906
+ }
1907
+ async function start(req) {
1908
+ const url = new URL(req.url, "http://x");
1909
+ const site = header(req.headers, "sec-fetch-site");
1910
+ if (site && site !== "same-origin" && site !== "same-site" && site !== "none") return json(403, { error: "cross_site" });
1911
+ let body = {};
1912
+ if (req.method === "GET") {
1913
+ if (url.searchParams.get("mode") !== "web2app") return json(405, { error: "method_not_allowed" });
1914
+ body = { mode: "web2app", lang: url.searchParams.get("lang") ?? void 0 };
1915
+ } else if (req.method === "POST") {
1916
+ if (!header(req.headers, "content-type").toLowerCase().startsWith("application/json")) {
1917
+ return json(415, { error: "json_required" });
1918
+ }
1919
+ try {
1920
+ const b = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
1921
+ if (b && typeof b === "object") body = b;
1922
+ } catch {
1923
+ return json(400, { error: "invalid_body" });
1924
+ }
1925
+ } else {
1926
+ return json(405, { error: "method_not_allowed" });
1792
1927
  }
1793
- const iss = q.get("iss");
1794
- if (iss !== null && iss !== this.issuer) {
1795
- throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1928
+ const mode = body.mode;
1929
+ if (mode !== "qr" && mode !== "push" && mode !== "web2app") return json(400, { error: "invalid_mode" });
1930
+ const lang = body.lang === "en" ? "eng" : "mon";
1931
+ const ctx = { request: req, mode };
1932
+ if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
1933
+ const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
1934
+ if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
1935
+ const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
1936
+ if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
1937
+ if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
1938
+ let r;
1939
+ try {
1940
+ r = await begin(req, mode, idCode, lang);
1941
+ } catch (e) {
1942
+ onError(e, ctx);
1943
+ const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
1944
+ return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
1796
1945
  }
1797
- const err = q.get("error");
1798
- if (err) {
1799
- const code2 = KNOWN_CODES.has(err) ? err : "server_error";
1800
- 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);
1946
+ if (mode === "web2app") {
1947
+ return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
1801
1948
  }
1802
- const code = q.get("code") ?? "";
1803
- if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
1804
- const idToken = await this.exchangeCode(code, checks.codeVerifier);
1805
- const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
1806
- return { claims, idToken };
1807
- }
1808
- /** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
1809
- async exchangeCode(code, codeVerifier) {
1810
- const d = await this.discover();
1811
- const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
1812
- const res = await this.fetch(d.token_endpoint, {
1813
- method: "POST",
1814
- headers: {
1815
- Authorization: "Basic " + basic,
1816
- "Content-Type": "application/x-www-form-urlencoded",
1817
- Accept: "application/json"
1818
- },
1819
- body: new URLSearchParams({
1820
- grant_type: "authorization_code",
1821
- code,
1822
- redirect_uri: this.cfg.redirectUri,
1823
- code_verifier: codeVerifier
1824
- }).toString()
1825
- });
1826
- let j = {};
1949
+ return json(
1950
+ 200,
1951
+ { mode, vc: r.tx.vc, ...mode === "qr" ? { qr: qrLink(r.tx) } : {}, expiresIn: Math.ceil(ttl / 1e3) },
1952
+ { "Set-Cookie": r.setCookie }
1953
+ );
1954
+ }
1955
+ async function finish(req, tx, r, flow, verifier) {
1956
+ const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
1957
+ if (!await store.delete(tx.sessionId)) return void 0;
1958
+ const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
1959
+ return out;
1960
+ }
1961
+ async function status(req) {
1962
+ if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
1963
+ const tx = await loadTx(req);
1964
+ if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
1965
+ const ctx = { request: req, mode: tx.mode };
1966
+ let r;
1827
1967
  try {
1828
- j = await res.json();
1829
- } catch {
1968
+ r = await client.session.poll(tx.sessionId, pollMs);
1969
+ } catch (e) {
1970
+ if (e instanceof ApiError && e.status === 404) {
1971
+ await store.delete(tx.sessionId);
1972
+ return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
1973
+ }
1974
+ onError(e, ctx);
1975
+ return json(502, { error: "eid_unavailable" });
1830
1976
  }
1831
- if (!res.ok) {
1832
- const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
1833
- throw new EidOidcError(
1834
- e,
1835
- `OIDC token ${res.status}: ${e}`,
1836
- typeof j.error_description === "string" ? j.error_description : void 0
1837
- );
1977
+ const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
1978
+ if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
1979
+ if (r.endResult !== "OK") {
1980
+ await store.delete(tx.sessionId);
1981
+ return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
1838
1982
  }
1839
- if (typeof j.id_token !== "string" || j.id_token === "") {
1840
- throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
1983
+ if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
1984
+ try {
1985
+ const out = await finish(req, tx, r, tx.mode === "qr" ? "QR" : "Notification");
1986
+ if (!out) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
1987
+ return json(200, { state: "OK", redirect: out.redirect, data: out.data }, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
1988
+ } catch (e) {
1989
+ onError(e, ctx);
1990
+ if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
1991
+ await store.delete(tx.sessionId);
1992
+ return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
1841
1993
  }
1842
- return j.id_token;
1843
1994
  }
1844
- /**
1845
- * id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
1846
- * (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
1847
- */
1848
- async verifyIdToken(idToken, opts) {
1849
- const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
1850
- const parts = idToken.split(".");
1851
- if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1852
- const header = decodeJsonPart(parts[0]);
1853
- const payload = decodeJsonPart(parts[1]);
1854
- if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
1855
- if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
1856
- const kid = typeof header.kid === "string" ? header.kid : "";
1857
- if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
1858
- const pick = (ks) => ks.find(
1859
- (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")
1860
- );
1861
- const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
1862
- if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
1863
- const sig = Buffer.from(parts[2], "base64url");
1864
- 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");
1865
- let ok = false;
1995
+ async function callback(req) {
1996
+ if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
1997
+ const q = new URL(req.url, "http://x").searchParams;
1998
+ const sid = q.get("sessionId") ?? "";
1999
+ const verifier = q.get("userChallengeVerifier") ?? "";
2000
+ const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
2001
+ const tx = await loadTx(req);
2002
+ if (!tx || tx.sessionId !== sid || tx.mode !== "web2app") return fail2(tx ? "unbound" : "expired");
2003
+ if (!verifier) return fail2("unbound");
2004
+ const ctx = { request: req, mode: tx.mode };
2005
+ let r;
1866
2006
  try {
1867
- const key = await globalThis.crypto.subtle.importKey(
1868
- "jwk",
1869
- { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
1870
- { name: "ECDSA", namedCurve: "P-256" },
1871
- false,
1872
- ["verify"]
1873
- );
1874
- ok = await globalThis.crypto.subtle.verify(
1875
- { name: "ECDSA", hash: "SHA-256" },
1876
- key,
1877
- sig,
1878
- new TextEncoder().encode(parts[0] + "." + parts[1])
1879
- );
1880
- } catch {
1881
- ok = false;
2007
+ r = await client.session.poll(tx.sessionId, 1e4);
2008
+ } catch (e) {
2009
+ onError(e, ctx);
2010
+ return fail2("eid_unavailable");
2011
+ }
2012
+ if (r.state !== "COMPLETE") return fail2("not_complete");
2013
+ if (r.endResult !== "OK") {
2014
+ await store.delete(tx.sessionId);
2015
+ return fail2(r.endResult ?? "UNKNOWN");
1882
2016
  }
1883
- if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
1884
- const c = payload;
1885
- const now = Math.floor(Date.now() / 1e3);
1886
- if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1887
- const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
1888
- if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1889
- if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1890
- 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");
1891
- if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
1892
- if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
1893
- if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
1894
- return c;
1895
- }
1896
- keys(force) {
1897
- const c = this.jwks;
1898
- if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
1899
- const keys = this.discover().then(async (d) => {
1900
- const j = await this.getJson(d.jwks_uri);
1901
- return Array.isArray(j?.keys) ? j.keys : [];
1902
- });
1903
- this.jwks = { at: Date.now(), keys };
1904
- keys.catch(() => {
1905
- if (this.jwks?.keys === keys) this.jwks = void 0;
1906
- });
1907
- return keys;
1908
- }
1909
- async getJson(url) {
1910
- const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
1911
- if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
1912
2017
  try {
1913
- return await res.json();
1914
- } catch {
1915
- throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
2018
+ const out = await finish(req, tx, r, "Web2App", verifier);
2019
+ if (!out) return fail2("expired");
2020
+ return redirect(out.redirect ?? successRedirect, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
2021
+ } catch (e) {
2022
+ onError(e, ctx);
2023
+ if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
2024
+ await store.delete(tx.sessionId);
2025
+ return fail2("VALIDATION");
1916
2026
  }
1917
2027
  }
1918
- async fetch(url, init) {
1919
- const f = this.cfg.fetchImpl ?? fetch;
2028
+ async function handleRequest(req) {
2029
+ const seg = new URL(req.url, "http://x").pathname.replace(/\/+$/, "").split("/").pop();
1920
2030
  try {
1921
- return await f(url, {
1922
- ...init,
1923
- redirect: "error",
1924
- signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
1925
- });
2031
+ if (seg === "start") return await start(req);
2032
+ if (seg === "status") return await status(req);
2033
+ if (seg === "callback") return await callback(req);
1926
2034
  } catch (e) {
1927
- const err = e;
1928
- if (err.name === "TimeoutError" || err.name === "AbortError") {
1929
- 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}`);
1930
- }
1931
- throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
2035
+ onError(e, { request: req, mode: "qr" });
2036
+ if (e instanceof AuthenticationError || e instanceof ForbiddenError) return json(502, { error: "eid_unavailable" });
2037
+ return json(500, { error: "internal" });
1932
2038
  }
2039
+ return json(404, { error: "not_found" });
1933
2040
  }
1934
- };
1935
- async function pkceChallenge(codeVerifier) {
1936
- const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
1937
- return Buffer.from(h).toString("base64url");
1938
- }
1939
- function randomB64url(bytes) {
1940
- return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
1941
- }
1942
- function formEncode(s) {
1943
- return new URLSearchParams({ v: s }).toString().slice(2);
1944
- }
1945
- function timingSafeEqualStr(a, b) {
1946
- const x = Buffer.from(a, "utf8");
1947
- const y = Buffer.from(b, "utf8");
1948
- return x.length === y.length && timingSafeEqual2(x, y);
1949
- }
1950
- function decodeJsonPart(s) {
1951
- try {
1952
- const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
1953
- return v && typeof v === "object" && !Array.isArray(v) ? v : null;
1954
- } catch {
1955
- return null;
2041
+ async function handle(req) {
2042
+ const headers = {};
2043
+ req.headers.forEach((v, k) => headers[k] = v);
2044
+ const body = req.method === "POST" ? await req.text() : void 0;
2045
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body });
2046
+ const h = new Headers();
2047
+ for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2048
+ return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
1956
2049
  }
1957
- }
1958
- function callbackParams(input) {
1959
- if (input instanceof URLSearchParams) return input;
1960
- if (input instanceof URL) return input.searchParams;
1961
- const s = input.trim();
1962
- if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
1963
- const i = s.indexOf("?");
1964
- return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
2050
+ return { handleRequest, handle };
1965
2051
  }
1966
2052
  export {
1967
2053
  ApiError,
@@ -1972,13 +2058,8 @@ export {
1972
2058
  DEFAULT_PDF_MAX_BYTES,
1973
2059
  DEFAULT_PDF_TIMEOUT_MS,
1974
2060
  DeviceLinkError,
1975
- EID_ACR_ADVANCED,
1976
- EID_ACR_QUALIFIED,
1977
- EID_OIDC_ISSUER,
1978
2061
  EidClient,
1979
2062
  EidError,
1980
- EidOidcClient,
1981
- EidOidcError,
1982
2063
  ForbiddenError,
1983
2064
  Http,
1984
2065
  NetworkError,
@@ -1997,15 +2078,16 @@ export {
1997
2078
  acspV2Digest,
1998
2079
  buildAcspV2Payload,
1999
2080
  buildDeviceLink,
2081
+ createEidLogin,
2000
2082
  deviceLink,
2001
2083
  interactionsToBase64,
2002
2084
  isMobileUserAgent,
2085
+ memoryLoginStore,
2003
2086
  openOrShowQR,
2004
2087
  parseRetryAfter,
2005
2088
  parseSessionResult,
2006
2089
  parseSubject,
2007
2090
  personMatchesCertificate,
2008
- pkceChallenge,
2009
2091
  qrDeviceLinkTicker,
2010
2092
  randomChallenge,
2011
2093
  resolveBaseUrl,
@@ -2015,5 +2097,6 @@ export {
2015
2097
  userChallengeOf,
2016
2098
  verifyPayloadSignature,
2017
2099
  verifyPrehashedSignature,
2018
- verifyTokenCertificates
2100
+ verifyTokenCertificates,
2101
+ withReturnHint
2019
2102
  };