@gerege-systems/eid-mongolia-sdk 0.15.0 → 0.15.2

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
@@ -3,6 +3,13 @@ import {
3
3
  isMobileUserAgent,
4
4
  openOrShowQR
5
5
  } from "./chunk-REWNE7PO.js";
6
+ import {
7
+ OID,
8
+ equal,
9
+ ocspRequest,
10
+ parseCertificate,
11
+ parseOCSPForCert
12
+ } from "./chunk-52O3T7DZ.js";
6
13
  import {
7
14
  ApiError,
8
15
  AuthenticationError,
@@ -33,6 +40,13 @@ var Http = class {
33
40
  post(path, body, timeoutMs) {
34
41
  return this.json("POST", path, JSON.stringify(body), "application/json", timeoutMs);
35
42
  }
43
+ /** {@link post} + HTTP статус (201 / 200-г ялгах: `POST /pdf/signatures`). */
44
+ async postWithStatus(path, body, timeoutMs) {
45
+ const op = `POST ${maskPath(path)}`;
46
+ const res = await this.send("POST", path, JSON.stringify(body), "application/json", timeoutMs);
47
+ const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, op));
48
+ return { status: res.status, body: text ? JSON.parse(text) : {} };
49
+ }
36
50
  get(path, timeoutMs) {
37
51
  return this.json("GET", path, void 0, void 0, timeoutMs);
38
52
  }
@@ -346,12 +360,32 @@ function registrationFromCompleted(done, extra) {
346
360
  if (extra.medium) out.medium = extra.medium;
347
361
  if (extra.sessionID) out.sessionID = extra.sessionID;
348
362
  if (done.hasPage && done.pageSha256) out.pageSha256 = done.pageSha256;
349
- if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
350
- if (extra.priorRevisionSha256?.length) out.priorRevisionSha256 = extra.priorRevisionSha256;
363
+ if (extra.priorRevisionSha256?.length) {
364
+ if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
365
+ out.priorRevisionSha256 = extra.priorRevisionSha256;
366
+ } else if (done.priorCmsSha256.length) {
367
+ out.priorCmsSha256 = done.priorCmsSha256;
368
+ const rev = done.priorRevisionSha256 ?? [];
369
+ if (rev.length === done.priorCmsSha256.length) {
370
+ out.priorSignatures = done.priorCmsSha256.map((c, i) => ({ cmsSha256: c, revisionSha256: rev[i] }));
371
+ }
372
+ }
351
373
  if (extra.onBehalfOf) out.onBehalfOf = extra.onBehalfOf;
352
374
  if (extra.signerEtsi) out.signerEtsi = extra.signerEtsi;
353
375
  return out;
354
376
  }
377
+ function priorSplits(n) {
378
+ const out = [];
379
+ const seen = /* @__PURE__ */ new Set();
380
+ for (let k = 1; k <= n; k++) {
381
+ seen.add((1 << k) - 1);
382
+ out.push(Array.from({ length: n }, (_, i) => i < k));
383
+ }
384
+ if (n <= 4) {
385
+ for (let m = 1; m < 1 << n; m++) if (!seen.has(m)) out.push(Array.from({ length: n }, (_, i) => (m & 1 << i) !== 0));
386
+ }
387
+ return out;
388
+ }
355
389
  var HEX64 = /^[0-9a-f]{64}$/;
356
390
  var MAX_CMS_BYTES = 28672;
