@gerege-systems/eid-mongolia-sdk 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PersonBlock, b as PdfSessionBlock, c as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-BWCDj8xO.js';
2
- export { d as ApiError, e as AuthenticationError, f as DeviceLinkError, g as DeviceLinkInput, h as EndResult, i as ForbiddenError, j as HttpErrorInit, k as NavigatorLike, l as NetworkError, O as ON_BEHALF_ERROR_CODES, m as OnBehalfErrorCode, n as OpenOrShowQROptions, o as OrgResult, p as PdfDocumentStatus, q as PdfSigner, r as RETRYABLE_ERROR_CODES, s as Representation, t as RetryableErrorCode, u as SessionFailedError, v as SessionState, w as SignatureAlgorithmParameters, x as SignatureProtocol, T as TokenCertSessionBlock, y as TokenCertStatus, V as ValidationError, z as deviceLink, B as isMobileUserAgent, G as openOrShowQR, J as parseRetryAfter } from './browser-BWCDj8xO.js';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PersonBlock, b as PdfSessionBlock, c as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-B_xuB7Rt.js';
2
+ export { d as ApiError, e as AuthenticationError, f as DeviceLinkError, g as DeviceLinkInput, h as EndResult, i as ForbiddenError, j as HttpErrorInit, k as NavigatorLike, l as NetworkError, O as ON_BEHALF_ERROR_CODES, m as OnBehalfErrorCode, n as OpenOrShowQROptions, o as OrgResult, p as PdfDocumentStatus, q as PdfSigner, r as RETRYABLE_ERROR_CODES, s as Representation, t as ResponseTooLargeError, u as RetryableErrorCode, v as SessionFailedError, w as SessionState, x as SignatureAlgorithmParameters, y as SignatureProtocol, T as TokenCertSessionBlock, z as TokenCertStatus, V as ValidationError, B as deviceLink, G as isMobileUserAgent, J as openOrShowQR, K as parseRetryAfter } from './browser-B_xuB7Rt.js';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -17,6 +17,8 @@ interface HttpConfig {
17
17
  dispatcher?: unknown;
18
18
  /** Тест/custom орчинд fetch-ийг орлуулах (default — global fetch). */
19
19
  fetchImpl?: typeof fetch;
20
+ /** `Accept-Language` header-ийн утга (ж: "en"); хоосон бол илгээхгүй. */
21
+ language?: string;
20
22
  }
21
23
  declare class Http {
22
24
  private readonly cfg;
@@ -25,8 +27,8 @@ declare class Http {
25
27
  get(path: string, timeoutMs?: number): Promise<unknown>;
26
28
  /** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
27
29
  postMultipart(path: string, form: FormData, timeoutMs?: number): Promise<unknown>;
28
- /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
29
- getBytes(path: string, timeoutMs?: number): Promise<Uint8Array>;
30
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
31
+ getBytes(path: string, timeoutMs?: number, maxBytes?: number): Promise<Uint8Array>;
30
32
  private json;
31
33
  private send;
32
34
  }
@@ -153,7 +155,11 @@ interface PdfDefaults {
153
155
  certificateLevel: CertificateLevel;
154
156
  /** prepare / document дуудлагын хугацаа (мс); анхдагч {@link DEFAULT_PDF_TIMEOUT_MS}. */
155
157
  timeoutMs?: number;
158
+ /** document / tokenDocument-ийн санах ойд унших дээд хэмжээ (байт); анхдагч {@link DEFAULT_PDF_MAX_BYTES}. */
159
+ maxBytes?: number;
156
160
  }
161
+ /** Гарын үсэгтэй PDF татахад санах ойд уншиж болох анхдагч дээд хэмжээ (64 MiB) — `ClientConfig.pdfMaxBytes`. */
162
+ declare const DEFAULT_PDF_MAX_BYTES: number;
157
163
  /**
158
164
  * PAdES prepare / баримт татах нэг дуудлагын анхдагч хугацаа (мс). CA prepare нь ≤ 10 MiB уншиж, өмнөх гарын үсэг +
159
165
  * LTV өгөгдөл шалгаад push эхлүүлдэг; баримт татахад угсралтыг ≤ 30с хүлээдэг (PADES_SIGNING §3.4). 80с нь хоёуланд
@@ -209,6 +215,35 @@ interface PdfPrepareSession {
209
215
  /** SDK хариу хүлээн авсан мөч (ms epoch) — QR-ийн `receivedAt` ({@link buildDeviceLink}). */
210
216
  receivedAt: number;
211
217
  }
