@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/README.md +79 -86
- package/dist/button.cjs +952 -0
- package/dist/button.d.cts +79 -0
- package/dist/button.d.ts +79 -0
- package/dist/button.js +923 -0
- package/dist/index.cjs +350 -270
- package/dist/index.d.cts +122 -127
- package/dist/index.d.ts +122 -127
- package/dist/index.js +349 -266
- package/package.json +12 -4
package/dist/index.cjs
CHANGED
|
@@ -28,13 +28,8 @@ __export(index_exports, {
|
|
|
28
28
|
DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
|
|
29
29
|
DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
|
|
30
30
|
DeviceLinkError: () => DeviceLinkError,
|
|
31
|
-
EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
|
|
32
|
-
EID_ACR_QUALIFIED: () => EID_ACR_QUALIFIED,
|
|
33
|
-
EID_OIDC_ISSUER: () => EID_OIDC_ISSUER,
|
|
34
31
|
EidClient: () => EidClient,
|
|
35
32
|
EidError: () => EidError,
|
|
36
|
-
EidOidcClient: () => EidOidcClient,
|
|
37
|
-
EidOidcError: () => EidOidcError,
|
|
38
33
|
ForbiddenError: () => ForbiddenError,
|
|
39
34
|
Http: () => Http,
|
|
40
35
|
NetworkError: () => NetworkError,
|
|
@@ -53,15 +48,16 @@ __export(index_exports, {
|
|
|
53
48
|
acspV2Digest: () => acspV2Digest,
|
|
54
49
|
buildAcspV2Payload: () => buildAcspV2Payload,
|
|
55
50
|
buildDeviceLink: () => buildDeviceLink,
|
|
51
|
+
createEidLogin: () => createEidLogin,
|
|
56
52
|
deviceLink: () => deviceLink,
|
|
57
53
|
interactionsToBase64: () => interactionsToBase64,
|
|
58
54
|
isMobileUserAgent: () => isMobileUserAgent,
|
|
55
|
+
memoryLoginStore: () => memoryLoginStore,
|
|
59
56
|
openOrShowQR: () => openOrShowQR,
|
|
60
57
|
parseRetryAfter: () => parseRetryAfter,
|
|
61
58
|
parseSessionResult: () => parseSessionResult,
|
|
62
59
|
parseSubject: () => parseSubject,
|
|
63
60
|
personMatchesCertificate: () => personMatchesCertificate,
|
|
64
|
-
pkceChallenge: () => pkceChallenge,
|
|
65
61
|
qrDeviceLinkTicker: () => qrDeviceLinkTicker,
|
|
66
62
|
randomChallenge: () => randomChallenge,
|
|
67
63
|
resolveBaseUrl: () => resolveBaseUrl,
|
|
@@ -71,7 +67,8 @@ __export(index_exports, {
|
|
|
71
67
|
userChallengeOf: () => userChallengeOf,
|
|
72
68
|
verifyPayloadSignature: () => verifyPayloadSignature,
|
|
73
69
|
verifyPrehashedSignature: () => verifyPrehashedSignature,
|
|
74
|
-
verifyTokenCertificates: () => verifyTokenCertificates
|
|
70
|
+
verifyTokenCertificates: () => verifyTokenCertificates,
|
|
71
|
+
withReturnHint: () => withReturnHint
|
|
75
72
|
});
|
|
76
73
|
module.exports = __toCommonJS(index_exports);
|
|
77
74
|
|
|
@@ -350,13 +347,6 @@ function routePath(route, ...args) {
|
|
|
350
347
|
if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
|
|
351
348
|
return out;
|
|
352
349
|
}
|
|
353
|
-
var OIDC_EXPECT = {
|
|
354
|
-
defaultScope: "openid",
|
|
355
|
-
pkceMethod: "S256",
|
|
356
|
-
idTokenAlg: "ES256",
|
|
357
|
-
/** `Authorization: Basic` (RFC 6749 §2.3.1). */
|
|
358
|
-
tokenEndpointAuthMethod: "client_secret_basic"
|
|
359
|
-
};
|
|
360
350
|
|
|
361
351
|
// src/crypto.ts
|
|
362
352
|
var import_node_crypto = require("crypto");
|
|
@@ -1773,6 +1763,8 @@ var EidClient = class {
|
|
|
1773
1763
|
token;
|
|
1774
1764
|
session;
|
|
1775
1765
|
validator;
|
|
1766
|
+
/** `credentials.rpName` — иргэний утсанд харагдах RP нэр. */
|
|
1767
|
+
rpName;
|
|
1776
1768
|
constructor(cfg) {
|
|
1777
1769
|
if (!cfg.credentials?.apiSecret) throw new Error("EidClient: credentials.apiSecret \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
1778
1770
|
const base = resolveBaseUrl(cfg.baseUrl);
|
|
@@ -1797,6 +1789,7 @@ var EidClient = class {
|
|
|
1797
1789
|
this.token = new TokenApi(http, cfg.credentials);
|
|
1798
1790
|
this.session = new SessionApi(http);
|
|
1799
1791
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1792
|
+
this.rpName = cfg.credentials.rpName;
|
|
1800
1793
|
}
|
|
1801
1794
|
};
|
|
1802
1795
|
|
|
@@ -1912,281 +1905,371 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1912
1905
|
return () => clearInterval(t);
|
|
1913
1906
|
}
|
|
1914
1907
|
|
|
1915
|
-
// src/
|
|
1908
|
+
// src/login.ts
|
|
1916
1909
|
var import_node_crypto7 = require("crypto");
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1910
|
+
function memoryLoginStore() {
|
|
1911
|
+
const m = /* @__PURE__ */ new Map();
|
|
1912
|
+
return {
|
|
1913
|
+
get(k) {
|
|
1914
|
+
const e = m.get(k);
|
|
1915
|
+
if (!e) return void 0;
|
|
1916
|
+
if (e.exp <= Date.now()) {
|
|
1917
|
+
m.delete(k);
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
return e.tx;
|
|
1921
|
+
},
|
|
1922
|
+
set(k, tx, ttl) {
|
|
1923
|
+
const now = Date.now();
|
|
1924
|
+
if (m.size > 1e3) {
|
|
1925
|
+
for (const [key, e] of m) if (e.exp <= now) m.delete(key);
|
|
1926
|
+
}
|
|
1927
|
+
m.set(k, { tx, exp: now + ttl });
|
|
1928
|
+
},
|
|
1929
|
+
delete(k) {
|
|
1930
|
+
return m.delete(k);
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1935
|
+
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1936
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1937
|
+
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1938
|
+
var IOS_BROWSERS = [
|
|
1939
|
+
[/CriOS/i, "googlechromes"],
|
|
1940
|
+
[/EdgiOS/i, "microsoft-edge-https"],
|
|
1941
|
+
[/FxiOS/i, "firefox"],
|
|
1942
|
+
[/OPT\//i, "touch-https"],
|
|
1943
|
+
[/OPiOS|OPR\//i, "opera-https"],
|
|
1944
|
+
[/DuckDuckGo/i, "ddgQuickLink"]
|
|
1945
|
+
];
|
|
1946
|
+
var ANDROID_BROWSERS = [
|
|
1947
|
+
[/EdgA\//i, "com.microsoft.emmx"],
|
|
1948
|
+
[/Opera Mini/i, "com.opera.mini.native"],
|
|
1949
|
+
[/OPR\//i, "com.opera.browser"],
|
|
1950
|
+
[/SamsungBrowser\//i, "com.sec.android.app.sbrowser"],
|
|
1951
|
+
[/YaBrowser\//i, "com.yandex.browser"],
|
|
1952
|
+
[/DuckDuckGo\//i, "com.duckduckgo.mobile.android"],
|
|
1953
|
+
[/Vivaldi\//i, "com.vivaldi.browser"],
|
|
1954
|
+
[/Firefox\//i, "org.mozilla.firefox"],
|
|
1955
|
+
[/Chrome\//i, "com.android.chrome"]
|
|
1956
|
+
];
|
|
1957
|
+
function withReturnHint(callbackUrl, userAgent) {
|
|
1958
|
+
const u = new URL(callbackUrl);
|
|
1959
|
+
const pick = (t) => t.find(([re]) => re.test(userAgent))?.[1];
|
|
1960
|
+
if (/iPhone|iPad|iPod/i.test(userAgent)) {
|
|
1961
|
+
const v = pick(IOS_BROWSERS);
|
|
1962
|
+
if (v) u.searchParams.set("retScheme", v);
|
|
1963
|
+
} else if (/Android/i.test(userAgent)) {
|
|
1964
|
+
const v = pick(ANDROID_BROWSERS);
|
|
1965
|
+
if (v) u.searchParams.set("retPkg", v);
|
|
1942
1966
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1967
|
+
return u.toString();
|
|
1968
|
+
}
|
|
1969
|
+
function header(h, name) {
|
|
1970
|
+
const v = h[name] ?? h[name.toLowerCase()];
|
|
1971
|
+
return (Array.isArray(v) ? v[0] : v) ?? "";
|
|
1972
|
+
}
|
|
1973
|
+
function readCookie(h, name) {
|
|
1974
|
+
for (const part of header(h, "cookie").split(";")) {
|
|
1975
|
+
const i = part.indexOf("=");
|
|
1976
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
1954
1977
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1978
|
+
return "";
|
|
1979
|
+
}
|
|
1980
|
+
var sha256hex = (s) => (0, import_node_crypto7.createHash)("sha256").update(s).digest("hex");
|
|
1981
|
+
function safeEqualHex(a, b) {
|
|
1982
|
+
const x = Buffer.from(a, "hex");
|
|
1983
|
+
const y = Buffer.from(b, "hex");
|
|
1984
|
+
return x.length === y.length && x.length > 0 && (0, import_node_crypto7.timingSafeEqual)(x, y);
|
|
1985
|
+
}
|
|
1986
|
+
function toIdentity(r, v, flowType) {
|
|
1987
|
+
const s = v.parsedSubject;
|
|
1988
|
+
const etsi = s.serialNumber ?? "";
|
|
1989
|
+
const m = /^PNOMN-([0-9]+)$/.exec(etsi);
|
|
1990
|
+
return {
|
|
1991
|
+
etsi,
|
|
1992
|
+
civilId: m ? m[1] : null,
|
|
1993
|
+
givenName: s.givenName ?? null,
|
|
1994
|
+
surname: s.surname ?? null,
|
|
1995
|
+
name: s.name,
|
|
1996
|
+
country: s.country ?? null,
|
|
1997
|
+
documentNumber: v.documentNumber,
|
|
1998
|
+
certificateLevel: v.certificateLevel,
|
|
1999
|
+
certificateDerB64: r.certificateDerB64 ?? "",
|
|
2000
|
+
flowType,
|
|
2001
|
+
person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function createEidLogin(opts) {
|
|
2005
|
+
const client = opts.client;
|
|
2006
|
+
if (!opts.onSuccess) throw new Error("createEidLogin: onSuccess \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
2007
|
+
if (opts.callbackUrl) {
|
|
2008
|
+
const u = new URL(opts.callbackUrl);
|
|
2009
|
+
if (u.protocol !== "https:" || u.pathname !== CALLBACK_PATH) {
|
|
2010
|
+
throw new Error(`createEidLogin: callbackUrl \u043D\u044C https://<\u0442\u0430\u043D\u044B host>${CALLBACK_PATH} \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
|
|
2011
|
+
}
|
|
1970
2012
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2013
|
+
const store = opts.store ?? memoryLoginStore();
|
|
2014
|
+
const ttl = opts.ttlMs ?? 12e4;
|
|
2015
|
+
const pollMs = opts.pollMs ?? 1e3;
|
|
2016
|
+
const cookieName = opts.cookie?.name ?? "eid_login";
|
|
2017
|
+
const cookiePath = opts.cookie?.path ?? "/";
|
|
2018
|
+
const secure = opts.cookie?.secure ?? true;
|
|
2019
|
+
const successRedirect = opts.successRedirect ?? "/";
|
|
2020
|
+
const failureRedirect = opts.failureRedirect ?? "/";
|
|
2021
|
+
const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
|
|
2022
|
+
const pushHits = /* @__PURE__ */ new Map();
|
|
2023
|
+
const rateLimit = opts.rateLimit ?? ((key) => {
|
|
2024
|
+
if (!key.startsWith("push:")) return true;
|
|
2025
|
+
const now = Date.now();
|
|
2026
|
+
const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
|
|
2027
|
+
if (pushHits.size > 1e4) pushHits.clear();
|
|
2028
|
+
if (hits.length >= 3) return false;
|
|
2029
|
+
hits.push(now);
|
|
2030
|
+
pushHits.set(key, hits);
|
|
2031
|
+
return true;
|
|
2032
|
+
});
|
|
2033
|
+
const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
|
|
2034
|
+
const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
|
|
2035
|
+
const json = (status2, body, extra = {}) => ({
|
|
2036
|
+
status: status2,
|
|
2037
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...extra },
|
|
2038
|
+
body: JSON.stringify(body)
|
|
2039
|
+
});
|
|
2040
|
+
const cookieAttrs = (maxAge) => `Path=${cookiePath}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` + (opts.cookie?.domain ? `; Domain=${opts.cookie.domain}` : "");
|
|
2041
|
+
const clearCookie = `${cookieName}=; ${cookieAttrs(0)}`;
|
|
2042
|
+
const redirect = (to, extra = {}) => ({
|
|
2043
|
+
status: 303,
|
|
2044
|
+
headers: { Location: to, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", ...extra },
|
|
2045
|
+
body: ""
|
|
2046
|
+
});
|
|
2047
|
+
const failTo = (code) => {
|
|
2048
|
+
const u = new URL(failureRedirect, "http://x");
|
|
2049
|
+
u.searchParams.set("eid_error", code);
|
|
2050
|
+
return /^https?:/i.test(failureRedirect) ? u.toString() : u.pathname + u.search + u.hash;
|
|
2051
|
+
};
|
|
2052
|
+
const mergeHeaders = (a, b) => {
|
|
2053
|
+
const out = { ...a };
|
|
2054
|
+
for (const [k, v] of Object.entries(b ?? {})) {
|
|
2055
|
+
const key = Object.keys(out).find((x) => x.toLowerCase() === k.toLowerCase()) ?? k;
|
|
2056
|
+
const prev = out[key];
|
|
2057
|
+
out[key] = key.toLowerCase() === "set-cookie" && prev ? [...[prev].flat(), ...[v].flat()] : v;
|
|
1975
2058
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2059
|
+
return out;
|
|
2060
|
+
};
|
|
2061
|
+
async function loadTx(req) {
|
|
2062
|
+
const raw = readCookie(req.headers, cookieName);
|
|
2063
|
+
const dot = raw.indexOf(".");
|
|
2064
|
+
const sid = raw.slice(0, dot);
|
|
2065
|
+
const binding = raw.slice(dot + 1);
|
|
2066
|
+
if (dot < 0 || !UUID_RE.test(sid) || !B64URL_RE.test(binding)) return void 0;
|
|
2067
|
+
const tx = await store.get(sid);
|
|
2068
|
+
if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
|
|
2069
|
+
if (tx.expiresAt <= Date.now()) {
|
|
2070
|
+
await store.delete(sid);
|
|
2071
|
+
return void 0;
|
|
1980
2072
|
}
|
|
1981
|
-
return
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
redirect_uri: this.cfg.redirectUri,
|
|
1997
|
-
scope: opts.scope ?? this.scope,
|
|
1998
|
-
state,
|
|
1999
|
-
nonce,
|
|
2000
|
-
code_challenge: await pkceChallenge(codeVerifier),
|
|
2001
|
-
code_challenge_method: OIDC_EXPECT.pkceMethod
|
|
2073
|
+
return tx;
|
|
2074
|
+
}
|
|
2075
|
+
function qrLink(tx) {
|
|
2076
|
+
if (tx.mode !== "qr" || !tx.link) return void 0;
|
|
2077
|
+
return buildDeviceLink({
|
|
2078
|
+
deviceLinkBase: tx.link.base,
|
|
2079
|
+
deviceLinkType: "QR",
|
|
2080
|
+
sessionToken: tx.link.token,
|
|
2081
|
+
sessionSecret: tx.link.secret,
|
|
2082
|
+
sessionType: "auth",
|
|
2083
|
+
lang: tx.link.lang,
|
|
2084
|
+
receivedAt: tx.link.receivedAt,
|
|
2085
|
+
rpChallenge: tx.acsp.rpChallenge,
|
|
2086
|
+
relyingPartyName: tx.acsp.relyingPartyName,
|
|
2087
|
+
interactions: tx.acsp.interactions
|
|
2002
2088
|
});
|
|
2003
|
-
if (opts.display) p.set("display", opts.display);
|
|
2004
|
-
u.search = p.toString();
|
|
2005
|
-
return { url: u.toString(), state, nonce, codeVerifier };
|
|
2006
2089
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2090
|
+
async function begin(req, mode, idCode, lang) {
|
|
2091
|
+
const level = opts.certificateLevel ? { certificateLevel: opts.certificateLevel } : {};
|
|
2092
|
+
let tx;
|
|
2093
|
+
let appLink;
|
|
2094
|
+
if (mode === "push") {
|
|
2095
|
+
const s = await client.auth.notificationByEtsi(idCode, interactions, level);
|
|
2096
|
+
tx = { sessionId: s.sessionId, mode, vc: s.vc, acsp: s.acsp };
|
|
2097
|
+
} else {
|
|
2098
|
+
const callbackUrl = mode === "web2app" ? withReturnHint(opts.callbackUrl, header(req.headers, "user-agent")) : void 0;
|
|
2099
|
+
const s = await client.auth.deviceLinkAnonymous(interactions, { ...level, callbackUrl });
|
|
2100
|
+
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");
|
|
2101
|
+
tx = {
|
|
2102
|
+
sessionId: s.sessionId,
|
|
2103
|
+
mode,
|
|
2104
|
+
vc: s.vc,
|
|
2105
|
+
acsp: s.acsp,
|
|
2106
|
+
link: { base: s.deviceLinkBase, token: s.sessionToken, secret: s.sessionSecret, receivedAt: s.receivedAt, lang }
|
|
2107
|
+
};
|
|
2108
|
+
if (mode === "web2app") {
|
|
2109
|
+
appLink = buildDeviceLink({
|
|
2110
|
+
deviceLinkBase: s.deviceLinkBase,
|
|
2111
|
+
deviceLinkType: "Web2App",
|
|
2112
|
+
sessionToken: s.sessionToken,
|
|
2113
|
+
sessionSecret: s.sessionSecret,
|
|
2114
|
+
sessionType: "auth",
|
|
2115
|
+
lang,
|
|
2116
|
+
rpChallenge: s.acsp.rpChallenge,
|
|
2117
|
+
relyingPartyName: s.acsp.relyingPartyName,
|
|
2118
|
+
interactions: s.acsp.interactions,
|
|
2119
|
+
initialCallbackUrl: s.acsp.initialCallbackUrl
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2017
2122
|
}
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2123
|
+
const binding = (0, import_node_crypto7.randomBytes)(32).toString("base64url");
|
|
2124
|
+
const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
|
|
2125
|
+
await store.set(full.sessionId, full, ttl);
|
|
2126
|
+
const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
|
|
2127
|
+
return { tx: full, setCookie, appLink };
|
|
2128
|
+
}
|
|
2129
|
+
async function start(req) {
|
|
2130
|
+
const url = new URL(req.url, "http://x");
|
|
2131
|
+
const site = header(req.headers, "sec-fetch-site");
|
|
2132
|
+
if (site && site !== "same-origin" && site !== "same-site" && site !== "none") return json(403, { error: "cross_site" });
|
|
2133
|
+
let body = {};
|
|
2134
|
+
if (req.method === "GET") {
|
|
2135
|
+
if (url.searchParams.get("mode") !== "web2app") return json(405, { error: "method_not_allowed" });
|
|
2136
|
+
body = { mode: "web2app", lang: url.searchParams.get("lang") ?? void 0 };
|
|
2137
|
+
} else if (req.method === "POST") {
|
|
2138
|
+
if (!header(req.headers, "content-type").toLowerCase().startsWith("application/json")) {
|
|
2139
|
+
return json(415, { error: "json_required" });
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
const b = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
|
|
2143
|
+
if (b && typeof b === "object") body = b;
|
|
2144
|
+
} catch {
|
|
2145
|
+
return json(400, { error: "invalid_body" });
|
|
2146
|
+
}
|
|
2147
|
+
} else {
|
|
2148
|
+
return json(405, { error: "method_not_allowed" });
|
|
2021
2149
|
}
|
|
2022
|
-
const
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
2025
|
-
|
|
2150
|
+
const mode = body.mode;
|
|
2151
|
+
if (mode !== "qr" && mode !== "push" && mode !== "web2app") return json(400, { error: "invalid_mode" });
|
|
2152
|
+
const lang = body.lang === "en" ? "eng" : "mon";
|
|
2153
|
+
const ctx = { request: req, mode };
|
|
2154
|
+
if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
|
|
2155
|
+
const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
|
|
2156
|
+
if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
|
|
2157
|
+
const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
|
|
2158
|
+
if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
|
|
2159
|
+
if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
|
|
2160
|
+
let r;
|
|
2161
|
+
try {
|
|
2162
|
+
r = await begin(req, mode, idCode, lang);
|
|
2163
|
+
} catch (e) {
|
|
2164
|
+
onError(e, ctx);
|
|
2165
|
+
const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
|
|
2166
|
+
return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
|
|
2026
2167
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
code_verifier: codeVerifier
|
|
2049
|
-
}).toString()
|
|
2050
|
-
});
|
|
2051
|
-
let j = {};
|
|
2168
|
+
if (mode === "web2app") {
|
|
2169
|
+
return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
|
|
2170
|
+
}
|
|
2171
|
+
return json(
|
|
2172
|
+
200,
|
|
2173
|
+
{ mode, vc: r.tx.vc, ...mode === "qr" ? { qr: qrLink(r.tx) } : {}, expiresIn: Math.ceil(ttl / 1e3) },
|
|
2174
|
+
{ "Set-Cookie": r.setCookie }
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
async function finish(req, tx, r, flow, verifier) {
|
|
2178
|
+
const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
|
|
2179
|
+
if (!await store.delete(tx.sessionId)) return void 0;
|
|
2180
|
+
const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2183
|
+
async function status(req) {
|
|
2184
|
+
if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
|
|
2185
|
+
const tx = await loadTx(req);
|
|
2186
|
+
if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2187
|
+
const ctx = { request: req, mode: tx.mode };
|
|
2188
|
+
let r;
|
|
2052
2189
|
try {
|
|
2053
|
-
|
|
2054
|
-
} catch {
|
|
2190
|
+
r = await client.session.poll(tx.sessionId, pollMs);
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
2193
|
+
await store.delete(tx.sessionId);
|
|
2194
|
+
return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2195
|
+
}
|
|
2196
|
+
onError(e, ctx);
|
|
2197
|
+
return json(502, { error: "eid_unavailable" });
|
|
2055
2198
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
typeof j.error_description === "string" ? j.error_description : void 0
|
|
2062
|
-
);
|
|
2199
|
+
const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
|
|
2200
|
+
if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
|
|
2201
|
+
if (r.endResult !== "OK") {
|
|
2202
|
+
await store.delete(tx.sessionId);
|
|
2203
|
+
return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
|
|
2063
2204
|
}
|
|
2064
|
-
if (
|
|
2065
|
-
|
|
2205
|
+
if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
|
|
2206
|
+
try {
|
|
2207
|
+
const out = await finish(req, tx, r, tx.mode === "qr" ? "QR" : "Notification");
|
|
2208
|
+
if (!out) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2209
|
+
return json(200, { state: "OK", redirect: out.redirect, data: out.data }, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
|
|
2210
|
+
} catch (e) {
|
|
2211
|
+
onError(e, ctx);
|
|
2212
|
+
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
|
|
2213
|
+
await store.delete(tx.sessionId);
|
|
2214
|
+
return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
|
|
2066
2215
|
}
|
|
2067
|
-
return j.id_token;
|
|
2068
2216
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
2081
|
-
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
2082
|
-
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
2083
|
-
const pick = (ks) => ks.find(
|
|
2084
|
-
(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")
|
|
2085
|
-
);
|
|
2086
|
-
const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
|
|
2087
|
-
if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
|
|
2088
|
-
const sig = Buffer.from(parts[2], "base64url");
|
|
2089
|
-
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");
|
|
2090
|
-
let ok = false;
|
|
2217
|
+
async function callback(req) {
|
|
2218
|
+
if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
|
|
2219
|
+
const q = new URL(req.url, "http://x").searchParams;
|
|
2220
|
+
const sid = q.get("sessionId") ?? "";
|
|
2221
|
+
const verifier = q.get("userChallengeVerifier") ?? "";
|
|
2222
|
+
const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
|
|
2223
|
+
const tx = await loadTx(req);
|
|
2224
|
+
if (!tx || tx.sessionId !== sid || tx.mode !== "web2app") return fail2(tx ? "unbound" : "expired");
|
|
2225
|
+
if (!verifier) return fail2("unbound");
|
|
2226
|
+
const ctx = { request: req, mode: tx.mode };
|
|
2227
|
+
let r;
|
|
2091
2228
|
try {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
key,
|
|
2102
|
-
sig,
|
|
2103
|
-
new TextEncoder().encode(parts[0] + "." + parts[1])
|
|
2104
|
-
);
|
|
2105
|
-
} catch {
|
|
2106
|
-
ok = false;
|
|
2229
|
+
r = await client.session.poll(tx.sessionId, 1e4);
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
onError(e, ctx);
|
|
2232
|
+
return fail2("eid_unavailable");
|
|
2233
|
+
}
|
|
2234
|
+
if (r.state !== "COMPLETE") return fail2("not_complete");
|
|
2235
|
+
if (r.endResult !== "OK") {
|
|
2236
|
+
await store.delete(tx.sessionId);
|
|
2237
|
+
return fail2(r.endResult ?? "UNKNOWN");
|
|
2107
2238
|
}
|
|
2108
|
-
if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
2109
|
-
const c = payload;
|
|
2110
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2111
|
-
if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
2112
|
-
const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
|
|
2113
|
-
if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
2114
|
-
if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
2115
|
-
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");
|
|
2116
|
-
if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
|
|
2117
|
-
if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
2118
|
-
if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
|
|
2119
|
-
return c;
|
|
2120
|
-
}
|
|
2121
|
-
keys(force) {
|
|
2122
|
-
const c = this.jwks;
|
|
2123
|
-
if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
|
|
2124
|
-
const keys = this.discover().then(async (d) => {
|
|
2125
|
-
const j = await this.getJson(d.jwks_uri);
|
|
2126
|
-
return Array.isArray(j?.keys) ? j.keys : [];
|
|
2127
|
-
});
|
|
2128
|
-
this.jwks = { at: Date.now(), keys };
|
|
2129
|
-
keys.catch(() => {
|
|
2130
|
-
if (this.jwks?.keys === keys) this.jwks = void 0;
|
|
2131
|
-
});
|
|
2132
|
-
return keys;
|
|
2133
|
-
}
|
|
2134
|
-
async getJson(url) {
|
|
2135
|
-
const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
2136
|
-
if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
|
|
2137
2239
|
try {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2240
|
+
const out = await finish(req, tx, r, "Web2App", verifier);
|
|
2241
|
+
if (!out) return fail2("expired");
|
|
2242
|
+
return redirect(out.redirect ?? successRedirect, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
onError(e, ctx);
|
|
2245
|
+
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
|
|
2246
|
+
await store.delete(tx.sessionId);
|
|
2247
|
+
return fail2("VALIDATION");
|
|
2141
2248
|
}
|
|
2142
2249
|
}
|
|
2143
|
-
async
|
|
2144
|
-
const
|
|
2250
|
+
async function handleRequest(req) {
|
|
2251
|
+
const seg = new URL(req.url, "http://x").pathname.replace(/\/+$/, "").split("/").pop();
|
|
2145
2252
|
try {
|
|
2146
|
-
return await
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
2150
|
-
});
|
|
2253
|
+
if (seg === "start") return await start(req);
|
|
2254
|
+
if (seg === "status") return await status(req);
|
|
2255
|
+
if (seg === "callback") return await callback(req);
|
|
2151
2256
|
} catch (e) {
|
|
2152
|
-
|
|
2153
|
-
if (
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
|
|
2257
|
+
onError(e, { request: req, mode: "qr" });
|
|
2258
|
+
if (e instanceof AuthenticationError || e instanceof ForbiddenError) return json(502, { error: "eid_unavailable" });
|
|
2259
|
+
return json(500, { error: "internal" });
|
|
2157
2260
|
}
|
|
2261
|
+
return json(404, { error: "not_found" });
|
|
2158
2262
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
}
|
|
2167
|
-
function formEncode(s) {
|
|
2168
|
-
return new URLSearchParams({ v: s }).toString().slice(2);
|
|
2169
|
-
}
|
|
2170
|
-
function timingSafeEqualStr(a, b) {
|
|
2171
|
-
const x = Buffer.from(a, "utf8");
|
|
2172
|
-
const y = Buffer.from(b, "utf8");
|
|
2173
|
-
return x.length === y.length && (0, import_node_crypto7.timingSafeEqual)(x, y);
|
|
2174
|
-
}
|
|
2175
|
-
function decodeJsonPart(s) {
|
|
2176
|
-
try {
|
|
2177
|
-
const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
|
|
2178
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
2179
|
-
} catch {
|
|
2180
|
-
return null;
|
|
2263
|
+
async function handle(req) {
|
|
2264
|
+
const headers = {};
|
|
2265
|
+
req.headers.forEach((v, k) => headers[k] = v);
|
|
2266
|
+
const body = req.method === "POST" ? await req.text() : void 0;
|
|
2267
|
+
const r = await handleRequest({ method: req.method, url: req.url, headers, body });
|
|
2268
|
+
const h = new Headers();
|
|
2269
|
+
for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
|
|
2270
|
+
return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
|
|
2181
2271
|
}
|
|
2182
|
-
}
|
|
2183
|
-
function callbackParams(input) {
|
|
2184
|
-
if (input instanceof URLSearchParams) return input;
|
|
2185
|
-
if (input instanceof URL) return input.searchParams;
|
|
2186
|
-
const s = input.trim();
|
|
2187
|
-
if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
|
|
2188
|
-
const i = s.indexOf("?");
|
|
2189
|
-
return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
|
|
2272
|
+
return { handleRequest, handle };
|
|
2190
2273
|
}
|
|
2191
2274
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2192
2275
|
0 && (module.exports = {
|
|
@@ -2198,13 +2281,8 @@ function callbackParams(input) {
|
|
|
2198
2281
|
DEFAULT_PDF_MAX_BYTES,
|
|
2199
2282
|
DEFAULT_PDF_TIMEOUT_MS,
|
|
2200
2283
|
DeviceLinkError,
|
|
2201
|
-
EID_ACR_ADVANCED,
|
|
2202
|
-
EID_ACR_QUALIFIED,
|
|
2203
|
-
EID_OIDC_ISSUER,
|
|
2204
2284
|
EidClient,
|
|
2205
2285
|
EidError,
|
|
2206
|
-
EidOidcClient,
|
|
2207
|
-
EidOidcError,
|
|
2208
2286
|
ForbiddenError,
|
|
2209
2287
|
Http,
|
|
2210
2288
|
NetworkError,
|
|
@@ -2223,15 +2301,16 @@ function callbackParams(input) {
|
|
|
2223
2301
|
acspV2Digest,
|
|
2224
2302
|
buildAcspV2Payload,
|
|
2225
2303
|
buildDeviceLink,
|
|
2304
|
+
createEidLogin,
|
|
2226
2305
|
deviceLink,
|
|
2227
2306
|
interactionsToBase64,
|
|
2228
2307
|
isMobileUserAgent,
|
|
2308
|
+
memoryLoginStore,
|
|
2229
2309
|
openOrShowQR,
|
|
2230
2310
|
parseRetryAfter,
|
|
2231
2311
|
parseSessionResult,
|
|
2232
2312
|
parseSubject,
|
|
2233
2313
|
personMatchesCertificate,
|
|
2234
|
-
pkceChallenge,
|
|
2235
2314
|
qrDeviceLinkTicker,
|
|
2236
2315
|
randomChallenge,
|
|
2237
2316
|
resolveBaseUrl,
|
|
@@ -2241,5 +2320,6 @@ function callbackParams(input) {
|
|
|
2241
2320
|
userChallengeOf,
|
|
2242
2321
|
verifyPayloadSignature,
|
|
2243
2322
|
verifyPrehashedSignature,
|
|
2244
|
-
verifyTokenCertificates
|
|
2323
|
+
verifyTokenCertificates,
|
|
2324
|
+
withReturnHint
|
|
2245
2325
|
});
|