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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ export const eidLogin = createEidLogin({
72
72
  };
73
73
  },
74
74
  // store: redisStore, // олон instance / serverless бол (анхдагч — процессын санах ой)
75
- // rateLimit: (key) => redisLimiter.allow(key), // "start:<ip>", "push:<idCode>"; анхдагч push 3/мин нэг дугаарт
75
+ // rateLimit: { start: (key) => redisLimiter.allow(key) }, // анхдагчтай нэгтгэгдэнэ (push 3/мин хэвээр) — доор «Хязгаарлалт»
76
76
  trustProxy: 1, // handle(Request)-д клиент IP: nginx/LB нэг hop бол 1 (доор «Клиент IP»)
77
77
  });
78
78
  ```
@@ -95,8 +95,27 @@ export const POST = (req: Request) => eidLogin.handle(req);
95
95
  Vercel `req.headers.get("x-real-ip")`). `trustProxy`-оос давуу.
96
96
 
97
97
  Аль нь ч үгүй бол IP-ээр хязгаарлахгүй (эхний хүсэлтэд `onError`-оор нэг удаа анхааруулна). `handleRequest`-д `req.ip`
98
- (Express `req.ip`, `app.set("trust proxy", …)`-тэй) шууд хэрэглэгдэнэ. Анхдагч `rateLimit` зөвхөн `push:`-ийг хязгаарладаг —
99
- `start:<ip>`-ийг өөрийн `rateLimit`-ээр (Redis г.м.) хязгаарлана. Go: `eidlogin.Config.ClientIP` (`eidlogin.ForwardedClientIP(n)`) — ижил дүрэм.
98
+ (Express `req.ip`, `app.set("trust proxy", …)`-тэй) шууд хэрэглэгдэнэ. Go: `eidlogin.Config.ClientIP` (`eidlogin.ForwardedClientIP(n)`) — ижил дүрэм.
99
+
100
+ **Хязгаарлалт** (`rateLimit`). Түлхүүр: `start:<ip>` (IP мэдэгдэж байвал), `push:<idCode>`; татгалзвал 429 + `Retry-After: 60`.
101
+ Анхдагч `defaultRateLimit` = `{ push: { limit: 3, windowMs: 60_000 } }` — нэг дугаарт минутад 3 мэдэгдэл (MFA fatigue);
102
+ `start`-ийг анхдагчаар хязгаарладаггүй.
103
+
104
+ - **Объект** `{ start?, push? }` — анхдагчтай **нэгтгэгдэнэ**: өгсөн төрөл анхдагчийг дарна, өгөөгүй нь анхдагчаараа үлдэнэ.
105
+ Утга: `{ limit, windowMs }` (процессын санах ойн гулсах цонх), `(key) => boolean | Promise<boolean>` (Redis г.м.),
106
+ эсвэл `false` (тухайн төрлийг хязгаарлахгүй). Өөр түлхүүр → `createEidLogin` throw.
107
+ - **Функц** `(key) => …` — бүх түлхүүрт дуудагдана, анхдагч хязгаар **дээр нэмэгдэнэ** (хоёулаа зөвшөөрөх ёстой).
108
+ Анхдагчийг сулруулах/солих бол объектоор тухайн төрлийг өг (`{ push: … }`).
109
+
110
+ ```ts
111
+ import { defaultRateLimit } from "@gerege-systems/eid-mongolia-sdk";
112
+ rateLimit: { start: { limit: 20, windowMs: 60_000 } }, // push 3/мин хэвээр
113
+ rateLimit: { ...defaultRateLimit, start: (k) => redis.allow(k) }, // ижил — анхдагчийг илэрхий бичсэн
114
+ rateLimit: { push: (k) => redis.allow(k, 3, 60) }, // олон instance: push-ийг Redis-ээр солино
115
+ ```
116
+
117
+ Go: `eidlogin.Config.RateLimits` (`map[string]eidlogin.RateRule{"start": {Max: 20, Window: time.Minute}}`,
118
+ `eidlogin.DefaultRateLimit()`) — ижил нэгтгэл; `Config.RateLimit` функц мөн анхдагч дээр нэмэгдэнэ.
100
119
 
101
120
  Express:
102
121
 
@@ -128,21 +147,39 @@ app.use("/auth/eid", express.json(), async (req, res) => {
128
147
  **Дизайн, theme.** Дизайн нь eID нэвтрэх хуудсынх (лого, өнгө). Фонт нь хуудаснаас өвлөгдөнө (товч, цонх, input бүгд).
129
148
  `theme="auto"` үед дараалал: хуудасны `html[data-theme="dark|light"]` → `html.dark` / `html.light` → OS
130
149
  (`prefers-color-scheme`); хуудас theme солиход (MutationObserver) шууд дагана. `theme="light|dark"` нь хуудсыг давна.
131
- Өнгө, радиусыг CSS хувьсагчаар (элемент дээр — shadow-ийн `:host`-оос давуу) дарна:
150
+ Өнгө, радиусыг CSS хувьсагчаар дарна. `--eid-*` нь зөвхөн **оролт** (shadow дотор тодорхойлогддоггүй) тул хуудасны
151
+ аль ч түвшинд (`:root`, эцэг элемент, `eid-login-button`) өгсөн утга өвлөгдөж **light ба dark хоёуланд** үйлчилнэ;
152
+ `--eid-<нэр>-dark` нь зөвхөн dark-д, `--eid-<нэр>`-ээс давуу. Аль нь ч үгүй бол eID-ийн анхдагч (доорх хүснэгт) — fallback л.
153
+ Dark-ийн дараалал: `--eid-<нэр>-dark` → `--eid-<нэр>` → dark анхдагч.
132
154
 
133
155
  ```css