218
+ /** USB токены PAdES — POST /pdf/token/prepare-ийн оролт. */
219
+ interface PdfTokenPrepareInput {
220
+ /** Эх PDF (≤ 10 223 616 байт). */
221
+ pdf: Uint8Array;
222
+ /** Баримтын нэр (≤ 120 тэмдэгт, заавал). */
223
+ fileName: string;
224
+ /** Иргэний ETSI (`PNOMN-…`) — токены гэрчилгээний SERIALNUMBER-тэй таарна. */
225
+ signerEtsi: string;
226
+ /** Токены SIGN гэрчилгээ (DER): eID Mongolia Issuing CA-аас, ACTIVE токенд холбогдсон. */
227
+ certificate: Uint8Array;
228
+ /** Олон гарын үсэгтэй баримтын docID (заавал биш, UUID). */
229
+ docID?: string;
230
+ /** Байгууллагын токенд ЗААВАЛ тухайн байгууллага (`NTRMN-…`); хувийн токенд хоосон. Зөрвөл 403 TOKEN_ORG_MISMATCH. */
231
+ onBehalfOf?: string;
232
+ }
233
+ /** POST /pdf/token/prepare-ийн хариу. */
234
+ interface PdfTokenPrepareSession {
235
+ signingId: string;
236
+ docId: string;
237
+ /** Токеноор зурах 32 байт (SHA-256 DTBS): CKM_ECDSA (prehashed) → түүхий r||s (64 байт). */
238
+ hashToSign: Uint8Array;
239
+ hashAlgorithm: string;
240
+ signatureAlgorithm: string;
241
+ /** "RAW_RS". */
242
+ signatureFormat: string;
243
+ /** tokenFinalize-ийн эцсийн хугацаа (prepare-ээс 10 мин), ISO-8601. */
244
+ expiresAt: string;
245
+ raw: unknown;
246
+ }
212
247
  declare class PdfApi {
213
248
  private readonly http;
214
249
  private readonly creds;
@@ -226,6 +261,28 @@ declare class PdfApi {
226
261
  * дараа дахин татна (`retryable`).
227
262
  */
228
263
  document(sessionId: string): Promise<Uint8Array>;
264
+ private get timeout();
265
+ private get maxBytes();
266
+ /**
267
+ * USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
268
+ * (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
269
+ * зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
270
+ * LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
271
+ * TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
272
+ */
273
+ tokenPrepare(input: PdfTokenPrepareInput): Promise<PdfTokenPrepareSession>;
274
+ /**
275
+ * POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
276
+ * Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
277
+ * Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
278
+ * TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
279
+ */
280
+ tokenFinalize(signingId: string, signature: Uint8Array): Promise<PdfSessionBlock>;
281
+ /**
282
+ * GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
283
+ * хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
284
+ */
285
+ tokenDocument(signingId: string): Promise<Uint8Array>;
229
286
  /**
230
287
  * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
231
288
  * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
@@ -696,7 +753,15 @@ declare class ResponseValidator {
696
753
  private checkIssuer;
697
754
  private checkValidity;
698
755
  private checkLevel;
699
- /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
756
+ /**
757
+ * RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
758
+ * замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
759
+ * гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
760
+ * завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
761
+ * бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
762
+ * fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
763
+ * хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
764
+ */
700
765
  private verifyChain;
701
766
  /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
702
767
  private findIssuer;
@@ -734,10 +799,20 @@ interface ClientConfig {
734
799
  timeoutMs?: number;
735
800
  /** PAdES prepare / баримт татах дуудлагын хугацаа (мс). Анхдагч 80000 ({@link DEFAULT_PDF_TIMEOUT_MS}). */
736
801
  pdfTimeoutMs?: number;
802
+ /**
803
+ * `pdf.document` / `pdf.tokenDocument`-ийн санах ойд унших дээд хэмжээ (байт). Анхдагч 64 MiB
804
+ * ({@link DEFAULT_PDF_MAX_BYTES}); хэтэрвэл `ResponseTooLargeError`.
805
+ */
806
+ pdfMaxBytes?: number;
737
807
  /** undici Dispatcher (mTLS client cert). */
738
808
  dispatcher?: unknown;
739
809
  /** fetch орлуулагч (тест). */
740
810
  fetchImpl?: typeof fetch;
811
+ /**
812
+ * Хүнд харуулах текстийн хэл (`roleLabel` г.м.) — дуудлага бүрд `Accept-Language` болж илгээгдэнэ (`"mn"` | `"en"`).
813
+ * Анхдагч: илгээхгүй (сервер mn).
814
+ */
815
+ language?: string;
741
816
  /** Trust anchor (Root CA) — validator-д. Production-д ЗААВАЛ. */
742
817
  trust?: TrustConfig;
743
818
  }
@@ -989,4 +1064,4 @@ declare class EidOidcClient {
989
1064
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
990
1065
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
991
1066
 
992
- export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DEFAULT_PDF_TIMEOUT_MS, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, 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, interactionsToBase64, parseSessionResult, parseSubject, personMatchesCertificate, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates };
1067
+ export { AcspContext, type AcspV2Params, type ActivityCounts, type ActivityItem, type ActivityQuery, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CITIZEN_ISSUING_CA_SKI, type CertCounts, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DEFAULT_PDF_MAX_BYTES, DEFAULT_PDF_TIMEOUT_MS, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type PdfTokenPrepareInput, type PdfTokenPrepareSession, type PersonActivity, PersonApi, PersonBlock, type PersonCertificate, type PersonCertificates, type PersonChild, type PersonChildren, type PersonDevice, type PersonDevices, type PersonSummary, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationOutcome, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, 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, interactionsToBase64, parseSessionResult, parseSubject, personMatchesCertificate, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, tokenAuthDigest, tokenLimitOf, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature, verifyTokenCertificates };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  NetworkError,
8
8
  ON_BEHALF_ERROR_CODES,
9
9
  RETRYABLE_ERROR_CODES,
10
+ ResponseTooLargeError,
10
11
  SessionFailedError,
11
12
  ValidationError,
12
13
  deviceLink,
@@ -14,10 +15,11 @@ import {
14
15
  maskPath,
15
16
  openOrShowQR,
16
17
  parseRetryAfter
17
- } from "./chunk-YDVPZMAL.js";
18
+ } from "./chunk-QPJDZLHR.js";
18
19
 
19
20
  // src/http.ts
20
21
  var DEFAULT_TIMEOUT_MS = 15e3;
22
+ var MAX_JSON_BYTES = 4 << 20;
21
23
  var Http = class {
22
24
  constructor(cfg) {
23
25
  this.cfg = cfg;
@@ -33,14 +35,14 @@ var Http = class {
33
35
  postMultipart(path, form, timeoutMs) {
34
36
  return this.json("POST", path, form, void 0, timeoutMs);
35
37
  }
36
- /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
37
- async getBytes(path, timeoutMs) {
38
+ /** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
39
+ async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
38
40
  const res = await this.send("GET", path, void 0, void 0, timeoutMs);
39
- return new Uint8Array(await res.arrayBuffer());
41
+ return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
40
42
  }
41
43
  async json(method, path, body, contentType, timeoutMs) {
42
44
  const res = await this.send(method, path, body, contentType, timeoutMs);
43
- const text = await res.text();
45
+ const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
44
46
  return text ? JSON.parse(text) : {};
45
47
  }
46
48
  async send(method, path, body, contentType, timeoutMs) {
@@ -50,7 +52,8 @@ var Http = class {
50
52
  method,
51
53
  headers: {
52
54
  Authorization: `Bearer ${this.cfg.apiSecret}`,
53
- ...contentType ? { "Content-Type": contentType } : {}
55
+ ...contentType ? { "Content-Type": contentType } : {},
56
+ ...this.cfg.language ? { "Accept-Language": this.cfg.language } : {}
54
57
  },
55
58
  body,
56
59
  signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
@@ -85,6 +88,34 @@ var Http = class {
85
88
  throw new ApiError(res.status, text, code, { headers, retryAfterMs });
86
89
  }
87
90
  };
91
+ async function readLimited(res, limit, op) {
92
+ const declared = Number(res.headers.get("content-length") ?? NaN);
93
+ if (declared > limit) {
94
+ await res.body?.cancel().catch(() => void 0);
95
+ throw new ResponseTooLargeError(op, limit);
96
+ }
97
+ if (!res.body) return new Uint8Array(0);
98
+ const reader = res.body.getReader();
99
+ const chunks = [];
100
+ let total = 0;
101
+ for (; ; ) {
102
+ const { done, value } = await reader.read();
103
+ if (done) break;
104
+ total += value.byteLength;
105
+ if (total > limit) {
106
+ await reader.cancel().catch(() => void 0);
107
+ throw new ResponseTooLargeError(op, limit);
108
+ }
109
+ chunks.push(value);
110
+ }
111
+ const out = new Uint8Array(total);
112
+ let off = 0;
113
+ for (const c of chunks) {
114
+ out.set(c, off);
115
+ off += c.byteLength;
116
+ }
117
+ return out;
118
+ }
88
119
  function errorBody(text) {
89
120
  try {
90
121
  const v = JSON.parse(text);
@@ -121,7 +152,10 @@ var RP_ROUTES = {
121
152
  };
122
153
  var RP_ROUTES_UNDISCOVERED = {
123
154
  tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
124
- tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
155
+ tokenAuthVerify: { method: "POST", path: "/token/auth/verify" },
156
+ pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
157
+ pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
158
+ pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
125
159
  };
126
160
  function routePath(route, ...args) {
127
161
  let i = 0;
@@ -429,10 +463,12 @@ function parseOrg(raw) {
429
463
  const o = asRecord(raw);
430
464
  const etsi = str(o.orgEtsi);
431
465
  if (!etsi) return null;
466
+ const roleLabel = str(o.roleLabel);
432
467
  return {
433
468
  orgEtsi: etsi,
434
469
  orgName: str(o.orgName),
435
470
  role: str(o.role),
471
+ ...roleLabel ? { roleLabel } : {},
436
472
  rightType: str(o.rightType)
437
473
  };
438
474
  }
@@ -469,6 +505,7 @@ var SessionApi = class {
469
505
 
470
506
  // src/pdf.ts
471
507
  import { createHash as createHash2 } from "crypto";
508
+ var DEFAULT_PDF_MAX_BYTES = 64 << 20;
472
509
  var DEFAULT_PDF_TIMEOUT_MS = 8e4;
473
510
  function rec3(v) {
474
511
  return v && typeof v === "object" ? v : {};
@@ -501,7 +538,7 @@ var PdfApi = class {
501
538
  const form = new FormData();
502
539
  form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
503
540
  form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
504
- const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS));
541
+ const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
505
542
  const receivedAt = Date.now();
506
543
  const vc = rec3(r.vc).value;
507
544
  const dl = rec3(r.deviceLink);
@@ -528,7 +565,75 @@ var PdfApi = class {
528
565
  * дараа дахин татна (`retryable`).
529
566
  */
530
567
  document(sessionId) {
531
- return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS);
568
+ return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
569
+ }
570
+ get timeout() {
571
+ return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
572
+ }
573
+ get maxBytes() {
574
+ return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
575
+ }
576
+ /**
577
+ * USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
578
+ * (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
579
+ * зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
580
+ * LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
581
+ * TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
582
+ */
583
+ async tokenPrepare(input) {
584
+ if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
585
+ throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
586
+ }
587
+ const request = {
588
+ relyingPartyUUID: this.creds.rpUUID,
589
+ relyingPartyName: this.creds.rpName,
590
+ signer: { etsi: input.signerEtsi.trim() },
591
+ certificate: Buffer.from(input.certificate).toString("base64"),
592
+ fileName: input.fileName
593
+ };
594
+ if (input.docID?.trim()) request.docID = input.docID.trim();
595
+ if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
596
+ const form = new FormData();
597
+ form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
598
+ form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
599
+ const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
600
+ const r = rec3(raw);
601
+ const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
602
+ if (!r.signingID || hashToSign.length !== 32) {
603
+ throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
604
+ }
605
+ return {
606
+ signingId: String(r.signingID),
607
+ docId: String(r.docID ?? ""),
608
+ hashToSign,
609
+ hashAlgorithm: String(r.hashAlgorithm ?? ""),
610
+ signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
611
+ signatureFormat: String(r.signatureFormat ?? ""),
612
+ expiresAt: String(r.expiresAt ?? ""),
613
+ raw
614
+ };
615
+ }
616
+ /**
617
+ * POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
618
+ * Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
619
+ * Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
620
+ * TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
621
+ */
622
+ async tokenFinalize(signingId, signature) {
623
+ if (signature?.length !== 64) throw new Error("eid: pdf token: signature \u043D\u044C \u0442\u04AF\u04AF\u0445\u0438\u0439 r||s 64 \u0431\u0430\u0439\u0442 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 (P-256, DER \u0431\u0438\u0448)");
624
+ const r = await this.http.post(
625
+ routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
626
+ { signature: Buffer.from(signature).toString("base64") },
627
+ this.timeout
628
+ );
629
+ return r;
630
+ }
631
+ /**
632
+ * GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
633
+ * хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
634
+ */
635
+ tokenDocument(signingId) {
636
+ return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
532
637
  }
533
638
  /**
534
639
  * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
@@ -562,7 +667,7 @@ var OrganizationApi = class {
562
667
  async getRepresentations(personEtsi) {
563
668
  const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
564
669
  const list = Array.isArray(raw.representations) ? raw.representations : [];
565
- return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
670
+ return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list, raw };
566
671
  }
567
672
  };
568
673
 
@@ -714,6 +819,10 @@ function parseSubject(cert) {
714
819
  return out;
715
820
  }
716
821
  function certExtension(der, oid) {
822
+ return certExtensions(der).find((e) => e.oid === oid)?.value;
823
+ }
824
+ function certExtensions(der) {
825
+ const out = [];
717
826
  const top = tlv(der, 0, der.length);
718
827
  const tbs = tlv(der, top.start, top.end);
719
828
  let f = tlv(der, tbs.start, tbs.end);
@@ -727,16 +836,20 @@ function certExtension(der, oid) {
727
836
  const ext = tlv(der, ep, seq.end);
728
837
  const id = tlv(der, ext.start, ext.end);
729
838
  let v = tlv(der, id.end, ext.end);
730
- if (v.tag === 1) v = tlv(der, v.end, ext.end);
839
+ let critical = false;
840
+ if (v.tag === 1) {
841
+ critical = v.end > v.start && der[v.start] !== 0;
842
+ v = tlv(der, v.end, ext.end);
843
+ }
731
844
  if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
732
- if (oidOf(der, id) === oid) return der.subarray(v.start, v.end);
845
+ out.push({ oid: oidOf(der, id), critical, value: der.subarray(v.start, v.end) });
733
846
  ep = ext.end;
734
847
  }
735
- return void 0;
848
+ return out;
736
849
  }
737
850
  pos = t.end;
738
851
  }
739
- return void 0;
852
+ return out;
740
853
  }
741
854
 
742
855
  // src/token.ts
@@ -1086,7 +1199,7 @@ var ResponseValidator = class {
1086
1199
  );
1087
1200
  }
1088
1201
  } else {
1089
- this.verifyChain(leaf);
1202
+ this.verifyChain(leaf, protocol === "ACSP_V2" ? EKU_CLIENT_AUTH : EKU_EMAIL_PROTECTION);
1090
1203
  }
1091
1204
  this.checkIssuer(leaf);
1092
1205
  return leaf;
@@ -1130,21 +1243,40 @@ var ResponseValidator = class {
1130
1243
  const need = rank[this.requiredLevel] ?? 2;
1131
1244
  if (got < need) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u0432\u0448\u0438\u043D \u0445\u04AF\u0440\u044D\u043B\u0446\u044D\u0445\u0433\u04AF\u0439: ${level} < ${this.requiredLevel}`);
1132
1245
  }
1133
- /** leaf-ээс эхлэн issuer-ийг intermediates/anchors дотроос олж, anchor хүртэл chain зангидна. */
1134
- verifyChain(leaf) {
1135
- let current = leaf;
1136
- const pool = [...this.intermediates, ...this.anchors];
1137
- for (let depth = 0; depth < 8; depth++) {
1138
- for (const anchor of this.anchors) {
1139
- if (isCa(anchor) && current.checkIssued(anchor) && current.verify(anchor.publicKey)) return;
1246
+ /**
1247
+ * RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
1248
+ * замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
1249
+ * гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
1250
+ * завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
1251
+ * бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
1252
+ * fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
1253
+ * хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
1254
+ */
1255
+ verifyChain(leaf, usage) {
1256
+ const now = Math.min(Math.max(Date.now(), Date.parse(leaf.validFrom)), Date.parse(leaf.validTo));
1257
+ const leafReason = criticalReason(leaf) ?? ekuReason(leaf, usage);
1258
+ if (leafReason) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${leafReason}`);
1259
+ let reason;
1260
+ const extend = (cur, cas) => {
1261
+ if (cas.length >= 8) return false;
1262
+ for (const [cand, isAnchor] of [
1263
+ ...this.anchors.map((c) => [c, true]),
1264
+ ...this.intermediates.map((c) => [c, false])
1265
+ ]) {
1266
+ if (cas.includes(cand) || !signedBy(cur, cand)) continue;
1267
+ const bad = caReason(cand, cas.length, now, usage);
1268
+ if (bad) {
1269
+ reason ??= bad;
1270
+ continue;
1271
+ }
1272
+ if (isAnchor || extend(cand, [...cas, cand])) return true;
1140
1273
  }
1141
- const issuer = pool.find(
1142
- (c) => isCa(c) && current.checkIssued(c) && current.verify(c.publicKey)
1143
- );
1144
- if (!issuer) break;
1145
- current = issuer;
1146
- }
1147
- throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439");
1274
+ return false;
1275
+ };
1276
+ if (extend(leaf, [])) return;
1277
+ throw new ValidationError(
1278
+ reason ? `\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${reason}` : "\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439"
1279
+ );
1148
1280
  }
1149
1281
  /** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
1150
1282
  findIssuer(leaf) {
@@ -1355,6 +1487,79 @@ function authorityKeyId(cert) {
1355
1487
  function isCa(cert) {
1356
1488
  return cert.ca === true;
1357
1489
  }
1490
+ var EKU_ANY = "2.5.29.37.0";
1491
+ var EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
1492
+ var EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
1493
+ var OID_NAME_CONSTRAINTS = "2.5.29.30";
1494
+ var HANDLED_CRITICAL = /* @__PURE__ */ new Set([
1495
+ "2.5.29.14",
1496
+ // subjectKeyIdentifier
1497
+ "2.5.29.15",
1498
+ // keyUsage
1499
+ "2.5.29.17",
1500
+ // subjectAltName
1501
+ "2.5.29.19",
1502
+ // basicConstraints
1503
+ "2.5.29.30",
1504
+ // nameConstraints (CA-д — caReason fail-closed)
1505
+ "2.5.29.31",
1506
+ // cRLDistributionPoints
1507
+ "2.5.29.32",
1508
+ // certificatePolicies
1509
+ "2.5.29.35",
1510
+ // authorityKeyIdentifier
1511
+ "2.5.29.37",
1512
+ // extKeyUsage
1513
+ "1.3.6.1.5.5.7.1.1"
1514
+ // authorityInfoAccess
1515
+ ]);
1516
+ function signedBy(child, cand) {
1517
+ if (child.issuer !== cand.subject) return false;
1518
+ const aki = authorityKeyId(child);
1519
+ const ski = subjectKeyId(cand);
1520
+ if (aki && ski && aki !== ski) return false;
1521
+ try {
1522
+ return child.verify(cand.publicKey);
1523
+ } catch {
1524
+ return false;
1525
+ }
1526
+ }
1527
+ function criticalReason(cert) {
1528
+ return certExtensions(cert.raw).some((e) => e.critical && !HANDLED_CRITICAL.has(e.oid)) ? "\u04AF\u043B \u0442\u0430\u043D\u0438\u0445 critical extension" : void 0;
1529
+ }
1530
+ function ekuReason(cert, usage) {
1531
+ const eku = extKeyUsage(cert);
1532
+ return eku.length && !eku.includes(usage) && !eku.includes(EKU_ANY) ? "\u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 ExtKeyUsage \u044D\u043D\u044D \u04AF\u0439\u043B\u0434\u044D\u043B\u0434 \u0442\u043E\u0445\u0438\u0440\u043E\u0445\u0433\u04AF\u0439 (\u0433\u0438\u043D\u0436\u0438\u0439\u043D EKU)" : void 0;
1533
+ }
1534
+ function caReason(ca, below, now, usage) {
1535
+ if (!isCa(ca)) return "\u0437\u0430\u0432\u0441\u0440\u044B\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D CA \u0431\u0438\u0448 \u044D\u0441\u0432\u044D\u043B keyCertSign \u044D\u0440\u0445\u0433\u04AF\u0439";
1536
+ if (certExtension(ca.raw, "2.5.29.15") && !(keyUsageBits(ca) & 4)) return "CA-\u0438\u0439\u043D KeyUsage-\u0434 keyCertSign \u0430\u043B\u0433\u0430";
1537
+ const from = Date.parse(ca.validFrom);
1538
+ const to = Date.parse(ca.validTo);
1539
+ if (Number.isNaN(from) || Number.isNaN(to) || now < from || now > to) {
1540
+ return "\u0433\u0438\u043D\u0436\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D \u044D\u0441\u0432\u044D\u043B \u0445\u04AF\u0447\u0438\u043D\u0442\u044D\u0439 \u0431\u043E\u043B\u043E\u043E\u0433\u04AF\u0439";
1541
+ }
1542
+ const pathLen = basicConstraintsPathLen(ca);
1543
+ if (pathLen !== void 0 && below > pathLen) return "pathLenConstraint \u0437\u04E9\u0440\u0447\u0441\u04E9\u043D (\u0437\u0430\u0432\u0441\u0440\u044B\u043D CA \u0445\u044D\u0442 \u043E\u043B\u043E\u043D)";
1544
+ if (certExtension(ca.raw, OID_NAME_CONSTRAINTS)) return "CA-\u0434 name constraints \u0431\u0430\u0439\u043D\u0430 (SDK \u04AF\u043D\u044D\u043B\u0434\u044D\u0433\u0433\u04AF\u0439 \u2014 fail-closed)";
1545
+ return criticalReason(ca) ?? ekuReason(ca, usage);
1546
+ }
1547
+ function basicConstraintsPathLen(cert) {
1548
+ const v = certExtension(cert.raw, "2.5.29.19");
1549
+ if (!v) return void 0;
1550
+ const seq = tlv(v, 0, v.length);
1551
+ for (let p = seq.start; p < seq.end; ) {
1552
+ const t = tlv(v, p, seq.end);
1553
+ if (t.tag === 2) {
1554
+ if (t.end - t.start > 4 || (v[t.start] & 128) !== 0) throw new ValidationError("basicConstraints: pathLen \u0431\u0443\u0440\u0443\u0443");
1555
+ let n = 0;
1556
+ for (let i = t.start; i < t.end; i++) n = n * 256 + v[i];
1557
+ return n;
1558
+ }
1559
+ p = t.end;
1560
+ }
1561
+ return void 0;
1562
+ }
1358
1563
 
1359
1564
  // src/client.ts
1360
1565
  var DEFAULT_BASE_URL = "https://rp.eidmongolia.mn";
@@ -1391,12 +1596,17 @@ var EidClient = class {
1391
1596
  apiSecret: cfg.credentials.apiSecret,
1392
1597
  timeoutMs: cfg.timeoutMs,
1393
1598
  dispatcher: cfg.dispatcher,
1394
- fetchImpl: cfg.fetchImpl
1599
+ fetchImpl: cfg.fetchImpl,
1600
+ language: cfg.language?.trim() || void 0
1395
1601
  });
1396
1602
  const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
1397
1603
  this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
1398
1604
  this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
1399
- this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level, timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS });
1605
+ this.pdf = new PdfApi(http, cfg.credentials, {
1606
+ certificateLevel: level,
1607
+ timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
1608
+ maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
1609
+ });
1400
1610
  this.organization = new OrganizationApi(http);
1401
1611
  this.person = new PersonApi(http);
1402
1612
  this.token = new TokenApi(http, cfg.credentials);
@@ -1757,6 +1967,7 @@ export {
1757
1967
  AuthenticationError,
1758
1968
  CITIZEN_ISSUING_CA_SKI,
1759
1969
  DEFAULT_BASE_URL,
1970
+ DEFAULT_PDF_MAX_BYTES,
1760
1971
  DEFAULT_PDF_TIMEOUT_MS,
1761
1972
  DeviceLinkError,
1762
1973
  EID_ACR_ADVANCED,
@@ -1774,6 +1985,7 @@ export {
1774
1985
  PdfApi,
1775
1986
  PersonApi,
1776
1987
  RETRYABLE_ERROR_CODES,
1988
+ ResponseTooLargeError,
1777
1989
  ResponseValidator,
1778
1990
  SessionApi,
1779
1991
  SessionFailedError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsup src/index.ts src/browser.ts --format esm,cjs --dts --clean",
32
- "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/oidc.test.ts test/portal-gaps.test.ts",
32
+ "test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/oidc.test.ts test/portal-gaps.test.ts test/chain.test.ts",
33
33
  "test:contract": "npm run build && node --experimental-strip-types test/contract.test.ts",
34
34
  "lint": "tsc --noEmit"
35
35
  },