@gerege-systems/eid-mongolia-sdk 0.14.0 → 0.14.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
@@ -61,6 +61,7 @@ export const eidLogin = createEidLogin({
61
61
  callbackUrl: "https://bank.example.mn/auth/eid/callback", // host нь RP-ийн callbackHosts-д; утасны урсгалд заавал
62
62
  async onSuccess(id) {
63
63
  // id.etsi (PNOMN-…) — хэрэглэгчийг ҮҮГЭЭР таньна; id.givenName/surname (латин), id.person (кирилл, байвал)
64
+ // id.sessionId, id.documentNumber, id.certificateDerB64 — дараагийн алхамд (гарын үсэг, гэрчилгээ холбох)
64
65
  const user = await users.findOrCreateByEtsi(id.etsi);
65
66
  const sid = await sessions.create(user.id); // ШИНЭ session (session fixation)
66
67
  return {
@@ -70,6 +71,7 @@ export const eidLogin = createEidLogin({
70
71
  },
71
72
  // store: redisStore, // олон instance / serverless бол (анхдагч — процессын санах ой)
72
73
  // rateLimit: (key) => redisLimiter.allow(key), // "start:<ip>", "push:<idCode>"; анхдагч push 3/мин нэг дугаарт
74
+ trustProxy: 1, // handle(Request)-д клиент IP: nginx/LB нэг hop бол 1 (доор «Клиент IP»)
73
75
  });
74
76
  ```
75
77
 
@@ -82,6 +84,18 @@ export const GET = (req: Request) => eidLogin.handle(req);
82
84
  export const POST = (req: Request) => eidLogin.handle(req);
83
85
  ```
84
86
 
87
+ **Клиент IP** (`start:<ip>` хязгаар). Web-standard `Request`-д IP байдаггүй — `handle(req)` ашиглаж байвал аль нэгийг өг:
88
+
89
+ - `trustProxy: n` — итгэмжит reverse proxy-ийн ард: `X-Forwarded-For`-ийн **баруунаас n** дахь утга (n = таны proxy-ийн тоо),
90
+ XFF байхгүй бол `X-Real-IP`. `true` = XFF-ийн зүүн утга (proxy нь клиентийн XFF-ийг дарж бичдэг үед л). Proxy-гүй
91
+ үед бүү асаа — header-ийг клиент хуурамчаар илгээнэ.
92
+ - `getClientIp: (req) => …` — платформын IP (Bun `server.requestIP(req)?.address`, Deno `info.remoteAddr.hostname`,
93
+ Vercel `req.headers.get("x-real-ip")`). `trustProxy`-оос давуу.
94
+
95
+ Аль нь ч үгүй бол IP-ээр хязгаарлахгүй (эхний хүсэлтэд `onError`-оор нэг удаа анхааруулна). `handleRequest`-д `req.ip`
96
+ (Express `req.ip`, `app.set("trust proxy", …)`-тэй) шууд хэрэглэгдэнэ. Анхдагч `rateLimit` зөвхөн `push:`-ийг хязгаарладаг —
97
+ `start:<ip>`-ийг өөрийн `rateLimit`-ээр (Redis г.м.) хязгаарлана. Go: `eidlogin.Config.ClientIP` (`eidlogin.ForwardedClientIP(n)`) — ижил дүрэм.
98
+
85
99
  Express:
86
100
 
87
101
  ```ts
@@ -103,11 +117,37 @@ app.use("/auth/eid", express.json(), async (req, res) => {
103
117
  </script>
104
118
  ```
105
119
 
106
- эсвэл `mountEidLoginButton(el, { startUrl, statusUrl, lang: "en", successUrl, onSuccess, onError })`.
107
- Атрибут: `lang` (`mn` | `en`), `mode` (`auto` анхдагч — утас → Web2App, бусад → QR цонх; `qr`; `app`), `success-url`.
120
+ эсвэл `mountEidLoginButton(el, { startUrl, statusUrl, lang: "en", theme: "auto", successUrl, onSuccess, onError })`.
121
+ Атрибут: `lang` (`mn` | `en`), `mode` (`auto` анхдагч — утас → Web2App, бусад → QR цонх; `qr`; `app`),
122
+ `theme` (`auto` анхдагч | `light` | `dark`), `success-url`.
108
123
  Үйл явдал: `eid-login-success`, `eid-login-error` (`detail.error`: `USER_REFUSED…`, `VALIDATION`, `EXPIRED` …).
109
- Дизайн нь eID нэвтрэх хуудсынх (лого, өнгө, light/dark); `--eid-accent`, `--eid-bg` г.м. CSS хувьсагчаар, `::part(button)`-оор
110
- тохируулна. Гадны хамааралгүй (QR encoder дотроо), цонх нь focus trap, Esc, `aria-live` төлөвтэй.
124
+ Гадны хамааралгүй (QR encoder дотроо), цонх нь focus trap, Esc, `aria-live` төлөвтэй.
125
+
126
+ **Дизайн, theme.** Дизайн нь eID нэвтрэх хуудсынх (лого, өнгө). Фонт нь хуудаснаас өвлөгдөнө (товч, цонх, input бүгд).
127
+ `theme="auto"` үед дараалал: хуудасны `html[data-theme="dark|light"]` → `html.dark` / `html.light` → OS
128
+ (`prefers-color-scheme`); хуудас theme солиход (MutationObserver) шууд дагана. `theme="light|dark"` нь хуудсыг давна.
129
+ Өнгө, радиусыг CSS хувьсагчаар (элемент дээр — shadow-ийн `:host`-оос давуу) дарна:
130
+
131
+ ```css
132
+ eid-login-button {
133
+ --eid-accent: #0b5cff; --eid-accent-fg: #fff; /* үндсэн товч, QR-ийн алхмын тоо (--eid-accent-subtle, --eid-accent-subtle-fg) */
134
+ --eid-bg: #fff; --eid-bg-subtle: …; --eid-bg-muted: …; /* цонх, hover */
135
+ --eid-fg: …; --eid-fg-muted: …; --eid-fg-subtle: …; /* текст */
136
+ --eid-border: …; --eid-border-strong: …; --eid-ring: …; /* хүрээ, focus */
137
+ --eid-warning-bg: …; --eid-warning-border: …; --eid-warning-fg: …; --eid-danger-fg: …;
138
+ --eid-radius: 6px; --eid-radius-lg: 8px;
139
+ }
140
+ html[data-theme="dark"] eid-login-button { --eid-accent: …; } /* dark-д өөр өнгө бол */
141
+ eid-login-button::part(button) { width: 100%; } /* part: button, backdrop, dialog */
142
+ ```
143
+
144
+ **eID аппын доод хувилбар.** `flowType` бэхлэлт (device link v3) ба Web2App-ийн `userChallengeVerifier` нь
145
+ **iOS 2.2.3 (build 59)**, **Android 2.2.3 (versionCode 60)**-аас эхэлсэн. eID сервер 2026-10-07-оос legacy холбоосыг
146
+ бүрэн хассан (v3-гүй session-д утас `DEVICE_LINK_REQUIRED`) бөгөөд доод хувилбарыг `/mobile/app-config`-оор албадан
147
+ шинэчлүүлдэг: Android 2026-10-07-оос ≥ 70 (төхөөрөмжийн баталгаатай), 2026-10-14-ний байдлаар iOS ≥ 2.2.7,
148
+ Android ≥ 83 (`curl https://ca.eidmongolia.mn/mobile/app-config`). Иймд гарын үсэг зурж чадах бүх апп эдгээрийг дэмжинэ —
149
+ RP талд нэмэлт шалгалт хэрэггүй. Эх сурвалж: eID CA-ийн `docs/DEVICE_LINK_V3.md` §5, §7; changelog
150
+ `2026-10-02-ios-min-version-2-2-3`, `2026-10-07-android-min-70`.
111
151
 
112
152
  **Аюулгүй байдал.**
113
153
 
@@ -120,6 +160,33 @@ app.use("/auth/eid", express.json(), async (req, res) => {
120
160
  (store-ийн `delete` атомар байх ёстой — Redis `DEL`).
121
161
  - `onSuccess`-ийн `data` хөтөчид очно — нууц бүү оруул. Иргэний бүртгэлтэй эсэхийг push алдаагаар задлахгүй (`eid_unavailable`).
122
162
  - Олон instance / serverless (Vercel г.м.) дээр анхдагч санах ойн store ажиллахгүй — `EidLoginStore` (`get`/`set`/`delete`)-ийг Redis-ээр.
163
+ - `onSuccess` дуудагдахаас ӨМНӨ tx store-оос атомар устгагдана (нэг удаагийн баталгаа) — `onSuccess` дотор store-оос бүү
164
+ унш; хэрэгтэй бүхэн `identity`-д (`sessionId`, `documentNumber`, `certificateDerB64`, …).
165
+
166
+ **Redis store** (SDK нэмэлт хамааралгүй — жишээ нь `ioredis`):
167
+
168
+ ```ts
169
+ import Redis from "ioredis";
170
+ import type { EidLoginStore, EidLoginTx } from "@gerege-systems/eid-mongolia-sdk";
171
+
172
+ const redis = new Redis(process.env.REDIS_URL!);
173
+ const key = (sid: string) => `eid:login:${sid}`;
174
+
175
+ export const redisStore: EidLoginStore = {
176
+ async get(sid) {
177
+ const v = await redis.get(key(sid));
178
+ return v ? (JSON.parse(v) as EidLoginTx) : undefined;
179
+ },
180
+ async set(sid, tx, ttlMs) {
181
+ await redis.set(key(sid), JSON.stringify(tx), "PX", ttlMs);
182
+ },
183
+ async delete(sid) {
184
+ return (await redis.del(key(sid))) === 1; // DEL атомар — зэрэг хоёр status-ийн зөвхөн нэг нь onSuccess дуудна
185
+ },
186
+ };
187
+ ```
188
+
189
+ Tx-д `sessionSecret` (QR угсрах материал) байдаг — Redis-ийг сүлжээнээс хаалттай, TLS/AUTH-тай ашигла.
123
190
 
124
191
  ## 2. Нэвтрэлт (push)
125
192
 
@@ -201,6 +268,40 @@ eid.validator.validateSign(result, digest384, { hashAlgorithm: "SHA-384", expect
201
268
 
202
269
  RAW_DIGEST_SIGNATURE-д `flowType` гарын үсэгт ордоггүй (зөвхөн digest зурагдана) — энэ нь RP-API хариуны тулгалт.
203
270
 
271
+ **QR / Web2App / App2App** — `eid.sign.deviceLinkByEtsi` / `deviceLinkByDocument` (`callbackUrl`-тай бол Web2App/App2App);
272
+ холбоосыг backend-д `buildDeviceLink(deviceLinkInput(s, "QR", "mon"))` (auth/cert session-д ч мөн). `s.sign`
273
+ (`SignContext`: digest, hash, алгоритм, interactions, callback)-ийг session-тэй хамт серверт хадгал.
274
+
275
+ **`expiresAt`** (ms epoch) — sign / certificate-choice session-ий *тооцоолсон* дуусах мөч: CA session start хариунд
276
+ хугацаа буцаадаггүй (зөвхөн `sessionID`), CA-ийн анхдагч 600с (`DEFAULT_SIGN_SESSION_TTL_MS`; CA админ 60–1800с болгож
277
+ болно → `signSessionTtlMs`). Хүсэлтээс өмнөх цагаас тооцдог тул CA-ийнхаас хойно хэзээ ч биш; эцсийн үнэн нь
278
+ `GET /session` (`TIMEOUT`).
279
+
280
+ ### 3.1. Гэрчилгээг урьдчилж мэдэх (client-side PAdES / CMS)
281
+
282
+ Гэрчилгээ DTBS-д (`signingCertificateV2`) ордог тул эхлээд certificate-choice, дараа нь digest-ийг **яг тэр төхөөрөмж
283
+ рүү (`documentNumber`)** зуруулна. Олон утастай иргэнд etsi-ээр эхлүүлбэл өөр утас зурж, гарын үсэг гэрчилгээтэйгээ
284
+ таарахгүй.
285
+
286
+ ```ts
287
+ const cc = await eid.sign.certificateChoiceByEtsi("PNOMN-12345678"); // эсвэл certificateChoiceDeviceLink() (зөвхөн QR)
288
+ const signer = eid.validator.validateCertificateChoice(await eid.session.waitForResult(cc.sessionId), cc.sessionId);
289
+ const dtbs = buildDtbs(signer.certificate); // 32 байт SHA-256, стандарт base64
290
+ const s = await eid.sign.digestForSigner(signer, dtbs, ix, { fileName: "Гэрээ.pdf" }); // linked/{documentNumber}
291
+ // QR / Web2App: eid.sign.deviceLinkForSigner(signer, dtbs, ix, { callbackUrl })
292
+ const r = await eid.session.waitForResult(s.sessionId);
293
+ const sig = eid.validator.validateSign(r, s.sign!.digest, {
294
+ hashAlgorithm: s.sign!.hashAlgorithm,
295
+ expectedFlowType: "Notification",
296
+ expectedAlgorithm: s.sign!.signatureAlgorithm, // sha256WithRSAEncryption — PSS хариуг татгалзана
297
+ expectedCertificate: signer.certificate, // өөр гэрчилгээгээр зурсан бол татгалзана
298
+ });
299
+ ```
300
+
301
+ `digestForSigner` / `deviceLinkForSigner`-ийн анхдагч: `sha256WithRSAEncryption` + `SHA-256` (CA SplitKey RSA нь хүссэн
302
+ алгоритмаар, PKCS#1 v1.5; хуучин threshold-ECDSA credential нь үргэлж `ecdsa-with-SHA256` — `expectedAlgorithm` үүнийг
303
+ зөвхөн EC P-256 гэрчилгээтэй үед зөвшөөрнө). Digest-ийн урт hash-тай таарахгүй бол хүсэлт явахгүй.
304
+
204
305
  ## 3a. Байгууллагын нэрийн өмнөөс гарын үсэг (onBehalfOf)
205
306
 
206
307
  Иргэн **өөрийн PIN2 гэрчилгээгээр** зурна; тухайн мөчид байгууллагыг төлөөлөх эрхтэйг eID **өөрийн бүртгэлээр**
@@ -72,7 +72,38 @@ interface NotificationSession {
72
72
  rpChallenge: string;
73
73
  /** Auth session-д validator-т өгөх контекст (sign session-д undefined). */
74
74
  acsp?: AcspContext;
75
+ /** Sign session-ий хүсэлтэд илгээсэн утгууд ({@link SignContext}); auth / certificate-choice-д undefined. */
76
+ sign?: SignContext;
77
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch) — {@link sessionExpiry}; auth-д undefined. */
78
+ expiresAt?: number;
75
79
  }
80
+ /**
81
+ * Sign session-ий хүсэлтэд илгээсэн утгууд ЯГ ХЭВЭЭР: `validateSign` (digest, hashAlgorithm, signatureAlgorithm) ба
82
+ * device link (`buildDeviceLink`: digest, relyingPartyName, interactions, initialCallbackUrl)-д. RP session-тэй хамт
83
+ * серверт хадгална.
84
+ */
85
+ interface SignContext {
86
+ /** Илгээсэн digest (стандарт base64, ЯГ ХЭВЭЭР — device link-ийн authCode-д ордог). */
87
+ digest: string;
88
+ hashAlgorithm: HashAlgorithm;
89
+ signatureAlgorithm: SignatureAlgorithm;
90
+ relyingPartyName: string;
91
+ /** Илгээсэн interactions-ийн base64 МӨР. */
92
+ interactions: string;
93
+ initialCallbackUrl: string;
94
+ }
95
+ /**
96
+ * CA-ийн SIGNATURE ба CERTIFICATE_CHOICE session-ий анхдагч хүчинтэй хугацаа (мс; `EID_SIGN_SESSION_TTL_SECONDS` = 600).
97
+ * CA-ийн админ «Системийн тохиргоо»-нд 60–1800 секундэд өөрчилж болно; session start хариунд CA хугацааг буцаадаггүй
98
+ * (зөвхөн `sessionID`) тул SDK үүнийг `ClientConfig.signSessionTtlMs`-ээр тооцоолно.
99
+ */
100
+ declare const DEFAULT_SIGN_SESSION_TTL_MS = 600000;
101
+ /**
102
+ * Session start-ын хариунаас тооцоолох дуусах мөч (ms epoch): хүсэлт илгээхээс ӨМНӨХ цаг + ttl. CA ExpiresAt-ийг өөрийн
103
+ * хүлээн авсан мөчөөс тооцдог тул энэ утга CA-ийнхаас ХЭЗЭЭ Ч хойно биш (консерватив). RP UI-ийн тоолуур / «дахин
104
+ * эхлүүлэх» шийдвэрт л — эцсийн үнэн нь `GET /session` (`TIMEOUT`).
105
+ */
106
+ declare function sessionExpiry(sentAtMs: number, ttlMs?: number): number;
76
107
  /**
77
108
  * Device-link (QR/Web2App/App2App) session. Иргэн QR уншиж эсвэл товч дарж нэгдэнэ.
78
109
  * `sessionToken`, `sessionSecret`-ийг ЗӨВХӨН backend-д хадгална — браузер руу бүү гарга.
@@ -99,6 +130,14 @@ interface DeviceLinkSession {
99
130
  * ЗӨВХӨН backend-д хадгална.
100
131
  */
101
132
  receivedAt: number;
133
+ /** auth | sign | cert ({@link deviceLinkInput}-д). Хуучин хадгалсан session-д undefined = auth. */
134
+ sessionType?: "auth" | "sign" | "cert";
135
+ /** Sign session-д ({@link deviceLinkInput}, `validateSign`); бусад үед undefined. */
136
+ sign?: SignContext;
137
+ /** Certificate-choice session-ий хүсэлтэд илгээсэн нэр (cert холбоосын authCode-д). */
138
+ relyingPartyName?: string;
139
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch, {@link sessionExpiry}); auth-д undefined. */
140
+ expiresAt?: number;
102
141
  }
103
142
  /** Session poll-ийн нэгтгэсэн үр дүн (validator-т орох түүхий материал). */
104
143
  interface SessionResult {
@@ -444,4 +483,4 @@ declare class ValidationError extends EidError {
444
483
  declare class DeviceLinkError extends EidError {
445
484
  }
446
485
 
447
- export { type AcspContext as A, deviceLink as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, isMobileUserAgent as G, type HashAlgorithm as H, type Interaction as I, openOrShowQR as J, parseRetryAfter as K, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DeviceLinkError as f, type DeviceLinkInput as g, type EndResult as h, ForbiddenError as i, type HttpErrorInit as j, type NavigatorLike as k, NetworkError as l, type OnBehalfErrorCode as m, type OpenOrShowQROptions as n, type OrgResult as o, type PdfDocumentStatus as p, type PdfSigner as q, RETRYABLE_ERROR_CODES as r, type Representation as s, ResponseTooLargeError as t, type RetryableErrorCode as u, SessionFailedError as v, type SessionState as w, type SignatureAlgorithmParameters as x, type SignatureProtocol as y, type TokenCertStatus as z };
486
+ export { type AcspContext as A, type SignatureProtocol as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type TokenCertStatus as G, type HashAlgorithm as H, type Interaction as I, deviceLink as J, isMobileUserAgent as K, openOrShowQR as L, parseRetryAfter as M, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, sessionExpiry as Q, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DEFAULT_SIGN_SESSION_TTL_MS as f, DeviceLinkError as g, type DeviceLinkInput as h, type EndResult as i, ForbiddenError as j, type HttpErrorInit as k, type NavigatorLike as l, NetworkError as m, type OnBehalfErrorCode as n, type OpenOrShowQROptions as o, type OrgResult as p, type PdfDocumentStatus as q, type PdfSigner as r, RETRYABLE_ERROR_CODES as s, type Representation as t, ResponseTooLargeError as u, type RetryableErrorCode as v, SessionFailedError as w, type SessionState as x, type SignContext as y, type SignatureAlgorithmParameters as z };
@@ -72,7 +72,38 @@ interface NotificationSession {
72
72
  rpChallenge: string;
73
73
  /** Auth session-д validator-т өгөх контекст (sign session-д undefined). */
74
74
  acsp?: AcspContext;
75
+ /** Sign session-ий хүсэлтэд илгээсэн утгууд ({@link SignContext}); auth / certificate-choice-д undefined. */
76
+ sign?: SignContext;
77
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch) — {@link sessionExpiry}; auth-д undefined. */
78
+ expiresAt?: number;
75
79
  }
80
+ /**
81
+ * Sign session-ий хүсэлтэд илгээсэн утгууд ЯГ ХЭВЭЭР: `validateSign` (digest, hashAlgorithm, signatureAlgorithm) ба
82
+ * device link (`buildDeviceLink`: digest, relyingPartyName, interactions, initialCallbackUrl)-д. RP session-тэй хамт
83
+ * серверт хадгална.
84
+ */
85
+ interface SignContext {
86
+ /** Илгээсэн digest (стандарт base64, ЯГ ХЭВЭЭР — device link-ийн authCode-д ордог). */
87
+ digest: string;
88
+ hashAlgorithm: HashAlgorithm;
89
+ signatureAlgorithm: SignatureAlgorithm;
90
+ relyingPartyName: string;
91
+ /** Илгээсэн interactions-ийн base64 МӨР. */
92
+ interactions: string;
93
+ initialCallbackUrl: string;
94
+ }
95
+ /**
96
+ * CA-ийн SIGNATURE ба CERTIFICATE_CHOICE session-ий анхдагч хүчинтэй хугацаа (мс; `EID_SIGN_SESSION_TTL_SECONDS` = 600).
97
+ * CA-ийн админ «Системийн тохиргоо»-нд 60–1800 секундэд өөрчилж болно; session start хариунд CA хугацааг буцаадаггүй
98
+ * (зөвхөн `sessionID`) тул SDK үүнийг `ClientConfig.signSessionTtlMs`-ээр тооцоолно.
99
+ */
100
+ declare const DEFAULT_SIGN_SESSION_TTL_MS = 600000;
101
+ /**
102
+ * Session start-ын хариунаас тооцоолох дуусах мөч (ms epoch): хүсэлт илгээхээс ӨМНӨХ цаг + ttl. CA ExpiresAt-ийг өөрийн
103
+ * хүлээн авсан мөчөөс тооцдог тул энэ утга CA-ийнхаас ХЭЗЭЭ Ч хойно биш (консерватив). RP UI-ийн тоолуур / «дахин
104
+ * эхлүүлэх» шийдвэрт л — эцсийн үнэн нь `GET /session` (`TIMEOUT`).
105
+ */
106
+ declare function sessionExpiry(sentAtMs: number, ttlMs?: number): number;
76
107
  /**
77
108
  * Device-link (QR/Web2App/App2App) session. Иргэн QR уншиж эсвэл товч дарж нэгдэнэ.
78
109
  * `sessionToken`, `sessionSecret`-ийг ЗӨВХӨН backend-д хадгална — браузер руу бүү гарга.
@@ -99,6 +130,14 @@ interface DeviceLinkSession {
99
130
  * ЗӨВХӨН backend-д хадгална.
100
131
  */
101
132
  receivedAt: number;
133
+ /** auth | sign | cert ({@link deviceLinkInput}-д). Хуучин хадгалсан session-д undefined = auth. */
134
+ sessionType?: "auth" | "sign" | "cert";
135
+ /** Sign session-д ({@link deviceLinkInput}, `validateSign`); бусад үед undefined. */
136
+ sign?: SignContext;
137
+ /** Certificate-choice session-ий хүсэлтэд илгээсэн нэр (cert холбоосын authCode-д). */
138
+ relyingPartyName?: string;
139
+ /** Sign / certificate-choice session-ий ТООЦООЛСОН дуусах мөч (ms epoch, {@link sessionExpiry}); auth-д undefined. */
140
+ expiresAt?: number;
102
141
  }
103
142
  /** Session poll-ийн нэгтгэсэн үр дүн (validator-т орох түүхий материал). */
104
143
  interface SessionResult {
@@ -444,4 +483,4 @@ declare class ValidationError extends EidError {
444
483
  declare class DeviceLinkError extends EidError {
445
484
  }
446
485
 
447
- export { type AcspContext as A, deviceLink as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, isMobileUserAgent as G, type HashAlgorithm as H, type Interaction as I, openOrShowQR as J, parseRetryAfter as K, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DeviceLinkError as f, type DeviceLinkInput as g, type EndResult as h, ForbiddenError as i, type HttpErrorInit as j, type NavigatorLike as k, NetworkError as l, type OnBehalfErrorCode as m, type OpenOrShowQROptions as n, type OrgResult as o, type PdfDocumentStatus as p, type PdfSigner as q, RETRYABLE_ERROR_CODES as r, type Representation as s, ResponseTooLargeError as t, type RetryableErrorCode as u, SessionFailedError as v, type SessionState as w, type SignatureAlgorithmParameters as x, type SignatureProtocol as y, type TokenCertStatus as z };
486
+ export { type AcspContext as A, type SignatureProtocol as B, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type TokenCertStatus as G, type HashAlgorithm as H, type Interaction as I, deviceLink as J, isMobileUserAgent as K, openOrShowQR as L, parseRetryAfter as M, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PersonBlock as P, sessionExpiry as Q, type RpCredentials as R, type SignatureAlgorithm as S, type TokenCertSessionBlock as T, ValidationError as V, type SessionResult as a, type PdfSessionBlock as b, type RepresentationsResponse as c, ApiError as d, AuthenticationError as e, DEFAULT_SIGN_SESSION_TTL_MS as f, DeviceLinkError as g, type DeviceLinkInput as h, type EndResult as i, ForbiddenError as j, type HttpErrorInit as k, type NavigatorLike as l, NetworkError as m, type OnBehalfErrorCode as n, type OpenOrShowQROptions as o, type OrgResult as p, type PdfDocumentStatus as q, type PdfSigner as r, RETRYABLE_ERROR_CODES as s, type Representation as t, ResponseTooLargeError as u, type RetryableErrorCode as v, SessionFailedError as w, type SessionState as x, type SignContext as y, type SignatureAlgorithmParameters as z };
@@ -1 +1 @@
1
- export { f as DeviceLinkError, g as DeviceLinkInput, D as DeviceLinkSession, E as EidError, k as NavigatorLike, n as OpenOrShowQROptions, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR } from './browser-C6JHm0lR.cjs';
1
+ export { g as DeviceLinkError, h as DeviceLinkInput, D as DeviceLinkSession, E as EidError, l as NavigatorLike, o as OpenOrShowQROptions, J as deviceLink, K as isMobileUserAgent, L as openOrShowQR } from './browser-CuMH5IVG.cjs';
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { f as DeviceLinkError, g as DeviceLinkInput, D as DeviceLinkSession, E as EidError, k as NavigatorLike, n as OpenOrShowQROptions, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR } from './browser-C6JHm0lR.js';
1
+ export { g as DeviceLinkError, h as DeviceLinkInput, D as DeviceLinkSession, E as EidError, l as NavigatorLike, o as OpenOrShowQROptions, J as deviceLink, K as isMobileUserAgent, L as openOrShowQR } from './browser-CuMH5IVG.js';
package/dist/button.cjs CHANGED
@@ -22,7 +22,8 @@ var button_exports = {};
22
22
  __export(button_exports, {
23
23
  EidLoginButton: () => EidLoginButton,
24
24
  defineEidLoginButton: () => defineEidLoginButton,
25
- mountEidLoginButton: () => mountEidLoginButton
25
+ mountEidLoginButton: () => mountEidLoginButton,
26
+ resolveEidTheme: () => resolveEidTheme
26
27
  });
27
28
  module.exports = __toCommonJS(button_exports);
28
29
 
@@ -464,7 +465,7 @@ var T = {
464
465
  minute: "1 minute left"
465
466
  }
466
467
  };
467
- var CSS = `
468
+ var CSS_BASE = `
468
469
  :host { display: inline-block;
469
470
  --eid-bg: hsl(0 0% 100%); --eid-bg-subtle: hsl(210 40% 98%); --eid-bg-muted: hsl(210 40% 96%);
470
471
  --eid-fg: hsl(222 47% 11%); --eid-fg-muted: hsl(215 19% 35%); --eid-fg-subtle: hsl(215 16% 45%);
@@ -472,22 +473,26 @@ var CSS = `
472
473
  --eid-accent: oklch(0.534 0.2 259); --eid-accent-fg: #fff; --eid-accent-subtle: oklch(0.97 0.03 259);
473
474
  --eid-accent-subtle-fg: oklch(0.474 0.19 259); --eid-ring: oklch(0.604 0.2 259);
474
475
  --eid-warning-bg: hsl(48 100% 96%); --eid-warning-border: hsl(45 95% 78%); --eid-warning-fg: hsl(23 75% 33%);
475
- --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; }
476
- @media (prefers-color-scheme: dark) { :host {
476
+ --eid-danger-fg: hsl(0 74% 42%); --eid-radius: 6px; --eid-radius-lg: 8px; font-family: inherit; color-scheme: light; }
477
+ button, input { font-family: inherit; }
478
+ `;
479
+ var DARK_CSS = `:host { color-scheme: dark;
477
480
  --eid-bg: hsl(229 50% 6%); --eid-bg-subtle: hsl(222 47% 9%); --eid-bg-muted: hsl(217 33% 13%);
478
481
  --eid-fg: hsl(210 40% 98%); --eid-fg-muted: hsl(213 27% 84%); --eid-fg-subtle: hsl(215 20% 65%);
479
482
  --eid-border: hsl(217 33% 17%); --eid-border-strong: hsl(215 25% 27%);
480
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);
481
484
  --eid-accent-subtle-fg: oklch(0.82 0.1 259); --eid-ring: oklch(0.68 0.15 259);
482
485
  --eid-warning-bg: hsl(20 60% 10%); --eid-warning-border: hsl(22 70% 26%); --eid-warning-fg: hsl(40 92% 65%);
483
- --eid-danger-fg: hsl(0 91% 71%); } }
486
+ --eid-danger-fg: hsl(0 91% 71%); }
487
+ `;
488
+ var CSS_REST = `
484
489
  *, *::before, *::after { box-sizing: border-box; }
485
490
  [hidden] { display: none !important; }
486
491
  h2, p, ol { margin: 0; }
487
492
  :focus-visible { outline: 2px solid var(--eid-ring); outline-offset: 2px; }
488
493
  .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; }
489
494
  .btn { display: inline-flex; width: 100%; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 20px;
490
- border-radius: var(--eid-radius); font: 500 1rem/1.25 inherit; text-decoration: none; border: 1px solid transparent; cursor: pointer;
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;
491
496
  transition: background-color 160ms cubic-bezier(0,0,0.2,1); }
492
497
  .btn:disabled { opacity: 0.6; cursor: progress; }
493
498
  .btn-primary { background: var(--eid-accent); color: var(--eid-accent-fg); }
@@ -524,7 +529,7 @@ h2, p, ol { margin: 0; }
524
529
  .field { display: grid; gap: 8px; }
525
530
  .field label { font-weight: 500; }
526
531
  .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--eid-border-strong); border-radius: var(--eid-radius);
527
- background: var(--eid-bg); color: var(--eid-fg); font: 1rem/1.5rem inherit; letter-spacing: 0.02em; text-transform: uppercase; }
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; }
528
533
  .field input::placeholder { color: var(--eid-fg-subtle); text-transform: none; }
529
534
  .field input[aria-invalid="true"] { border-color: var(--eid-danger-fg); }
530
535
  .hint { font-size: 0.8125rem; color: var(--eid-fg-subtle); }
@@ -544,6 +549,17 @@ h2, p, ol { margin: 0; }
544
549
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
545
550
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
546
551
  `;
552
+ var CSS = CSS_BASE + CSS_REST;
553
+ function resolveEidTheme(theme, doc = document) {
554
+ if (theme === "light" || theme === "dark") return theme;
555
+ const html = doc.documentElement;
556
+ const dt = html?.getAttribute("data-theme");
557
+ if (dt === "dark" || dt === "light") return dt;
558
+ if (html?.classList.contains("dark")) return "dark";
559
+ if (html?.classList.contains("light")) return "light";
560
+ const mq = doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
561
+ return mq?.matches ? "dark" : "light";
562
+ }
547
563
  var ID_RE = /^([А-ЯЁӨҮ]{2}[0-9]{8}|[0-9]{8,14})$/;
548
564
  var FOCUSABLE = 'button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
549
565
  function el(tag, attrs = {}, text) {
@@ -555,7 +571,7 @@ function el(tag, attrs = {}, text) {
555
571
  var Base = typeof HTMLElement === "undefined" ? class {
556
572
  } : HTMLElement;
557
573
  var EidLoginButton = class extends Base {
558
- static observedAttributes = ["lang"];
574
+ static observedAttributes = ["lang", "theme"];
559
575
  /** Тест/дотоод: гар утас эсэхийг дарж бичих. */
560
576
  isMobile = () => isMobileUserAgent();
561
577
  /** Тест/дотоод: шилжилт (location.assign). */
@@ -572,6 +588,10 @@ var EidLoginButton = class extends Base {
572
588
  gen = 0;
573
589
  // цонх нээх бүрд нэмэгдэнэ — хуучин fetch-ийн хариуг үл тооно
574
590
  lastFocus = null;
591
+ darkStyle;
592
+ themeObs = null;
593
+ mq = null;
594
+ onScheme = () => this.applyTheme();
575
595
  get t() {
576
596
  return T[this.getAttribute("lang") === "en" ? "en" : "mn"];
577
597
  }
@@ -579,21 +599,52 @@ var EidLoginButton = class extends Base {
579
599
  return this.getAttribute(name) ?? def;
580
600
  }
581
601
  connectedCallback() {
582
- if (this.root) return;
583
- this.root = this.attachShadow({ mode: "open" });
584
- const style = el("style");
585
- style.textContent = CSS;
586
- this.trigger = el("button", { type: "button", class: "btn btn-outline trigger", part: "button", "aria-haspopup": "dialog" });
587
- this.trigger.append(el("img", { src: EID_LOGO_DATA_URI, alt: "", width: "20", height: "20" }), el("span", {}, this.t.button));
588
- this.trigger.addEventListener("click", () => this.start());
589
- this.root.append(style, this.trigger);
590
- }
591
- attributeChangedCallback() {
602
+ if (!this.root) {
603
+ this.root = this.attachShadow({ mode: "open" });
604
+ const style = el("style");
605
+ style.textContent = CSS;
606
+ this.darkStyle = el("style", { "data-eid-theme": "dark" });
607
+ this.trigger = el("button", { type: "button", class: "btn btn-outline trigger", part: "button", "aria-haspopup": "dialog" });
608
+ this.trigger.append(el("img", { src: EID_LOGO_DATA_URI, alt: "", width: "20", height: "20" }), el("span", {}, this.t.button));
609
+ this.trigger.addEventListener("click", () => this.start());
610
+ this.root.append(style, this.darkStyle, this.trigger);
611
+ }
612
+ this.watchTheme();
613
+ this.applyTheme();
614
+ }
615
+ attributeChangedCallback(name) {
616
+ if (name === "theme") {
617
+ if (this.root) this.applyTheme();
618
+ return;
619
+ }
592
620
  const label = this.trigger?.querySelector("span");
593
621
  if (label) label.textContent = this.t.button;
594
622
  }
595
623
  disconnectedCallback() {
596
624
  this.close();
625
+ this.themeObs?.disconnect();
626
+ this.themeObs = null;
627
+ this.mq?.removeEventListener?.("change", this.onScheme);
628
+ this.mq = null;
629
+ }
630
+ /** Одоогийн бодит схем (`theme` + auto үед хуудас/OS). */
631
+ get scheme() {
632
+ return resolveEidTheme(this.getAttribute("theme"));
633
+ }
634
+ applyTheme() {
635
+ const css = this.scheme === "dark" ? DARK_CSS : "";
636
+ if (this.darkStyle.textContent !== css) this.darkStyle.textContent = css;
637
+ }
638
+ /** auto үед хуудасны html[data-theme]/class ба OS-ийн схемийн өөрчлөлтийг дагана. */
639
+ watchTheme() {
640
+ if (!this.themeObs && typeof MutationObserver !== "undefined" && document.documentElement) {
641
+ this.themeObs = new MutationObserver(this.onScheme);
642
+ this.themeObs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
643
+ }
644
+ if (!this.mq && typeof matchMedia === "function") {
645
+ this.mq = matchMedia("(prefers-color-scheme: dark)");
646
+ this.mq.addEventListener?.("change", this.onScheme);
647
+ }
597
648
  }
598
649
  /** Товчны үйлдэл (программаар ч дуудаж болно). */
599
650
  start() {
@@ -937,6 +988,7 @@ function mountEidLoginButton(target, opts) {
937
988
  b.setAttribute("status-url", opts.statusUrl);
938
989
  b.setAttribute("lang", opts.lang ?? "mn");
939
990
  if (opts.mode) b.setAttribute("mode", opts.mode);
991
+ if (opts.theme) b.setAttribute("theme", opts.theme);
940
992
  if (opts.successUrl) b.setAttribute("success-url", opts.successUrl);
941
993
  if (opts.onSuccess) b.addEventListener("eid-login-success", (e) => opts.onSuccess(e.detail));
942
994
  if (opts.onError) b.addEventListener("eid-login-error", (e) => opts.onError(e.detail.error));
@@ -948,5 +1000,6 @@ defineEidLoginButton();
948
1000
  0 && (module.exports = {
949
1001
  EidLoginButton,
950
1002
  defineEidLoginButton,
951
- mountEidLoginButton
1003
+ mountEidLoginButton,
1004
+ resolveEidTheme
952
1005
  });
package/dist/button.d.cts CHANGED
@@ -17,13 +17,18 @@ interface EidLoginButtonOptions {
17
17
  lang?: Lang;
18
18
  /** auto (анхдагч: утас → Web2App, бусад → QR цонх) | qr | app. */
19
19
  mode?: "auto" | "qr" | "app";
20
+ /** auto (анхдагч: html[data-theme] / html.dark / prefers-color-scheme-ийг дагана) | light | dark. */
21
+ theme?: "auto" | "light" | "dark";
20
22
  /** Амжилтын дараа (backend redirect өгөөгүй бол) шилжих URL. */
21
23
  successUrl?: string;
22
24
  onSuccess?: (s: EidLoginStatus) => void;
23
25
  onError?: (error: string) => void;
24
26
  }
27
+ type Scheme = "light" | "dark";
28
+ /** `theme` атрибутын утгыг (light | dark | auto) бодит схем болгоно. auto: html[data-theme] → html.dark/.light → OS. */
29
+ declare function resolveEidTheme(theme: string | null, doc?: Document): Scheme;
25
30
  declare const Base: typeof HTMLElement;
26
- /** `<eid-login-button>` — атрибут: start-url, status-url, lang (mn|en), mode (auto|qr|app), success-url. */
31
+ /** `<eid-login-button>` — атрибут: start-url, status-url, lang (mn|en), mode (auto|qr|app), theme (auto|light|dark), success-url. */
27
32
  declare class EidLoginButton extends Base {
28
33
  static readonly observedAttributes: string[];
29
34
  /** Тест/дотоод: гар утас эсэхийг дарж бичих. */
@@ -41,11 +46,20 @@ declare class EidLoginButton extends Base {
41
46
  private fails;
42
47
  private gen;
43
48
  private lastFocus;
49
+ private darkStyle;
50
+ private themeObs;
51
+ private mq;
52
+ private readonly onScheme;
44
53
  private get t();
45
54
  private attr;
46
55
  connectedCallback(): void;
47
- attributeChangedCallback(): void;
56
+ attributeChangedCallback(name: string): void;
48
57
  disconnectedCallback(): void;
58
+ /** Одоогийн бодит схем (`theme` + auto үед хуудас/OS). */
59
+ get scheme(): Scheme;
60
+ private applyTheme;
61
+ /** auto үед хуудасны html[data-theme]/class ба OS-ийн схемийн өөрчлөлтийг дагана. */
62
+ private watchTheme;
49
63
  /** Товчны үйлдэл (программаар ч дуудаж болно). */
50
64
  start(): void;
51
65
  private open;
@@ -76,4 +90,4 @@ declare function defineEidLoginButton(tag?: string): void;
76
90
  /** Элементэд товч нэмнэ (framework-гүй хуудас, React/Vue-ийн ref). Үүссэн элементийг буцаана. */
77
91
  declare function mountEidLoginButton(target: Element, opts: EidLoginButtonOptions): EidLoginButton;
78
92
 
79
- export { EidLoginButton, type EidLoginButtonOptions, type EidLoginStatus, defineEidLoginButton, mountEidLoginButton };
93
+ export { EidLoginButton, type EidLoginButtonOptions, type EidLoginStatus, defineEidLoginButton, mountEidLoginButton, resolveEidTheme };
package/dist/button.d.ts CHANGED
@@ -17,13 +17,18 @@ interface EidLoginButtonOptions {
17
17
  lang?: Lang;
18
18
  /** auto (анхдагч: утас → Web2App, бусад → QR цонх) | qr | app. */
19
19
  mode?: "auto" | "qr" | "app";
20
+ /** auto (анхдагч: html[data-theme] / html.dark / prefers-color-scheme-ийг дагана) | light | dark. */
21
+ theme?: "auto" | "light" | "dark";
20
22
  /** Амжилтын дараа (backend redirect өгөөгүй бол) шилжих URL. */
21
23
  successUrl?: string;
22
24
  onSuccess?: (s: EidLoginStatus) => void;
23
25
  onError?: (error: string) => void;
24
26
  }
27
+ type Scheme = "light" | "dark";
28
+ /** `theme` атрибутын утгыг (light | dark | auto) бодит схем болгоно. auto: html[data-theme] → html.dark/.light → OS. */
29
+ declare function resolveEidTheme(theme: string | null, doc?: Document): Scheme;
25
30
  declare const Base: typeof HTMLElement;
26
- /** `<eid-login-button>` — атрибут: start-url, status-url, lang (mn|en), mode (auto|qr|app), success-url. */
31
+ /** `<eid-login-button>` — атрибут: start-url, status-url, lang (mn|en), mode (auto|qr|app), theme (auto|light|dark), success-url. */
27
32
  declare class EidLoginButton extends Base {
28
33
  static readonly observedAttributes: string[];
29
34
  /** Тест/дотоод: гар утас эсэхийг дарж бичих. */
@@ -41,11 +46,20 @@ declare class EidLoginButton extends Base {
41
46
  private fails;
42
47
  private gen;
43
48
  private lastFocus;
49
+ private darkStyle;
50
+ private themeObs;
51
+ private mq;
52
+ private readonly onScheme;
44
53
  private get t();
45
54
  private attr;
46
55
  connectedCallback(): void;
47
- attributeChangedCallback(): void;
56
+ attributeChangedCallback(name: string): void;
48
57
  disconnectedCallback(): void;
58
+ /** Одоогийн бодит схем (`theme` + auto үед хуудас/OS). */
59
+ get scheme(): Scheme;
60
+ private applyTheme;
61
+ /** auto үед хуудасны html[data-theme]/class ба OS-ийн схемийн өөрчлөлтийг дагана. */
62
+ private watchTheme;
49
63
  /** Товчны үйлдэл (программаар ч дуудаж болно). */
50
64
  start(): void;
51
65
  private open;
@@ -76,4 +90,4 @@ declare function defineEidLoginButton(tag?: string): void;
76
90
  /** Элементэд товч нэмнэ (framework-гүй хуудас, React/Vue-ийн ref). Үүссэн элементийг буцаана. */
77
91
  declare function mountEidLoginButton(target: Element, opts: EidLoginButtonOptions): EidLoginButton;
78
92
 
79
- export { EidLoginButton, type EidLoginButtonOptions, type EidLoginStatus, defineEidLoginButton, mountEidLoginButton };
93
+ export { EidLoginButton, type EidLoginButtonOptions, type EidLoginStatus, defineEidLoginButton, mountEidLoginButton, resolveEidTheme };