134
- eid-login-button {
135
- --eid-accent: #0b5cff; --eid-accent-fg: #fff; /* үндсэн товч, QR-ийн алхмын тоо (--eid-accent-subtle, --eid-accent-subtle-fg) */
136
- --eid-bg: #fff; --eid-bg-subtle: …; --eid-bg-muted: …; /* цонх, hover */
137
- --eid-fg: …; --eid-fg-muted: …; --eid-fg-subtle: …; /* текст */
138
- --eid-border: …; --eid-border-strong: …; --eid-ring: …; /* хүрээ, focus */
139
- --eid-warning-bg: …; --eid-warning-border: …; --eid-warning-fg: …; --eid-danger-fg: …;
156
+ :root { /* эсвэл eid-login-button { … } */
157
+ --eid-accent: var(--brand); /* хоёр theme-д */
158
+ --eid-accent-dark: var(--brand-400); /* зөвхөн dark-д (өгөхгүй бол --eid-accent) */
140
159
  --eid-radius: 6px; --eid-radius-lg: 8px;
141
160
  }
142
- html[data-theme="dark"] eid-login-button { --eid-accent: …; } /* dark-д өөр өнгө бол */
143
- eid-login-button::part(button) { width: 100%; } /* part: button, backdrop, dialog */
161
+ eid-login-button::part(button) { width: 100%; } /* part: button, backdrop, dialog */
144
162
  ```
145
163
 
164
+ | Хувьсагч (`-dark` хувилбартай) | Юунд | Light анхдагч | Dark анхдагч |
165
+ |---|---|---|---|
166
+ | `--eid-accent` | үндсэн товч | `oklch(0.534 0.2 259)` | `oklch(0.64 0.15 259)` |
167
+ | `--eid-accent-fg` | үндсэн товчны текст | `#fff` | `hsl(229 50% 6%)` |
168
+ | `--eid-accent-subtle` | алхмын дугаарын дэвсгэр | `oklch(0.97 0.03 259)` | `oklch(0.24 0.06 259)` |
169
+ | `--eid-accent-subtle-fg` | алхмын дугаар | `oklch(0.474 0.19 259)` | `oklch(0.82 0.1 259)` |
170
+ | `--eid-ring` | focus | `oklch(0.604 0.2 259)` | `oklch(0.68 0.15 259)` |
171
+ | `--eid-bg` | цонх, outline товч, input | `hsl(0 0% 100%)` | `hsl(229 50% 6%)` |
172
+ | `--eid-bg-subtle` | — (нөөц) | `hsl(210 40% 98%)` | `hsl(222 47% 9%)` |
173
+ | `--eid-bg-muted` | hover | `hsl(210 40% 96%)` | `hsl(217 33% 13%)` |
174
+ | `--eid-fg` | текст | `hsl(222 47% 11%)` | `hsl(210 40% 98%)` |
175
+ | `--eid-fg-muted` | хоёрдогч текст | `hsl(215 19% 35%)` | `hsl(213 27% 84%)` |
176
+ | `--eid-fg-subtle` | тайлбар, placeholder | `hsl(215 16% 45%)` | `hsl(215 20% 65%)` |
177
+ | `--eid-border` | цонх, QR, тусгаарлагч | `hsl(214 32% 91%)` | `hsl(217 33% 17%)` |
178
+ | `--eid-border-strong` | outline товч, input | `hsl(213 27% 84%)` | `hsl(215 25% 27%)` |
179
+ | `--eid-warning-bg` / `-border` / `-fg` | сүлжээний анхааруулга | `hsl(48 100% 96%)` / `hsl(45 95% 78%)` / `hsl(23 75% 33%)` | `hsl(20 60% 10%)` / `hsl(22 70% 26%)` / `hsl(40 92% 65%)` |
180
+ | `--eid-danger-fg` | алдаа | `hsl(0 74% 42%)` | `hsl(0 91% 71%)` |
181
+ | `--eid-radius` / `--eid-radius-lg` (theme-гүй) | булан | `6px` / `8px` | — |
182
+
146
183
  **eID аппын доод хувилбар.** `flowType` бэхлэлт (device link v3) ба Web2App-ийн `userChallengeVerifier` нь
147
184
  **iOS 2.2.3 (build 59)**, **Android 2.2.3 (versionCode 60)**-аас эхэлсэн. eID сервер 2026-10-07-оос legacy холбоосыг
148
185
  бүрэн хассан (v3-гүй session-д утас `DEVICE_LINK_REQUIRED`) бөгөөд доод хувилбарыг `/mobile/app-config`-оор албадан
package/dist/button.cjs CHANGED
@@ -465,87 +465,97 @@ var T = {
465
465
  minute: "1 minute left"
466
466
  }
467
467
  };
468
+ var EID_COLOR_TOKENS = [
469
+ ["bg", "hsl(0 0% 100%)", "hsl(229 50% 6%)"],
470
+ ["bg-subtle", "hsl(210 40% 98%)", "hsl(222 47% 9%)"],
471
+ ["bg-muted", "hsl(210 40% 96%)", "hsl(217 33% 13%)"],
472
+ ["fg", "hsl(222 47% 11%)", "hsl(210 40% 98%)"],
473
+ ["fg-muted", "hsl(215 19% 35%)", "hsl(213 27% 84%)"],
474
+ ["fg-subtle", "hsl(215 16% 45%)", "hsl(215 20% 65%)"],
475
+ ["border", "hsl(214 32% 91%)", "hsl(217 33% 17%)"],
476
+ ["border-strong", "hsl(213 27% 84%)", "hsl(215 25% 27%)"],
477
+ ["accent", "oklch(0.534 0.2 259)", "oklch(0.64 0.15 259)"],
478
+ ["accent-fg", "#fff", "hsl(229 50% 6%)"],
479
+ ["accent-subtle", "oklch(0.97 0.03 259)", "oklch(0.24 0.06 259)"],
480
+ ["accent-subtle-fg", "oklch(0.474 0.19 259)", "oklch(0.82 0.1 259)"],
481
+ ["ring", "oklch(0.604 0.2 259)", "oklch(0.68 0.15 259)"],
482
+ ["warning-bg", "hsl(48 100% 96%)", "hsl(20 60% 10%)"],
483
+ ["warning-border", "hsl(45 95% 78%)", "hsl(22 70% 26%)"],
484
+ ["warning-fg", "hsl(23 75% 33%)", "hsl(40 92% 65%)"],
485
+ ["danger-fg", "hsl(0 74% 42%)", "hsl(0 91% 71%)"]
486
+ ];
487
+ var lightVars = EID_COLOR_TOKENS.map(([n, l]) => `--_eid-${n}: var(--eid-${n}, ${l});`).join("\n ");
488
+ var darkVars = EID_COLOR_TOKENS.map(([n, , d]) => `--_eid-${n}: var(--eid-${n}-dark, var(--eid-${n}, ${d}));`).join("\n ");
468
489
  var CSS_BASE = `
469
- :host { display: inline-block;
470
- --eid-bg: hsl(0 0% 100%); --eid-bg-subtle: hsl(210 40% 98%); --eid-bg-muted: hsl(210 40% 96%);
471
- --eid-fg: hsl(222 47% 11%); --eid-fg-muted: hsl(215 19% 35%); --eid-fg-subtle: hsl(215 16% 45%);
472
- --eid-border: hsl(214 32% 91%); --eid-border-strong: hsl(213 27% 84%);
473
- --eid-accent: oklch(0.534 0.2 259); --eid-accent-fg: #fff; --eid-accent-subtle: oklch(0.97 0.03 259);
474
- --eid-accent-subtle-fg: oklch(0.474 0.19 259); --eid-ring: oklch(0.604 0.2 259);
475
- --eid-warning-bg: hsl(48 100% 96%); --eid-warning-border: hsl(45 95% 78%); --eid-warning-fg: hsl(23 75% 33%);
476
- --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; color-scheme: light; }
490
+ :host { display: inline-block; font-family: inherit; color-scheme: light;
491
+ ${lightVars}
492
+ --_eid-radius: var(--eid-radius, 6px); --_eid-radius-lg: var(--eid-radius-lg, 8px); }
477
493
  button, input { font-family: inherit; }
478
494
  `;