357
391
  function buildRegisterBody(input) {
@@ -553,16 +587,38 @@ var SignApi = class {
553
587
  * `/verify` «бүртгэлгүй» гэнэ.
554
588
  */
555
589
  async registerSignature(input) {
556
- return this.registerBody(buildRegisterBody(input));
590
+ const body = buildRegisterBody(input);
591
+ const prior = input.priorSignatures ?? [];
592
+ if (input.priorRevisionSha256?.length || prior.length === 0 || prior.length !== input.revIndex - 1) return this.registerBody(body);
593
+ try {
594
+ return await this.registerBody(body);
595
+ } catch (first) {
596
+ if (!(first instanceof ApiError && first.code === "PRIOR_MISMATCH")) throw first;
597
+ for (const asRevision of priorSplits(prior.length)) {
598
+ const next = {
599
+ ...input,
600
+ priorSignatures: void 0,
601
+ priorCmsSha256: prior.filter((_, i) => !asRevision[i]).map((p) => p.cmsSha256),
602
+ priorRevisionSha256: prior.filter((_, i) => asRevision[i]).map((p) => p.revisionSha256)
603
+ };
604
+ try {
605
+ return await this.registerBody(buildRegisterBody(next));
606
+ } catch (e) {
607
+ if (!(e instanceof ApiError && e.code === "PRIOR_MISMATCH")) throw e;
608
+ }
609
+ }
610
+ throw first;
611
+ }
557
612
  }
558
613
  async registerBody(body) {
559
- const raw = await this.http.post(routePath(RP_ROUTES.pdfSignatures), body);
614
+ const { status, body: raw } = await this.http.postWithStatus(routePath(RP_ROUTES.pdfSignatures), body);
560
615
  const r = rec2(raw);
561
616
  return {
562
617
  docID: String(r.docID ?? body.docID),
563
618
  revIndex: Number(r.revIndex ?? 0),
564
619
  registeredAt: typeof r.registeredAt === "string" ? r.registeredAt : "",
565
620
  verifyUrl: String(r.verifyUrl ?? ""),
621
+ created: status === 201,
566
622
  raw
567
623
  };
568
624
  }
@@ -1450,13 +1506,20 @@ var ResponseValidator = class {
1450
1506
  * "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
1451
1507
  */
1452
1508
  async checkRevocationStatus(cert) {
1453
- if (!this.revocation) return "skipped";
1454
- const status = await this.revocation(cert, this.findIssuer(cert));
1509
+ return this.checkRevocationWith(cert, this.revocation, this.revocationMode);
1510
+ }
1511
+ /**
1512
+ * {@link checkRevocationStatus}-тэй ижил, гэхдээ TrustConfig-ийн биш өгсөн checker ба горимоор (issuer-ийг trust
1513
+ * pool-оос олно). checker алга бол "skipped"; горим анхдагч "hard-fail".
1514
+ */
1515
+ async checkRevocationWith(cert, checker, mode = "hard-fail") {
1516
+ if (!checker) return "skipped";
1517
+ const status = await checker(cert, this.findIssuer(cert));
1455
1518
  if (status === "good") return "good";
1456
1519
  if (status === "revoked") {
1457
1520
  throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
1458
1521
  }
1459
- if (this.revocationMode === "hard-fail") {
1522
+ if (mode === "hard-fail") {
1460
1523
  throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 revocation \u0442\u04E9\u043B\u04E9\u0432 \u0442\u043E\u0433\u0442\u043E\u043E\u0433\u0434\u0441\u043E\u043D\u0433\u04AF\u0439 (hard-fail)");
1461
1524
  }
1462
1525
  return "unchecked";
@@ -1772,6 +1835,76 @@ var EidClient = class {
1772
1835
  }
1773
1836
  };
1774
1837
 
1838
+ // src/ocsp.ts
1839
+ import { createHash as createHash4 } from "crypto";
1840
+ var CACHE_TTL_MS = 5 * 6e4;
1841
+ var CACHE_MAX = 1024;
1842
+ var MAX_BYTES = 16 << 10;
1843
+ var CLOCK_SKEW_MS = 5 * 6e4;
1844
+ var MAX_AGE_MS = 48 * 36e5;
1845
+ function ocspRevocationChecker(opts = {}) {
1846
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
1847
+ const timeoutMs = opts.timeoutMs ?? 2e3;
1848
+ const totalMs = Math.max(timeoutMs, 3e3);
1849
+ const now = opts.now ?? Date.now;
1850
+ const cache = /* @__PURE__ */ new Map();
1851
+ async function ask(leaf, issuer) {
1852
+ const body = await ocspRequest(leaf, issuer);
1853
+ const deadline = Date.now() + totalMs;
1854
+ for (const raw of leaf.ocspServers) {
1855
+ const left = deadline - Date.now();
1856
+ if (left <= 0) break;
1857
+ try {
1858
+ const u = new URL(raw);
1859
+ if (u.protocol !== "http:" && u.protocol !== "https:") continue;
1860
+ const res = await doFetch(u.toString(), {
1861
+ method: "POST",
1862
+ headers: { "Content-Type": "application/ocsp-request", Accept: "application/ocsp-response" },
1863
+ body,
1864
+ redirect: "manual",
1865
+ signal: AbortSignal.timeout(Math.min(timeoutMs, left))
1866
+ });
1867
+ if (res.status !== 200) continue;
1868
+ const der = new Uint8Array(await res.arrayBuffer());
1869
+ if (der.length > MAX_BYTES) continue;
1870
+ const resp = await parseOCSPForCert(der, leaf, issuer);
1871
+ const t = now();
1872
+ const rc = resp.certificate;
1873
+ if (rc && !equal(rc.raw, issuer.raw)) {
1874
+ if (!rc.extKeyUsage.includes(OID.kpOCSPSigning)) continue;
1875
+ if (t < rc.notBefore.getTime() || t > rc.notAfter.getTime()) continue;
1876
+ }
1877
+ const tu = resp.thisUpdate.date.getTime();
1878
+ if (tu > t + CLOCK_SKEW_MS || tu < t - MAX_AGE_MS) continue;
1879
+ if (resp.nextUpdate && resp.nextUpdate.getTime() < t) continue;
1880
+ return resp.status === "good" ? "good" : "revoked";
1881
+ } catch {
1882
+ }
1883
+ }
1884
+ return void 0;
1885
+ }
1886
+ return async (cert, issuer) => {
1887
+ if (!issuer) return "unknown";
1888
+ let leaf, iss;
1889
+ try {
1890
+ leaf = parseCertificate(new Uint8Array(cert.raw));
1891
+ iss = parseCertificate(new Uint8Array(issuer.raw));
1892
+ } catch {
1893
+ return "unknown";
1894
+ }
1895
+ const key = createHash4("sha256").update(issuer.raw).digest("hex").slice(0, 16) + ":" + cert.serialNumber;
1896
+ const exp = cache.get(key);
1897
+ if (exp !== void 0 && now() < exp) return "good";
1898
+ const status = await ask(leaf, iss);
1899
+ if (!status) return "unknown";
1900
+ if (status === "good") {
1901
+ if (cache.size >= CACHE_MAX) cache.clear();
1902
+ cache.set(key, now() + CACHE_TTL_MS);
1903
+ }
1904
+ return status;
1905
+ };
1906
+ }
1907
+
1775
1908
  // src/devicelinkv3.ts
1776
1909
  import { createHmac } from "crypto";
1777
1910
  var TYPES = ["QR", "Web2App", "App2App"];
@@ -1874,7 +2007,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1874
2007
  }
1875
2008
 
1876
2009
  // src/login.ts
1877
- import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
2010
+ import { createHash as createHash5, createHmac as createHmac2, randomBytes as randomBytes2, randomInt, randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
1878
2011
  function memoryLoginStore() {
1879
2012
  const m = /* @__PURE__ */ new Map();
1880
2013
  return {
@@ -1899,6 +2032,51 @@ function memoryLoginStore() {
1899
2032
  }
1900
2033
  };
1901
2034
  }
2035
+ var defaultRateLimit = Object.freeze({
2036
+ push: Object.freeze({ limit: 3, windowMs: 6e4 })
2037
+ });
2038
+ var RATE_KINDS = ["start", "push"];
2039
+ function windowLimiter(limit, windowMs) {
2040
+ const hits = /* @__PURE__ */ new Map();
2041
+ return (key) => {
2042
+ const now = Date.now();
2043
+ const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
2044
+ if (hits.size > 1e4) hits.clear();
2045
+ if (h.length >= limit) {
2046
+ hits.set(key, h);
2047
+ return false;
2048
+ }
2049
+ h.push(now);
2050
+ hits.set(key, h);
2051
+ return true;
2052
+ };
2053
+ }
2054
+ function buildRateLimit(opt) {
2055
+ const extra = typeof opt === "function" ? opt : void 0;
2056
+ const user = opt && typeof opt === "object" ? opt : {};
2057
+ for (const k of Object.keys(user)) {
2058
+ if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
2059
+ }
2060
+ const merged = { ...defaultRateLimit, ...user };
2061
+ const perKind = /* @__PURE__ */ new Map();
2062
+ for (const k of RATE_KINDS) {
2063
+ const r = merged[k];
2064
+ if (r === void 0 || r === false) continue;
2065
+ if (typeof r === "function") {
2066
+ perKind.set(k, r);
2067
+ continue;
2068
+ }
2069
+ if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
2070
+ throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
2071
+ }
2072
+ perKind.set(k, windowLimiter(r.limit, r.windowMs));
2073
+ }
2074
+ return async (key) => {
2075
+ const fn = perKind.get(key.slice(0, key.indexOf(":")));
2076
+ if (fn && !await fn(key)) return false;
2077
+ return extra ? extra(key) : true;
2078
+ };
2079
+ }
1902
2080
  var CALLBACK_PATH = "/auth/eid/callback";
1903
2081
  function forwardedClientIp(headers, trustProxy) {
1904
2082
  if (trustProxy === false || trustProxy === 0) return void 0;
@@ -1955,13 +2133,43 @@ function readCookie(h, name) {
1955
2133
  }
1956
2134
  return "";
1957
2135
  }
1958
- var sha256hex = (s) => createHash4("sha256").update(s).digest("hex");
2136
+ var sha256hex = (s) => createHash5("sha256").update(s).digest("hex");
1959
2137
  function safeEqualHex(a, b) {
1960
2138
  const x = Buffer.from(a, "hex");
1961
2139
  const y = Buffer.from(b, "hex");
1962
2140
  return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
1963
2141
  }
1964
- function toIdentity(sessionId, r, v, flowType) {
2142
+ function sleep(ms2, signal) {
2143
+ return new Promise((resolve) => {
2144
+ if (ms2 <= 0 || signal?.aborted) return resolve();
2145
+ const done = () => {
2146
+ clearTimeout(t);
2147
+ signal?.removeEventListener("abort", done);
2148
+ resolve();
2149
+ };
2150
+ const t = setTimeout(done, ms2);
2151
+ signal?.addEventListener("abort", done, { once: true });
2152
+ });
2153
+ }
2154
+ function isRefusal(e) {
2155
+ if (!(e instanceof ApiError) || e.status < 400 || e.status >= 500 || e.status === 401 || e.status === 403 || e.status === 429) {
2156
+ return false;
2157
+ }
2158
+ try {
2159
+ JSON.parse(e.body);
2160
+ return true;
2161
+ } catch {
2162
+ return false;
2163
+ }
2164
+ }
2165
+ var RevocationFailure = class extends ValidationError {
2166
+ constructor(message, revoked) {
2167
+ super(message);
2168
+ this.revoked = revoked;
2169
+ }
2170
+ revoked;
2171
+ };
2172
+ function toIdentity(sessionId, r, v, flowType, revocation) {
1965
2173
  const s = v.parsedSubject;
1966
2174
  const etsi = s.serialNumber ?? "";
1967
2175
  const m = /^PNOMN-([0-9]+)$/.exec(etsi);
@@ -1977,7 +2185,8 @@ function toIdentity(sessionId, r, v, flowType) {
1977
2185
  certificateLevel: v.certificateLevel,
1978
2186
  certificateDerB64: r.certificateDerB64 ?? "",
1979
2187
  flowType,
1980
- person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
2188
+ person: personMatchesCertificate(r.person, s) ? r.person ?? null : null,
2189
+ revocation
1981
2190
  };
1982
2191
  }
1983
2192
  function createEidLogin(opts) {
@@ -1998,17 +2207,29 @@ function createEidLogin(opts) {
1998
2207
  const successRedirect = opts.successRedirect ?? "/";
1999
2208
  const failureRedirect = opts.failureRedirect ?? "/";
2000
2209
  const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
2001
- const pushHits = /* @__PURE__ */ new Map();
2002
- const rateLimit = opts.rateLimit ?? ((key) => {
2003
- if (!key.startsWith("push:")) return true;
2004
- const now = Date.now();
2005
- const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
2006
- if (pushHits.size > 1e4) pushHits.clear();
2007
- if (hits.length >= 3) return false;
2008
- hits.push(now);
2009
- pushHits.set(key, hits);
2010
- return true;
2011
- });
2210
+ const rateLimit = buildRateLimit(opts.rateLimit);
2211
+ const decoyOn = opts.decoy !== false;
2212
+ const pushFloorMs = opts.pushFloorMs ?? 1e3;
2213
+ const revocation = opts.revocation === false ? void 0 : opts.revocation ?? ocspRevocationChecker();
2214
+ const revocationMode = opts.revocationMode ?? "soft-fail";
2215
+ const auditKey = opts.auditKey ?? randomBytes2(32);
2216
+ const pushKey = (idCode) => createHmac2("sha256", auditKey).update(`eid-push-audit:${idCode}`).digest("hex").slice(0, 32);
2217
+ async function emit(kind, sessionId, meta, ctx) {
2218
+ if (!opts.onEvent) return;
2219
+ try {
2220
+ await opts.onEvent({ kind, sessionId, meta });
2221
+ } catch (e) {
2222
+ onError(e, ctx);
2223
+ }
2224
+ }
2225
+ function endEvent(tx, endResult, ctx) {
2226
+ const kind = endResult?.startsWith("USER_REFUSED") ? "refused" : endResult === "TIMEOUT" ? "expired" : "failed";
2227
+ return emit(kind, tx.sessionId, { mode: tx.mode, endResult: endResult ?? "UNKNOWN" }, ctx);
2228
+ }
2229
+ function unverified(tx, e, ctx) {
2230
+ const reason = e instanceof RevocationFailure ? e.revoked ? "revoked" : "revocation_unavailable" : "validation";
2231
+ return emit("unverified", tx.sessionId, { mode: tx.mode, reason }, ctx);
2232
+ }
2012
2233
  const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
2013
2234
  const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
2014
2235
  const json = (status2, body, extra = {}) => ({
@@ -2099,11 +2320,18 @@ function createEidLogin(opts) {
2099
2320
  });
2100
2321
  }
2101
2322
  }
2323
+ return { ...await save(tx), appLink };
2324
+ }
2325
+ async function save(tx) {
2102
2326
  const binding = randomBytes2(32).toString("base64url");
2103
2327
  const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
2104
2328
  await store.set(full.sessionId, full, ttl);
2105
2329
  const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
2106
- return { tx: full, setCookie, appLink };
2330
+ return { tx: full, setCookie, appLink: void 0 };
2331
+ }
2332
+ function startDecoy() {
2333
+ const acsp = void 0;
2334
+ return save({ sessionId: randomUUID(), mode: "push", vc: String(randomInt(1e5)).padStart(5, "0"), acsp, decoy: true });
2107
2335
  }
2108
2336
  async function start(req) {
2109
2337
  const url = new URL(req.url, "http://x");
@@ -2133,18 +2361,36 @@ function createEidLogin(opts) {
2133
2361
  if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
2134
2362
  const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
2135
2363
  if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
2136
- const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
2364
+ const limited = async () => {
2365
+ if (mode === "push") await emit("push_limited", null, { mode, pushKey: pushKey(idCode) }, ctx);
2366
+ return json(429, { error: "rate_limited" }, { "Retry-After": "60" });
2367
+ };
2137
2368
  const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
2138
2369
  if (ip && !await rateLimit(`start:${ip}`)) return limited();
2139
2370
  if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
2371
+ const began = Date.now();
2140
2372
  let r;
2373
+ let err;
2374
+ let decoy = false;
2141
2375
  try {
2142
2376
  r = await begin(req, mode, idCode, lang);
2143
2377
  } catch (e) {
2144
- onError(e, ctx);
2145
- const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
2146
- return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
2378
+ err = e;
2147
2379
  }
2380
+ if (err !== void 0 && mode === "push" && decoyOn && isRefusal(err)) {
2381
+ const ae = err;
2382
+ onError(new Error(`eid-login: eID push-\u0438\u0439\u0433 \u0442\u0430\u0442\u0433\u0430\u043B\u0437\u0430\u0432 (${ae.status}${ae.code ? ` ${ae.code}` : ""}); decoy session-\u043E\u043E\u0440 \u0445\u0430\u0440\u0438\u0443\u043B\u0430\u0432`), ctx);
2383
+ err = void 0;
2384
+ r = await startDecoy();
2385
+ decoy = true;
2386
+ }
2387
+ if (mode === "push" && pushFloorMs > 0) await sleep(began + pushFloorMs - Date.now(), req.signal);
2388
+ if (err !== void 0 || !r) {
2389
+ onError(err, ctx);
2390
+ if (err instanceof ApiError && err.status === 429) return limited();
2391
+ return json(502, { error: "eid_unavailable" });
2392
+ }
2393
+ if (mode === "push") await emit("push_sent", r.tx.sessionId, { mode, pushKey: pushKey(idCode), decoy }, ctx);
2148
2394
  if (mode === "web2app") {
2149
2395
  return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
2150
2396
  }
@@ -2156,8 +2402,19 @@ function createEidLogin(opts) {
2156
2402
  }
2157
2403
  async function finish(req, tx, r, flow, verifier) {
2158
2404
  const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
2405
+ const ctx = { request: req, mode: tx.mode };
2406
+ let rev;
2407
+ let status2;
2408
+ const checker = revocation && (async (c, i) => status2 = await revocation(c, i));
2409
+ try {
2410
+ rev = await client.validator.checkRevocationWith(v.certificate, checker, revocationMode);
2411
+ } catch (e) {
2412
+ throw new RevocationFailure(`revocation: ${e instanceof Error ? e.message : String(e)}`, status2 === "revoked");
2413
+ }
2414
+ if (rev === "unchecked") onError(new Error("eid-login: OCSP \u0445\u0430\u0440\u0438\u0443\u0433\u04AF\u0439 \u2014 \u0446\u0443\u0446\u043B\u0430\u043B\u0442 \u0448\u0430\u043B\u0433\u0430\u043B\u0433\u04AF\u0439 \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u0432 (soft-fail)"), ctx);
2159
2415
  if (!await store.delete(tx.sessionId)) return void 0;
2160
- const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
2416
+ const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow, rev), ctx) ?? {};
2417
+ await emit("success", tx.sessionId, { mode: tx.mode, flow, revocation: rev }, ctx);
2161
2418
  return out;
2162
2419
  }
2163
2420
  async function status(req) {
@@ -2166,20 +2423,28 @@ function createEidLogin(opts) {
2166
2423
  if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2167
2424
  const ctx = { request: req, mode: tx.mode };
2168
2425
  let r;
2169
- try {
2170
- r = await client.session.poll(tx.sessionId, pollMs);
2171
- } catch (e) {
2172
- if (e instanceof ApiError && e.status === 404) {
2173
- await store.delete(tx.sessionId);
2174
- return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2426
+ if (tx.decoy) {
2427
+ await sleep(pollMs, req.signal);
2428
+ r = { state: "RUNNING" };
2429
+ } else {
2430
+ try {
2431
+ r = await client.session.poll(tx.sessionId, pollMs);
2432
+ } catch (e) {
2433
+ if (e instanceof ApiError && e.status === 404) {
2434
+ await store.delete(tx.sessionId);
2435
+ await emit("expired", tx.sessionId, { mode: tx.mode }, ctx);
2436
+ return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2437
+ }
2438
+ onError(e, ctx);
2439
+ return json(502, { error: "eid_unavailable" });
2175
2440
  }
2176
- onError(e, ctx);
2177
- return json(502, { error: "eid_unavailable" });
2178
2441
  }
2179
2442
  const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
2180
2443
  if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
2181
2444
  if (r.endResult !== "OK") {
2182
2445
  await store.delete(tx.sessionId);
2446
+ await endEvent(tx, r.endResult, ctx);
2447
+ if (tx.mode === "push" && r.endResult === "TIMEOUT") return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
2183
2448
  return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
2184
2449
  }
2185
2450
  if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
@@ -2191,6 +2456,7 @@ function createEidLogin(opts) {
2191
2456
  onError(e, ctx);
2192
2457
  if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
2193
2458
  await store.delete(tx.sessionId);
2459
+ await unverified(tx, e, ctx);
2194
2460
  return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
2195
2461
  }
2196
2462
  }
@@ -2201,9 +2467,12 @@ function createEidLogin(opts) {
2201
2467
  const verifier = q.get("userChallengeVerifier") ?? "";
2202
2468
  const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
2203
2469
  const tx = await loadTx(req);
2204
- if (!tx || tx.sessionId !== sid || tx.mode !== "web2app") return fail2(tx ? "unbound" : "expired");
2205
- if (!verifier) return fail2("unbound");
2470
+ if (!tx) return fail2("expired");
2206
2471
  const ctx = { request: req, mode: tx.mode };
2472
+ if (tx.sessionId !== sid || tx.mode !== "web2app" || !verifier) {
2473
+ await emit("flow_refused", tx.sessionId, { mode: tx.mode, reason: "unbound" }, ctx);
2474
+ return fail2("unbound");
2475
+ }
2207
2476
  let r;
2208
2477
  try {
2209
2478
  r = await client.session.poll(tx.sessionId, 1e4);
@@ -2214,6 +2483,7 @@ function createEidLogin(opts) {
2214
2483
  if (r.state !== "COMPLETE") return fail2("not_complete");
2215
2484
  if (r.endResult !== "OK") {
2216
2485
  await store.delete(tx.sessionId);
2486
+ await endEvent(tx, r.endResult, ctx);
2217
2487
  return fail2(r.endResult ?? "UNKNOWN");
2218
2488
  }
2219
2489
  try {
@@ -2224,6 +2494,7 @@ function createEidLogin(opts) {
2224
2494
  onError(e, ctx);
2225
2495
  if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
2226
2496
  await store.delete(tx.sessionId);
2497
+ await unverified(tx, e, ctx);
2227
2498
  return fail2("VALIDATION");
2228
2499
  }
2229
2500
  }
@@ -2253,7 +2524,7 @@ function createEidLogin(opts) {
2253
2524
  mode: "qr"
2254
2525
  });
2255
2526
  }
2256
- const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
2527
+ const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip, signal: req.signal });
2257
2528
  const h = new Headers();
2258
2529
  for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
2259
2530
  return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
@@ -2293,12 +2564,14 @@ export {
2293
2564
  buildDeviceLink,
2294
2565
  buildRegisterBody,
2295
2566
  createEidLogin,
2567
+ defaultRateLimit,
2296
2568
  deviceLink,
2297
2569
  deviceLinkInput,
2298
2570
  forwardedClientIp,
2299
2571
  interactionsToBase64,
2300
2572
  isMobileUserAgent,
2301
2573
  memoryLoginStore,
2574
+ ocspRevocationChecker,
2302
2575
  openOrShowQR,
2303
2576
  parseExpiresAt,
2304
2577
  parseRetryAfter,
package/dist/pades.cjs CHANGED
@@ -3173,12 +3173,20 @@ function signatureCMSList(doc) {
3173
3173
  if (v.kind() !== "dict" || v.key("Contents").isNull() || v.key("ByteRange").isNull()) continue;
3174
3174
  const t = v.key("Type").name();
3175
3175
  if (t !== "" && t !== "Sig") continue;
3176
+ let cms;
3176
3177
  try {
3177
3178
  const n = readNode(v.key("Contents").rawString(), 0);
3178
- out.push(n.tag === TAG.SEQUENCE ? n.raw : void 0);
3179
+ cms = n.tag === TAG.SEQUENCE ? n.raw : void 0;
3179
3180
  } catch {
3180
- out.push(void 0);
3181
+ cms = void 0;
3181
3182
  }
3183
+ let end;
3184
+ const br = v.key("ByteRange");
3185
+ if (br.len() === 4 && br.index(2).kind() === "int" && br.index(3).kind() === "int") {
3186
+ const e = br.index(2).int64() + br.index(3).int64();
3187
+ if (e > 0 && e <= doc.length) end = e;
3188
+ }
3189
+ out.push({ cms, end });
3182
3190
  }
3183
3191
  return out;
3184
3192
  }
@@ -3489,9 +3497,13 @@ async function complete(state, signature, opts = {}) {
3489
3497
  }
3490
3498
  if (out.length > MAX_OUTPUT_BYTES) throw new PadesError("PDF_TOO_LARGE", "\u0433\u0430\u0440\u0430\u043B\u0442 10 MiB-\u0430\u0430\u0441 \u0438\u0445");
3491
3499
  const prior = [];
3500
+ const priorRev = [];
3492
3501
  try {
3493
3502
  const list = signatureCMSList(signed);
3494
- for (const c of list.slice(0, -1)) prior.push(c ? toHex(await sha256(c)) : "");
3503
+ for (const s of list.slice(0, -1)) {
3504
+ prior.push(s.cms ? toHex(await sha256(s.cms)) : "");
3505
+ priorRev.push(s.end !== void 0 ? toHex(await sha256(signed.subarray(0, s.end))) : "");
3506
+ }
3495
3507
  } catch (e) {
3496
3508
  throw new PadesError("SELF_VALIDATION_FAILED", "\u0433\u0430\u0440\u0430\u043B\u0442\u044B\u0433 \u0437\u0430\u0434\u0430\u043B\u0436 \u0447\u0430\u0434\u0441\u0430\u043D\u0433\u04AF\u0439", { cause: e });
3497
3509
  }
@@ -3506,6 +3518,7 @@ async function complete(state, signature, opts = {}) {
3506
3518
  revIndex: st.revIndex,
3507
3519
  sigRevSha256: toHex(await sha256(signed)),
3508
3520
  priorCmsSha256: prior,
3521
+ priorRevisionSha256: priorRev,
3509
3522
  signerCert: cert.raw
3510
3523
  };
3511
3524
  }
package/dist/pades.d.cts CHANGED
@@ -103,6 +103,11 @@ interface PadesCompleted {
103
103
  sigRevSha256: string;
104
104
  /** Өмнөх гарын үсгүүдийн CMS-ийн SHA-256 (hex), баримтын дарааллаар. */
105
105
  priorCmsSha256: string[];
106
+ /**
107
+ * Өмнөх гарын үсгүүдийн revision-ий SHA-256 (hex) — `sha256(doc[0 : ByteRange[2]+ByteRange[3]])`, `priorCmsSha256`-тай
108
+ * ижил дарааллаар. CA горим A-ийн (CA угсарсан) мөрийг үүгээр таньдаг (`priorRevisionSha256`).
109
+ */
110
+ priorRevisionSha256: string[];
106
111
  /** Зурагчийн гэрчилгээ (DER). */
107
112
  signerCert: Uint8Array;
108
113
  }
package/dist/pades.d.ts CHANGED
@@ -103,6 +103,11 @@ interface PadesCompleted {
103
103
  sigRevSha256: string;
104
104
  /** Өмнөх гарын үсгүүдийн CMS-ийн SHA-256 (hex), баримтын дарааллаар. */
105
105
  priorCmsSha256: string[];
106
+ /**
107
+ * Өмнөх гарын үсгүүдийн revision-ий SHA-256 (hex) — `sha256(doc[0 : ByteRange[2]+ByteRange[3]])`, `priorCmsSha256`-тай
108
+ * ижил дарааллаар. CA горим A-ийн (CA угсарсан) мөрийг үүгээр таньдаг (`priorRevisionSha256`).
109
+ */
110
+ priorRevisionSha256: string[];
106
111
  /** Зурагчийн гэрчилгээ (DER). */
107
112
  signerCert: Uint8Array;
108
113
  }