479
495
  var DARK_CSS = `:host { color-scheme: dark;
480
- --eid-bg: hsl(229 50% 6%); --eid-bg-subtle: hsl(222 47% 9%); --eid-bg-muted: hsl(217 33% 13%);
481
- --eid-fg: hsl(210 40% 98%); --eid-fg-muted: hsl(213 27% 84%); --eid-fg-subtle: hsl(215 20% 65%);
482
- --eid-border: hsl(217 33% 17%); --eid-border-strong: hsl(215 25% 27%);
483
- --eid-accent: oklch(0.64 0.15 259); --eid-accent-fg: hsl(229 50% 6%); --eid-accent-subtle: oklch(0.24 0.06 259);
484
- --eid-accent-subtle-fg: oklch(0.82 0.1 259); --eid-ring: oklch(0.68 0.15 259);
485
- --eid-warning-bg: hsl(20 60% 10%); --eid-warning-border: hsl(22 70% 26%); --eid-warning-fg: hsl(40 92% 65%);
486
- --eid-danger-fg: hsl(0 91% 71%); }
496
+ ${darkVars} }
487
497
  `;
488
498
  var CSS_REST = `
489
499
  *, *::before, *::after { box-sizing: border-box; }
490
500
  [hidden] { display: none !important; }
491
501
  h2, p, ol { margin: 0; }
492
- :focus-visible { outline: 2px solid var(--eid-ring); outline-offset: 2px; }
502
+ :focus-visible { outline: 2px solid var(--_eid-ring); outline-offset: 2px; }
493
503
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
494
504
  .btn { display: inline-flex; width: 100%; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 20px;
495
- border-radius: var(--eid-radius); font-family: inherit; font-weight: 500; font-size: 1rem; line-height: 1.25; text-decoration: none; border: 1px solid transparent; cursor: pointer;
505
+ border-radius: var(--_eid-radius); font-family: inherit; font-weight: 500; font-size: 1rem; line-height: 1.25; text-decoration: none; border: 1px solid transparent; cursor: pointer;
496
506
  transition: background-color 160ms cubic-bezier(0,0,0.2,1); }
497
507
  .btn:disabled { opacity: 0.6; cursor: progress; }
498
- .btn-primary { background: var(--eid-accent); color: var(--eid-accent-fg); }
499
- .btn-primary:hover { background: color-mix(in oklab, var(--eid-accent) 88%, black); }
500
- .btn-outline { background: var(--eid-bg); color: var(--eid-fg); border-color: var(--eid-border-strong); }
501
- .btn-outline:hover { background: var(--eid-bg-muted); }
508
+ .btn-primary { background: var(--_eid-accent); color: var(--_eid-accent-fg); }
509
+ .btn-primary:hover { background: color-mix(in oklab, var(--_eid-accent) 88%, black); }
510
+ .btn-outline { background: var(--_eid-bg); color: var(--_eid-fg); border-color: var(--_eid-border-strong); }
511
+ .btn-outline:hover { background: var(--_eid-bg-muted); }
502
512
  .btn-sm { width: auto; min-height: 36px; padding: 0 14px; font-size: 0.8125rem; }
503
513
  .trigger { width: auto; }
504
514
  .trigger img { width: 20px; height: 20px; border-radius: 4px; }
505
515
  .backdrop { position: fixed; inset: 0; z-index: 2147483000; background: hsl(222 47% 11% / 0.5); display: flex; align-items: center;
506
516
  justify-content: center; padding: 16px; overflow-y: auto; }
507
- .dialog { position: relative; width: 100%; max-width: 26rem; max-height: calc(100dvh - 32px); overflow-y: auto; background: var(--eid-bg);
508
- color: var(--eid-fg); border: 1px solid var(--eid-border); border-radius: var(--eid-radius-lg); padding: 24px; font-size: 0.875rem; line-height: 1.5rem; }
517
+ .dialog { position: relative; width: 100%; max-width: 26rem; max-height: calc(100dvh - 32px); overflow-y: auto; background: var(--_eid-bg);
518
+ color: var(--_eid-fg); border: 1px solid var(--_eid-border); border-radius: var(--_eid-radius-lg); padding: 24px; font-size: 0.875rem; line-height: 1.5rem; }
509
519
  @media (max-width: 639px) { .dialog { padding: 16px; } }
510
520
  .head { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
511
- .head img { width: 28px; height: 28px; border-radius: var(--eid-radius); }
521
+ .head img { width: 28px; height: 28px; border-radius: var(--_eid-radius); }
512
522
  .head h2 { flex: 1; font-size: 1.125rem; line-height: 1.75rem; font-weight: 600; }
513
- .x { width: 36px; height: 36px; border: 0; border-radius: var(--eid-radius); background: transparent; color: var(--eid-fg-muted); cursor: pointer; font-size: 1.25rem; line-height: 1; }
514
- .x:hover { background: var(--eid-bg-muted); color: var(--eid-fg); }
515
- .qr { position: relative; width: 256px; max-width: 100%; aspect-ratio: 1; margin: 0 auto; border: 1px solid var(--eid-border);
516
- border-radius: var(--eid-radius-lg); background: #fff; overflow: hidden; }
523
+ .x { width: 36px; height: 36px; border: 0; border-radius: var(--_eid-radius); background: transparent; color: var(--_eid-fg-muted); cursor: pointer; font-size: 1.25rem; line-height: 1; }
524
+ .x:hover { background: var(--_eid-bg-muted); color: var(--_eid-fg); }
525
+ .qr { position: relative; width: 256px; max-width: 100%; aspect-ratio: 1; margin: 0 auto; border: 1px solid var(--_eid-border);
526
+ border-radius: var(--_eid-radius-lg); background: #fff; overflow: hidden; }
517
527
  .qr svg { display: block; width: 100%; height: 100%; }
518
528
  .qr.stale svg { opacity: 0.12; }
519
529
  .qr-stale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
520
530
  color: hsl(222 47% 11%); font-weight: 500; }
521
- .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--eid-fg-subtle); }
522
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--eid-fg-muted); }
531
+ .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
532
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--_eid-fg-muted); }
523
533
  .steps { margin-top: 16px; padding: 0; list-style: none; counter-reset: st; display: grid; gap: 8px; }
524
534
  .steps li { counter-increment: st; display: flex; align-items: center; gap: 12px; }
525
- .steps li::before { content: counter(st); flex: none; width: 24px; height: 24px; border-radius: 9999px; background: var(--eid-accent-subtle);
526
- color: var(--eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
527
- .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--eid-fg-subtle); font-size: 0.8125rem; }
528
- .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--eid-border); }
535
+ .steps li::before { content: counter(st); flex: none; width: 24px; height: 24px; border-radius: 9999px; background: var(--_eid-accent-subtle);
536
+ color: var(--_eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
537
+ .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--_eid-fg-subtle); font-size: 0.8125rem; }
538
+ .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--_eid-border); }
529
539
  .field { display: grid; gap: 8px; }
530
540
  .field label { font-weight: 500; }
531
- .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--eid-border-strong); border-radius: var(--eid-radius);
532
- background: var(--eid-bg); color: var(--eid-fg); font-family: inherit; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0.02em; text-transform: uppercase; }
533
- .field input::placeholder { color: var(--eid-fg-subtle); text-transform: none; }
534
- .field input[aria-invalid="true"] { border-color: var(--eid-danger-fg); }
535
- .hint { font-size: 0.8125rem; color: var(--eid-fg-subtle); }
536
- .err { font-size: 0.8125rem; color: var(--eid-danger-fg); font-weight: 500; }
541
+ .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--_eid-border-strong); border-radius: var(--_eid-radius);
542
+ background: var(--_eid-bg); color: var(--_eid-fg); font-family: inherit; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0.02em; text-transform: uppercase; }
543
+ .field input::placeholder { color: var(--_eid-fg-subtle); text-transform: none; }
544
+ .field input[aria-invalid="true"] { border-color: var(--_eid-danger-fg); }
545
+ .hint { font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
546
+ .err { font-size: 0.8125rem; color: var(--_eid-danger-fg); font-weight: 500; }
537
547
  .push { margin-top: 16px; }
538
548
  .center { text-align: center; }
539
549
  .vc { font: 600 2.25rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.16em; padding-left: 0.16em; }
540
550
  .vc:focus, h2:focus { outline: none; }
541
- .text { margin-top: 12px; color: var(--eid-fg-muted); }
542
- .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--eid-fg-muted); }
551
+ .text { margin-top: 12px; color: var(--_eid-fg-muted); }
552
+ .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--_eid-fg-muted); }
543
553
  .end .btn, .center .btn { margin-top: 24px; }
544
554
  .spinner { width: 16px; height: 16px; flex: none; border: 2px solid currentColor; border-right-color: transparent; border-radius: 9999px; animation: spin 0.8s linear infinite; }
545
555
  @keyframes spin { to { transform: rotate(360deg); } }
546
- .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--eid-warning-border); border-radius: var(--eid-radius); background: var(--eid-warning-bg);
547
- color: var(--eid-warning-fg); font-size: 0.8125rem; }
548
- .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--eid-fg-subtle); }
556
+ .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--_eid-warning-border); border-radius: var(--_eid-radius); background: var(--_eid-warning-bg);
557
+ color: var(--_eid-warning-fg); font-size: 0.8125rem; }
558
+ .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
549
559
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
550
560
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
551
561
  `;
package/dist/button.js CHANGED
@@ -436,87 +436,97 @@ var T = {
436
436
  minute: "1 minute left"
437
437
  }
438
438
  };
439
+ var EID_COLOR_TOKENS = [
440
+ ["bg", "hsl(0 0% 100%)", "hsl(229 50% 6%)"],
441
+ ["bg-subtle", "hsl(210 40% 98%)", "hsl(222 47% 9%)"],
442
+ ["bg-muted", "hsl(210 40% 96%)", "hsl(217 33% 13%)"],
443
+ ["fg", "hsl(222 47% 11%)", "hsl(210 40% 98%)"],
444
+ ["fg-muted", "hsl(215 19% 35%)", "hsl(213 27% 84%)"],
445
+ ["fg-subtle", "hsl(215 16% 45%)", "hsl(215 20% 65%)"],
446
+ ["border", "hsl(214 32% 91%)", "hsl(217 33% 17%)"],
447
+ ["border-strong", "hsl(213 27% 84%)", "hsl(215 25% 27%)"],
448
+ ["accent", "oklch(0.534 0.2 259)", "oklch(0.64 0.15 259)"],
449
+ ["accent-fg", "#fff", "hsl(229 50% 6%)"],
450
+ ["accent-subtle", "oklch(0.97 0.03 259)", "oklch(0.24 0.06 259)"],
451
+ ["accent-subtle-fg", "oklch(0.474 0.19 259)", "oklch(0.82 0.1 259)"],
452
+ ["ring", "oklch(0.604 0.2 259)", "oklch(0.68 0.15 259)"],
453
+ ["warning-bg", "hsl(48 100% 96%)", "hsl(20 60% 10%)"],
454
+ ["warning-border", "hsl(45 95% 78%)", "hsl(22 70% 26%)"],
455
+ ["warning-fg", "hsl(23 75% 33%)", "hsl(40 92% 65%)"],
456
+ ["danger-fg", "hsl(0 74% 42%)", "hsl(0 91% 71%)"]
457
+ ];
458
+ var lightVars = EID_COLOR_TOKENS.map(([n, l]) => `--_eid-${n}: var(--eid-${n}, ${l});`).join("\n ");
459
+ var darkVars = EID_COLOR_TOKENS.map(([n, , d]) => `--_eid-${n}: var(--eid-${n}-dark, var(--eid-${n}, ${d}));`).join("\n ");
439
460
  var CSS_BASE = `
440
- :host { display: inline-block;
441
- --eid-bg: hsl(0 0% 100%); --eid-bg-subtle: hsl(210 40% 98%); --eid-bg-muted: hsl(210 40% 96%);
442
- --eid-fg: hsl(222 47% 11%); --eid-fg-muted: hsl(215 19% 35%); --eid-fg-subtle: hsl(215 16% 45%);
443
- --eid-border: hsl(214 32% 91%); --eid-border-strong: hsl(213 27% 84%);
444
- --eid-accent: oklch(0.534 0.2 259); --eid-accent-fg: #fff; --eid-accent-subtle: oklch(0.97 0.03 259);
445
- --eid-accent-subtle-fg: oklch(0.474 0.19 259); --eid-ring: oklch(0.604 0.2 259);
446
- --eid-warning-bg: hsl(48 100% 96%); --eid-warning-border: hsl(45 95% 78%); --eid-warning-fg: hsl(23 75% 33%);
447
- --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; color-scheme: light; }
461
+ :host { display: inline-block; font-family: inherit; color-scheme: light;
462
+ ${lightVars}
463
+ --_eid-radius: var(--eid-radius, 6px); --_eid-radius-lg: var(--eid-radius-lg, 8px); }
448
464
  button, input { font-family: inherit; }
449
465
  `;
450
466
  var DARK_CSS = `:host { color-scheme: dark;
451
- --eid-bg: hsl(229 50% 6%); --eid-bg-subtle: hsl(222 47% 9%); --eid-bg-muted: hsl(217 33% 13%);
452
- --eid-fg: hsl(210 40% 98%); --eid-fg-muted: hsl(213 27% 84%); --eid-fg-subtle: hsl(215 20% 65%);
453
- --eid-border: hsl(217 33% 17%); --eid-border-strong: hsl(215 25% 27%);
454
- --eid-accent: oklch(0.64 0.15 259); --eid-accent-fg: hsl(229 50% 6%); --eid-accent-subtle: oklch(0.24 0.06 259);
455
- --eid-accent-subtle-fg: oklch(0.82 0.1 259); --eid-ring: oklch(0.68 0.15 259);
456
- --eid-warning-bg: hsl(20 60% 10%); --eid-warning-border: hsl(22 70% 26%); --eid-warning-fg: hsl(40 92% 65%);
457
- --eid-danger-fg: hsl(0 91% 71%); }
467
+ ${darkVars} }
458
468
  `;
459
469
  var CSS_REST = `
460
470
  *, *::before, *::after { box-sizing: border-box; }
461
471
  [hidden] { display: none !important; }
462
472
  h2, p, ol { margin: 0; }
463
- :focus-visible { outline: 2px solid var(--eid-ring); outline-offset: 2px; }
473
+ :focus-visible { outline: 2px solid var(--_eid-ring); outline-offset: 2px; }
464
474
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
465
475
  .btn { display: inline-flex; width: 100%; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 20px;
466
- border-radius: var(--eid-radius); font-family: inherit; font-weight: 500; font-size: 1rem; line-height: 1.25; text-decoration: none; border: 1px solid transparent; cursor: pointer;
476
+ border-radius: var(--_eid-radius); font-family: inherit; font-weight: 500; font-size: 1rem; line-height: 1.25; text-decoration: none; border: 1px solid transparent; cursor: pointer;
467
477
  transition: background-color 160ms cubic-bezier(0,0,0.2,1); }
468
478
  .btn:disabled { opacity: 0.6; cursor: progress; }
469
- .btn-primary { background: var(--eid-accent); color: var(--eid-accent-fg); }
470
- .btn-primary:hover { background: color-mix(in oklab, var(--eid-accent) 88%, black); }
471
- .btn-outline { background: var(--eid-bg); color: var(--eid-fg); border-color: var(--eid-border-strong); }
472
- .btn-outline:hover { background: var(--eid-bg-muted); }
479
+ .btn-primary { background: var(--_eid-accent); color: var(--_eid-accent-fg); }
480
+ .btn-primary:hover { background: color-mix(in oklab, var(--_eid-accent) 88%, black); }
481
+ .btn-outline { background: var(--_eid-bg); color: var(--_eid-fg); border-color: var(--_eid-border-strong); }
482
+ .btn-outline:hover { background: var(--_eid-bg-muted); }
473
483
  .btn-sm { width: auto; min-height: 36px; padding: 0 14px; font-size: 0.8125rem; }
474
484
  .trigger { width: auto; }
475
485
  .trigger img { width: 20px; height: 20px; border-radius: 4px; }
476
486
  .backdrop { position: fixed; inset: 0; z-index: 2147483000; background: hsl(222 47% 11% / 0.5); display: flex; align-items: center;
477
487
  justify-content: center; padding: 16px; overflow-y: auto; }
478
- .dialog { position: relative; width: 100%; max-width: 26rem; max-height: calc(100dvh - 32px); overflow-y: auto; background: var(--eid-bg);
479
- color: var(--eid-fg); border: 1px solid var(--eid-border); border-radius: var(--eid-radius-lg); padding: 24px; font-size: 0.875rem; line-height: 1.5rem; }
488
+ .dialog { position: relative; width: 100%; max-width: 26rem; max-height: calc(100dvh - 32px); overflow-y: auto; background: var(--_eid-bg);
489
+ color: var(--_eid-fg); border: 1px solid var(--_eid-border); border-radius: var(--_eid-radius-lg); padding: 24px; font-size: 0.875rem; line-height: 1.5rem; }
480
490
  @media (max-width: 639px) { .dialog { padding: 16px; } }
481
491
  .head { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
482
- .head img { width: 28px; height: 28px; border-radius: var(--eid-radius); }
492
+ .head img { width: 28px; height: 28px; border-radius: var(--_eid-radius); }
483
493
  .head h2 { flex: 1; font-size: 1.125rem; line-height: 1.75rem; font-weight: 600; }
484
- .x { width: 36px; height: 36px; border: 0; border-radius: var(--eid-radius); background: transparent; color: var(--eid-fg-muted); cursor: pointer; font-size: 1.25rem; line-height: 1; }
485
- .x:hover { background: var(--eid-bg-muted); color: var(--eid-fg); }
486
- .qr { position: relative; width: 256px; max-width: 100%; aspect-ratio: 1; margin: 0 auto; border: 1px solid var(--eid-border);
487
- border-radius: var(--eid-radius-lg); background: #fff; overflow: hidden; }
494
+ .x { width: 36px; height: 36px; border: 0; border-radius: var(--_eid-radius); background: transparent; color: var(--_eid-fg-muted); cursor: pointer; font-size: 1.25rem; line-height: 1; }
495
+ .x:hover { background: var(--_eid-bg-muted); color: var(--_eid-fg); }
496
+ .qr { position: relative; width: 256px; max-width: 100%; aspect-ratio: 1; margin: 0 auto; border: 1px solid var(--_eid-border);
497
+ border-radius: var(--_eid-radius-lg); background: #fff; overflow: hidden; }
488
498
  .qr svg { display: block; width: 100%; height: 100%; }
489
499
  .qr.stale svg { opacity: 0.12; }
490
500
  .qr-stale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
491
501
  color: hsl(222 47% 11%); font-weight: 500; }
492
- .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--eid-fg-subtle); }
493
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--eid-fg-muted); }
502
+ .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
503
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--_eid-fg-muted); }
494
504
  .steps { margin-top: 16px; padding: 0; list-style: none; counter-reset: st; display: grid; gap: 8px; }
495
505
  .steps li { counter-increment: st; display: flex; align-items: center; gap: 12px; }
496
- .steps li::before { content: counter(st); flex: none; width: 24px; height: 24px; border-radius: 9999px; background: var(--eid-accent-subtle);
497
- color: var(--eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
498
- .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--eid-fg-subtle); font-size: 0.8125rem; }
499
- .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--eid-border); }
506
+ .steps li::before { content: counter(st); flex: none; width: 24px; height: 24px; border-radius: 9999px; background: var(--_eid-accent-subtle);
507
+ color: var(--_eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
508
+ .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--_eid-fg-subtle); font-size: 0.8125rem; }
509
+ .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--_eid-border); }
500
510
  .field { display: grid; gap: 8px; }
501
511
  .field label { font-weight: 500; }
502
- .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--eid-border-strong); border-radius: var(--eid-radius);
503
- background: var(--eid-bg); color: var(--eid-fg); font-family: inherit; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0.02em; text-transform: uppercase; }
504
- .field input::placeholder { color: var(--eid-fg-subtle); text-transform: none; }
505
- .field input[aria-invalid="true"] { border-color: var(--eid-danger-fg); }
506
- .hint { font-size: 0.8125rem; color: var(--eid-fg-subtle); }
507
- .err { font-size: 0.8125rem; color: var(--eid-danger-fg); font-weight: 500; }
512
+ .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--_eid-border-strong); border-radius: var(--_eid-radius);
513
+ background: var(--_eid-bg); color: var(--_eid-fg); font-family: inherit; font-size: 1rem; line-height: 1.5rem; letter-spacing: 0.02em; text-transform: uppercase; }
514
+ .field input::placeholder { color: var(--_eid-fg-subtle); text-transform: none; }
515
+ .field input[aria-invalid="true"] { border-color: var(--_eid-danger-fg); }
516
+ .hint { font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
517
+ .err { font-size: 0.8125rem; color: var(--_eid-danger-fg); font-weight: 500; }
508
518
  .push { margin-top: 16px; }
509
519
  .center { text-align: center; }
510
520
  .vc { font: 600 2.25rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.16em; padding-left: 0.16em; }
511
521
  .vc:focus, h2:focus { outline: none; }
512
- .text { margin-top: 12px; color: var(--eid-fg-muted); }
513
- .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--eid-fg-muted); }
522
+ .text { margin-top: 12px; color: var(--_eid-fg-muted); }
523
+ .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--_eid-fg-muted); }
514
524
  .end .btn, .center .btn { margin-top: 24px; }
515
525
  .spinner { width: 16px; height: 16px; flex: none; border: 2px solid currentColor; border-right-color: transparent; border-radius: 9999px; animation: spin 0.8s linear infinite; }
516
526
  @keyframes spin { to { transform: rotate(360deg); } }
517
- .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--eid-warning-border); border-radius: var(--eid-radius); background: var(--eid-warning-bg);
518
- color: var(--eid-warning-fg); font-size: 0.8125rem; }
519
- .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--eid-fg-subtle); }
527
+ .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--_eid-warning-border); border-radius: var(--_eid-radius); background: var(--_eid-warning-bg);
528
+ color: var(--_eid-warning-fg); font-size: 0.8125rem; }
529
+ .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
520
530
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
521
531
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
522
532
  `;
package/dist/index.cjs CHANGED
@@ -52,6 +52,7 @@ __export(index_exports, {
52
52
  buildDeviceLink: () => buildDeviceLink,
53
53
  buildRegisterBody: () => buildRegisterBody,
54
54
  createEidLogin: () => createEidLogin,
55
+ defaultRateLimit: () => defaultRateLimit,
55
56
  deviceLink: () => deviceLink,
56
57
  deviceLinkInput: () => deviceLinkInput,
57
58
  forwardedClientIp: () => forwardedClientIp,
@@ -2131,6 +2132,51 @@ function memoryLoginStore() {
2131
2132
  }
2132
2133
  };
2133
2134
  }
2135
+ var defaultRateLimit = Object.freeze({
2136
+ push: Object.freeze({ limit: 3, windowMs: 6e4 })
2137
+ });
2138
+ var RATE_KINDS = ["start", "push"];
2139
+ function windowLimiter(limit, windowMs) {
2140
+ const hits = /* @__PURE__ */ new Map();
2141
+ return (key) => {
2142
+ const now = Date.now();
2143
+ const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
2144
+ if (hits.size > 1e4) hits.clear();
2145
+ if (h.length >= limit) {
2146
+ hits.set(key, h);
2147
+ return false;
2148
+ }
2149
+ h.push(now);
2150
+ hits.set(key, h);
2151
+ return true;
2152
+ };
2153
+ }
2154
+ function buildRateLimit(opt) {
2155
+ const extra = typeof opt === "function" ? opt : void 0;
2156
+ const user = opt && typeof opt === "object" ? opt : {};
2157
+ for (const k of Object.keys(user)) {
2158
+ if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
2159
+ }
2160
+ const merged = { ...defaultRateLimit, ...user };
2161
+ const perKind = /* @__PURE__ */ new Map();
2162
+ for (const k of RATE_KINDS) {
2163
+ const r = merged[k];
2164
+ if (r === void 0 || r === false) continue;
2165
+ if (typeof r === "function") {
2166
+ perKind.set(k, r);
2167
+ continue;
2168
+ }
2169
+ if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
2170
+ throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
2171
+ }
2172
+ perKind.set(k, windowLimiter(r.limit, r.windowMs));
2173
+ }
2174
+ return async (key) => {
2175
+ const fn = perKind.get(key.slice(0, key.indexOf(":")));
2176
+ if (fn && !await fn(key)) return false;
2177
+ return extra ? extra(key) : true;
2178
+ };
2179
+ }
2134
2180
  var CALLBACK_PATH = "/auth/eid/callback";
2135
2181
  function forwardedClientIp(headers, trustProxy) {
2136
2182
  if (trustProxy === false || trustProxy === 0) return void 0;
@@ -2230,17 +2276,7 @@ function createEidLogin(opts) {
2230
2276
  const successRedirect = opts.successRedirect ?? "/";
2231
2277
  const failureRedirect = opts.failureRedirect ?? "/";
2232
2278
  const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
2233
- const pushHits = /* @__PURE__ */ new Map();
2234
- const rateLimit = opts.rateLimit ?? ((key) => {
2235
- if (!key.startsWith("push:")) return true;
2236
- const now = Date.now();
2237
- const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
2238
- if (pushHits.size > 1e4) pushHits.clear();
2239
- if (hits.length >= 3) return false;
2240
- hits.push(now);
2241
- pushHits.set(key, hits);
2242
- return true;
2243
- });
2279
+ const rateLimit = buildRateLimit(opts.rateLimit);
2244
2280
  const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
2245
2281
  const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
2246
2282
  const json = (status2, body, extra = {}) => ({
@@ -2526,6 +2562,7 @@ function createEidLogin(opts) {
2526
2562
  buildDeviceLink,
2527
2563
  buildRegisterBody,
2528
2564
  createEidLogin,
2565
+ defaultRateLimit,
2529
2566
  deviceLink,
2530
2567
  deviceLinkInput,
2531
2568
  forwardedClientIp,
package/dist/index.d.cts CHANGED
@@ -1092,6 +1092,20 @@ interface EidLoginSuccess {
1092
1092
  data?: unknown;
1093
1093
  headers?: Record<string, string | string[]>;
1094
1094
  }
1095
+ /** Хязгаарлагч: true — зөвшөөрөх, false — 429. */
1096
+ type EidLoginRateLimitFn = (key: string) => boolean | Promise<boolean>;
1097
+ /** Нэг төрлийн хязгаар: санах ойн гулсах цонх, өөрийн функц, эсвэл `false` (хязгааргүй). */
1098
+ type EidLoginRateRule = {
1099
+ limit: number;
1100
+ windowMs: number;
1101
+ } | EidLoginRateLimitFn | false;
1102
+ /** Төрөл тус бүрийн хязгаар: `start` — `start:<ip>`, `push` — `push:<idCode>`. */
1103
+ interface EidLoginRateLimits {
1104
+ start?: EidLoginRateRule;
1105
+ push?: EidLoginRateRule;
1106
+ }
1107
+ /** Анхдагч хязгаар — `rateLimit`-ийн объектод өгөөгүй төрөл энүүгээр үлдэнэ. `{ ...defaultRateLimit, start: … }`. */
1108
+ declare const defaultRateLimit: Readonly<EidLoginRateLimits>;
1095
1109
  interface EidLoginOptions {
1096
1110
  client: EidClient;
1097
1111
  /**
@@ -1123,10 +1137,15 @@ interface EidLoginOptions {
1123
1137
  domain?: string;
1124
1138
  };
1125
1139
  /**
1126
- * Хязгаарлалт: `start:<ip>` (ip мэдэгдэж байвал) ба `push:<idCode>` түлхүүрээр дуудагдана; false → 429.
1127
- * Анхдагч: push-д нэг дугаарт 60 секундэд 3 (санах ойд) — хохирогчийн утсыг мэдэгдлээр дарахаас (MFA fatigue).
1140
+ * Хязгаарлалт. Түлхүүр: `start:<ip>` (ip мэдэгдэж байвал), `push:<idCode>`; false → 429.
1141
+ * Анхдагч {@link defaultRateLimit}: push-д нэг дугаарт 60 секундэд 3 (санах ойд) — MFA fatigue-ээс.
1142
+ *
1143
+ * - Объект `{ start?, push? }` — анхдагчтай НЭГТГЭГДЭНЭ: өгсөн төрөл анхдагчийг дарна, өгөөгүй нь анхдагчаараа.
1144
+ * Утга: `{ limit, windowMs }` (санах ойн гулсах цонх), функц (Redis г.м.) эсвэл `false` (тухайн төрлийг хязгаарлахгүй).
1145
+ * - Функц `(key) => boolean` — бүх түлхүүрт дуудагдана, анхдагч хязгаар ДЭЭР НЭМЭГДЭНЭ (хоёулаа зөвшөөрөх ёстой).
1146
+ * Анхдагчийг сулруулах/солих бол объект хэлбэрээр тухайн төрлийг өг.
1128
1147
  */
1129
- rateLimit?: (key: string) => boolean | Promise<boolean>;
1148
+ rateLimit?: EidLoginRateLimitFn | EidLoginRateLimits;
1130
1149
  /**
1131
1150
  * `handle(Request)`-ийн клиент IP (`start:<ip>` хязгаарт). Web-standard `Request`-д IP байдаггүй тул эсвэл энийг, эсвэл
1132
1151
  * {@link EidLoginOptions.trustProxy}-г өг; аль нь ч үгүй бол IP-ээр хязгаарлахгүй (анхны дуудлагад onError-оор анхааруулна).
@@ -1178,4 +1197,4 @@ declare function withReturnHint(callbackUrl: string, userAgent: string): string;
1178
1197
  */
1179
1198
  declare function createEidLogin(opts: EidLoginOptions): EidLogin;
1180
1199
 
1181
- export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertChoiceOptions, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CompletedForRegistration, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, type EidLogin, type EidLoginContext, type EidLoginIdentity, type EidLoginMode, type EidLoginOptions, type EidLoginRequest, type EidLoginResponse, type EidLoginStore, type EidLoginSuccess, type EidLoginTx, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PDF_SIGNATURE_REGISTER_FIELDS, type PdfDigestInput, type PdfDigestSession, type PdfDigestSigner, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, type RegisterSignatureInput, type RegisterSignatureResult, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type Signer, type SignerRef, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, buildRegisterBody, createEidLogin, deviceLinkInput, forwardedClientIp, interactionsToBase64, memoryLoginStore, parseSessionResult, parseSubject, personMatchesCertificate, qrDeviceLinkTicker, randomChallenge, registrationFromCompleted, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates, withReturnHint };
1200
+ export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertChoiceOptions, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CompletedForRegistration, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, type EidLogin, type EidLoginContext, type EidLoginIdentity, type EidLoginMode, type EidLoginOptions, type EidLoginRateLimitFn, type EidLoginRateLimits, type EidLoginRateRule, type EidLoginRequest, type EidLoginResponse, type EidLoginStore, type EidLoginSuccess, type EidLoginTx, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PDF_SIGNATURE_REGISTER_FIELDS, type PdfDigestInput, type PdfDigestSession, type PdfDigestSigner, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, type RegisterSignatureInput, type RegisterSignatureResult, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type Signer, type SignerRef, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, buildRegisterBody, createEidLogin, defaultRateLimit, deviceLinkInput, forwardedClientIp, interactionsToBase64, memoryLoginStore, parseSessionResult, parseSubject, personMatchesCertificate, qrDeviceLinkTicker, randomChallenge, registrationFromCompleted, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates, withReturnHint };
package/dist/index.d.ts CHANGED
@@ -1092,6 +1092,20 @@ interface EidLoginSuccess {
1092
1092
  data?: unknown;
1093
1093
  headers?: Record<string, string | string[]>;
1094
1094
  }
1095
+ /** Хязгаарлагч: true — зөвшөөрөх, false — 429. */
1096
+ type EidLoginRateLimitFn = (key: string) => boolean | Promise<boolean>;
1097
+ /** Нэг төрлийн хязгаар: санах ойн гулсах цонх, өөрийн функц, эсвэл `false` (хязгааргүй). */
1098
+ type EidLoginRateRule = {
1099
+ limit: number;
1100
+ windowMs: number;
1101
+ } | EidLoginRateLimitFn | false;
1102
+ /** Төрөл тус бүрийн хязгаар: `start` — `start:<ip>`, `push` — `push:<idCode>`. */
1103
+ interface EidLoginRateLimits {
1104
+ start?: EidLoginRateRule;
1105
+ push?: EidLoginRateRule;
1106
+ }
1107
+ /** Анхдагч хязгаар — `rateLimit`-ийн объектод өгөөгүй төрөл энүүгээр үлдэнэ. `{ ...defaultRateLimit, start: … }`. */
1108
+ declare const defaultRateLimit: Readonly<EidLoginRateLimits>;
1095
1109
  interface EidLoginOptions {
1096
1110
  client: EidClient;
1097
1111
  /**
@@ -1123,10 +1137,15 @@ interface EidLoginOptions {
1123
1137
  domain?: string;
1124
1138
  };
1125
1139
  /**
1126
- * Хязгаарлалт: `start:<ip>` (ip мэдэгдэж байвал) ба `push:<idCode>` түлхүүрээр дуудагдана; false → 429.
1127
- * Анхдагч: push-д нэг дугаарт 60 секундэд 3 (санах ойд) — хохирогчийн утсыг мэдэгдлээр дарахаас (MFA fatigue).
1140
+ * Хязгаарлалт. Түлхүүр: `start:<ip>` (ip мэдэгдэж байвал), `push:<idCode>`; false → 429.
1141
+ * Анхдагч {@link defaultRateLimit}: push-д нэг дугаарт 60 секундэд 3 (санах ойд) — MFA fatigue-ээс.
1142
+ *
1143
+ * - Объект `{ start?, push? }` — анхдагчтай НЭГТГЭГДЭНЭ: өгсөн төрөл анхдагчийг дарна, өгөөгүй нь анхдагчаараа.
1144
+ * Утга: `{ limit, windowMs }` (санах ойн гулсах цонх), функц (Redis г.м.) эсвэл `false` (тухайн төрлийг хязгаарлахгүй).
1145
+ * - Функц `(key) => boolean` — бүх түлхүүрт дуудагдана, анхдагч хязгаар ДЭЭР НЭМЭГДЭНЭ (хоёулаа зөвшөөрөх ёстой).
1146
+ * Анхдагчийг сулруулах/солих бол объект хэлбэрээр тухайн төрлийг өг.
1128
1147
  */
1129
- rateLimit?: (key: string) => boolean | Promise<boolean>;
1148
+ rateLimit?: EidLoginRateLimitFn | EidLoginRateLimits;
1130
1149
  /**
1131
1150
  * `handle(Request)`-ийн клиент IP (`start:<ip>` хязгаарт). Web-standard `Request`-д IP байдаггүй тул эсвэл энийг, эсвэл
1132
1151
  * {@link EidLoginOptions.trustProxy}-г өг; аль нь ч үгүй бол IP-ээр хязгаарлахгүй (анхны дуудлагад onError-оор анхааруулна).
@@ -1178,4 +1197,4 @@ declare function withReturnHint(callbackUrl: string, userAgent: string): string;
1178
1197
  */
1179
1198
  declare function createEidLogin(opts: EidLoginOptions): EidLogin;
1180
1199
 
1181
- export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertChoiceOptions, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CompletedForRegistration, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, type EidLogin, type EidLoginContext, type EidLoginIdentity, type EidLoginMode, type EidLoginOptions, type EidLoginRequest, type EidLoginResponse, type EidLoginStore, type EidLoginSuccess, type EidLoginTx, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PDF_SIGNATURE_REGISTER_FIELDS, type PdfDigestInput, type PdfDigestSession, type PdfDigestSigner, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, type RegisterSignatureInput, type RegisterSignatureResult, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type Signer, type SignerRef, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, buildRegisterBody, createEidLogin, deviceLinkInput, forwardedClientIp, interactionsToBase64, memoryLoginStore, parseSessionResult, parseSubject, personMatchesCertificate, qrDeviceLinkTicker, randomChallenge, registrationFromCompleted, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates, withReturnHint };
1200
+ export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertChoiceOptions, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CompletedForRegistration, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EidClient, type EidLogin, type EidLoginContext, type EidLoginIdentity, type EidLoginMode, type EidLoginOptions, type EidLoginRateLimitFn, type EidLoginRateLimits, type EidLoginRateRule, type EidLoginRequest, type EidLoginResponse, type EidLoginStore, type EidLoginSuccess, type EidLoginTx, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PDF_SIGNATURE_REGISTER_FIELDS, type PdfDigestInput, type PdfDigestSession, type PdfDigestSigner, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, type RegisterSignatureInput, type RegisterSignatureResult, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type Signer, type SignerRef, TokenApi, type TokenAuthChallenge, type TokenAuthResult, type TokenCertInput, type TokenCertItem, type TokenCertResult, type TokenCertStart, type TokenIdentity, type TokenLimit, type TokenOrganization, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, buildRegisterBody, createEidLogin, defaultRateLimit, deviceLinkInput, forwardedClientIp, interactionsToBase64, memoryLoginStore, parseSessionResult, parseSubject, personMatchesCertificate, qrDeviceLinkTicker, randomChallenge, registrationFromCompleted, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates, withReturnHint };
package/dist/index.js CHANGED
@@ -1899,6 +1899,51 @@ function memoryLoginStore() {
1899
1899
  }
1900
1900
  };
1901
1901
  }
1902
+ var defaultRateLimit = Object.freeze({
1903
+ push: Object.freeze({ limit: 3, windowMs: 6e4 })
1904
+ });
1905
+ var RATE_KINDS = ["start", "push"];
1906
+ function windowLimiter(limit, windowMs) {
1907
+ const hits = /* @__PURE__ */ new Map();
1908
+ return (key) => {
1909
+ const now = Date.now();
1910
+ const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
1911
+ if (hits.size > 1e4) hits.clear();
1912
+ if (h.length >= limit) {
1913
+ hits.set(key, h);
1914
+ return false;
1915
+ }
1916
+ h.push(now);
1917
+ hits.set(key, h);
1918
+ return true;
1919
+ };
1920
+ }
1921
+ function buildRateLimit(opt) {
1922
+ const extra = typeof opt === "function" ? opt : void 0;
1923
+ const user = opt && typeof opt === "object" ? opt : {};
1924
+ for (const k of Object.keys(user)) {
1925
+ if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
1926
+ }
1927
+ const merged = { ...defaultRateLimit, ...user };
1928
+ const perKind = /* @__PURE__ */ new Map();
1929
+ for (const k of RATE_KINDS) {
1930
+ const r = merged[k];
1931
+ if (r === void 0 || r === false) continue;
1932
+ if (typeof r === "function") {
1933
+ perKind.set(k, r);
1934
+ continue;
1935
+ }
1936
+ if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
1937
+ throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
1938
+ }
1939
+ perKind.set(k, windowLimiter(r.limit, r.windowMs));
1940
+ }
1941
+ return async (key) => {
1942
+ const fn = perKind.get(key.slice(0, key.indexOf(":")));
1943
+ if (fn && !await fn(key)) return false;
1944
+ return extra ? extra(key) : true;
1945
+ };
1946
+ }
1902
1947
  var CALLBACK_PATH = "/auth/eid/callback";
1903
1948
  function forwardedClientIp(headers, trustProxy) {
1904
1949
  if (trustProxy === false || trustProxy === 0) return void 0;
@@ -1998,17 +2043,7 @@ function createEidLogin(opts) {
1998
2043
  const successRedirect = opts.successRedirect ?? "/";
1999
2044
  const failureRedirect = opts.failureRedirect ?? "/";
2000
2045
  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
- });
2046
+ const rateLimit = buildRateLimit(opts.rateLimit);
2012
2047
  const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
2013
2048
  const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
2014
2049
  const json = (status2, body, extra = {}) => ({
@@ -2293,6 +2328,7 @@ export {
2293
2328
  buildDeviceLink,
2294
2329
  buildRegisterBody,
2295
2330
  createEidLogin,
2331
+ defaultRateLimit,
2296
2332
  deviceLink,
2297
2333
  deviceLinkInput,
2298
2334
  forwardedClientIp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",