@gerege-systems/eid-mongolia-sdk 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -4
- package/dist/{browser-C6JHm0lR.d.cts → browser-CSZjH9Xs.d.cts} +65 -1
- package/dist/{browser-C6JHm0lR.d.ts → browser-CSZjH9Xs.d.ts} +65 -1
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/button.cjs +72 -19
- package/dist/button.d.cts +17 -3
- package/dist/button.d.ts +17 -3
- package/dist/button.js +70 -18
- package/dist/{chunk-QPJDZLHR.js → chunk-DXVHOZO7.js} +9 -0
- package/dist/index.cjs +281 -28
- package/dist/index.d.cts +136 -8
- package/dist/index.d.ts +136 -8
- package/dist/index.js +270 -29
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiError,
|
|
3
3
|
AuthenticationError,
|
|
4
|
+
CertChoiceInvalidError,
|
|
4
5
|
DeviceLinkError,
|
|
5
6
|
EidError,
|
|
6
7
|
ForbiddenError,
|
|
7
8
|
NetworkError,
|
|
9
|
+
NoSigningCertificateError,
|
|
8
10
|
ON_BEHALF_ERROR_CODES,
|
|
9
11
|
RETRYABLE_ERROR_CODES,
|
|
12
|
+
RequestInvalidError,
|
|
10
13
|
ResponseTooLargeError,
|
|
11
14
|
SessionFailedError,
|
|
12
15
|
ValidationError,
|
|
@@ -15,7 +18,7 @@ import {
|
|
|
15
18
|
maskPath,
|
|
16
19
|
openOrShowQR,
|
|
17
20
|
parseRetryAfter
|
|
18
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-DXVHOZO7.js";
|
|
19
22
|
|
|
20
23
|
// src/http.ts
|
|
21
24
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -85,7 +88,8 @@ var Http = class {
|
|
|
85
88
|
{ headers, retryAfterMs }
|
|
86
89
|
);
|
|
87
90
|
}
|
|
88
|
-
|
|
91
|
+
const Typed = code === "NO_SIGNING_CERTIFICATE" ? NoSigningCertificateError : code === "CERT_CHOICE_INVALID" ? CertChoiceInvalidError : code === "REQUEST_INVALID" ? RequestInvalidError : ApiError;
|
|
92
|
+
throw new Typed(res.status, text, code, { headers, retryAfterMs });
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
async function readLimited(res, limit, op) {
|
|
@@ -138,6 +142,11 @@ var RP_ROUTES = {
|
|
|
138
142
|
authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
|
|
139
143
|
signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
|
|
140
144
|
signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
|
|
145
|
+
signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
|
|
146
|
+
signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
|
|
147
|
+
signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
|
|
148
|
+
certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
|
|
149
|
+
certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
|
|
141
150
|
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
142
151
|
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
143
152
|
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
@@ -206,6 +215,21 @@ function acspV2Digest(p, hashAlgorithm) {
|
|
|
206
215
|
return createHash(nodeHash(hashAlgorithm)).update(buildAcspV2Payload(p)).digest();
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
// src/types.ts
|
|
219
|
+
var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
|
|
220
|
+
function sessionExpiry(sentAtMs, ttlMs) {
|
|
221
|
+
return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
|
|
222
|
+
}
|
|
223
|
+
function parseExpiresAt(v) {
|
|
224
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(v)) return void 0;
|
|
225
|
+
const t = Date.parse(v);
|
|
226
|
+
return Number.isFinite(t) ? t : void 0;
|
|
227
|
+
}
|
|
228
|
+
function startExpiry(raw, sentAtMs, ttlMs) {
|
|
229
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
230
|
+
return parseExpiresAt(r.expiresAt) ?? sessionExpiry(sentAtMs, ttlMs);
|
|
231
|
+
}
|
|
232
|
+
|
|
209
233
|
// src/auth.ts
|
|
210
234
|
function rec(v) {
|
|
211
235
|
return v && typeof v === "object" ? v : {};
|
|
@@ -251,11 +275,19 @@ var AuthApi = class {
|
|
|
251
275
|
parseNotification(raw, acsp) {
|
|
252
276
|
const r = rec(raw);
|
|
253
277
|
const vc = rec(r.vc).value;
|
|
254
|
-
|
|
278
|
+
const expiresAt = parseExpiresAt(r.expiresAt);
|
|
279
|
+
return {
|
|
280
|
+
sessionId: String(r.sessionID),
|
|
281
|
+
vc: vc != null ? String(vc) : null,
|
|
282
|
+
rpChallenge: acsp.rpChallenge,
|
|
283
|
+
acsp,
|
|
284
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
285
|
+
};
|
|
255
286
|
}
|
|
256
287
|
parseDeviceLink(raw, acsp) {
|
|
257
288
|
const r = rec(raw);
|
|
258
289
|
const vc = typeof r.vc === "object" && r.vc !== null ? rec(r.vc).value : r.vc;
|
|
290
|
+
const expiresAt = parseExpiresAt(r.expiresAt);
|
|
259
291
|
return {
|
|
260
292
|
sessionId: String(r.sessionID),
|
|
261
293
|
sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
|
|
@@ -264,7 +296,9 @@ var AuthApi = class {
|
|
|
264
296
|
vc: vc != null ? String(vc) : null,
|
|
265
297
|
rpChallenge: acsp.rpChallenge,
|
|
266
298
|
acsp,
|
|
267
|
-
receivedAt: Date.now()
|
|
299
|
+
receivedAt: Date.now(),
|
|
300
|
+
sessionType: "auth",
|
|
301
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
268
302
|
};
|
|
269
303
|
}
|
|
270
304
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
@@ -297,6 +331,10 @@ var AuthApi = class {
|
|
|
297
331
|
function rec2(v) {
|
|
298
332
|
return v && typeof v === "object" ? v : {};
|
|
299
333
|
}
|
|
334
|
+
var vcOf = (r) => {
|
|
335
|
+
const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
|
|
336
|
+
return vc != null ? String(vc) : null;
|
|
337
|
+
};
|
|
300
338
|
var SignApi = class {
|
|
301
339
|
constructor(http, creds, defaults) {
|
|
302
340
|
this.http = http;
|
|
@@ -306,46 +344,155 @@ var SignApi = class {
|
|
|
306
344
|
http;
|
|
307
345
|
creds;
|
|
308
346
|
defaults;
|
|
309
|
-
body(digestB64, interactions, opts) {
|
|
347
|
+
body(digestB64, interactions, opts, defAlg) {
|
|
348
|
+
const sign = {
|
|
349
|
+
digest: digestB64,
|
|
350
|
+
hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
|
|
351
|
+
signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
|
|
352
|
+
relyingPartyName: this.creds.rpName,
|
|
353
|
+
interactions: interactionsToBase64(interactions),
|
|
354
|
+
initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
|
|
355
|
+
};
|
|
310
356
|
const body = {
|
|
311
357
|
relyingPartyUUID: this.creds.rpUUID,
|
|
312
|
-
relyingPartyName:
|
|
358
|
+
relyingPartyName: sign.relyingPartyName,
|
|
313
359
|
certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
|
|
314
360
|
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
315
361
|
signatureProtocolParameters: {
|
|
316
362
|
digest: digestB64,
|
|
317
|
-
signatureAlgorithm:
|
|
318
|
-
signatureAlgorithmParameters: { hashAlgorithm:
|
|
363
|
+
signatureAlgorithm: sign.signatureAlgorithm,
|
|
364
|
+
signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
|
|
319
365
|
},
|
|
320
|
-
interactions:
|
|
366
|
+
interactions: sign.interactions
|
|
321
367
|
};
|
|
322
|
-
if (
|
|
368
|
+
if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
|
|
323
369
|
if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
|
|
324
370
|
if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
|
|
325
|
-
return body;
|
|
371
|
+
return { body, sign };
|
|
326
372
|
}
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
return {
|
|
373
|
+
async notification(path, body, sign) {
|
|
374
|
+
const sent = Date.now();
|
|
375
|
+
const r = rec2(await this.http.post(path, body));
|
|
376
|
+
return {
|
|
377
|
+
sessionId: String(r.sessionID),
|
|
378
|
+
vc: vcOf(r),
|
|
379
|
+
rpChallenge: "",
|
|
380
|
+
...sign ? { sign } : {},
|
|
381
|
+
expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async deviceLink(path, body, extra) {
|
|
385
|
+
const sent = Date.now();
|
|
386
|
+
const r = rec2(await this.http.post(path, body));
|
|
387
|
+
return {
|
|
388
|
+
sessionId: String(r.sessionID),
|
|
389
|
+
sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
|
|
390
|
+
sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
|
|
391
|
+
deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
|
|
392
|
+
vc: vcOf(r),
|
|
393
|
+
rpChallenge: "",
|
|
394
|
+
receivedAt: Date.now(),
|
|
395
|
+
...extra,
|
|
396
|
+
expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
|
|
397
|
+
};
|
|
331
398
|
}
|
|
332
399
|
/**
|
|
333
400
|
* Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
|
|
334
401
|
* digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
|
|
402
|
+
*
|
|
403
|
+
* ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
|
|
404
|
+
* (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
|
|
335
405
|
*/
|
|
336
406
|
async digestByEtsi(id, digestB64, interactions, opts) {
|
|
337
|
-
const
|
|
338
|
-
return this.
|
|
407
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
408
|
+
return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
|
|
339
409
|
}
|
|
340
|
-
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth
|
|
410
|
+
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
|
|
341
411
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
342
|
-
const
|
|
343
|
-
return this.
|
|
412
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
413
|
+
return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
|
|
344
414
|
}
|
|
345
415
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
346
416
|
async textByEtsi(id, text, interactions, opts) {
|
|
347
417
|
return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
|
|
348
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
|
|
421
|
+
* `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
|
|
422
|
+
* `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
|
|
423
|
+
*/
|
|
424
|
+
async deviceLinkByEtsi(id, digestB64, interactions, opts) {
|
|
425
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
426
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
|
|
427
|
+
}
|
|
428
|
+
/** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
|
|
429
|
+
async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
|
|
430
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
431
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
|
|
432
|
+
}
|
|
433
|
+
// ── Certificate-choice ──
|
|
434
|
+
certChoiceBody(opts) {
|
|
435
|
+
return {
|
|
436
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
437
|
+
relyingPartyName: this.creds.rpName,
|
|
438
|
+
certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
|
|
443
|
+
* `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
|
|
444
|
+
*/
|
|
445
|
+
async certificateChoiceByEtsi(id, opts) {
|
|
446
|
+
return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
|
|
450
|
+
* initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
|
|
451
|
+
*/
|
|
452
|
+
async certificateChoiceDeviceLink(opts) {
|
|
453
|
+
return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
|
|
454
|
+
sessionType: "cert",
|
|
455
|
+
relyingPartyName: this.creds.rpName
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
|
|
459
|
+
signerBody(signer, digestB64, interactions, opts) {
|
|
460
|
+
if (!signer?.documentNumber?.trim()) {
|
|
461
|
+
throw new Error("eid: signer.documentNumber \u0445\u043E\u043E\u0441\u043E\u043D \u2014 validateCertificateChoice-\u0438\u0439\u043D \u04AF\u0440 \u0434\u04AF\u043D\u0433 \u04E9\u0433");
|
|
462
|
+
}
|
|
463
|
+
const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
|
|
464
|
+
const want = hashLen(out.sign.hashAlgorithm);
|
|
465
|
+
const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
|
|
466
|
+
if (!d || d.length !== want || d.toString("base64") !== digestB64) {
|
|
467
|
+
throw new Error(`eid: digest \u043D\u044C \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 base64, ${out.sign.hashAlgorithm}-\u0438\u0439\u043D \u0443\u0440\u0442 (${want} \u0431\u0430\u0439\u0442) \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
|
|
473
|
+
* etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
|
|
474
|
+
* хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
|
|
475
|
+
*
|
|
476
|
+
* `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
|
|
477
|
+
* бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
|
|
478
|
+
* 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
|
|
479
|
+
* `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
|
|
480
|
+
* s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
|
|
481
|
+
*/
|
|
482
|
+
async digestForSigner(signer, digestB64, interactions, opts) {
|
|
483
|
+
const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
|
|
484
|
+
const linked = signer.certChoiceSessionId?.trim();
|
|
485
|
+
if (linked) {
|
|
486
|
+
body.linkedSessionID = linked;
|
|
487
|
+
return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
|
|
488
|
+
}
|
|
489
|
+
return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
|
|
490
|
+
}
|
|
491
|
+
/** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
|
|
492
|
+
async deviceLinkForSigner(signer, digestB64, interactions, opts) {
|
|
493
|
+
const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
|
|
494
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
|
|
495
|
+
}
|
|
349
496
|
};
|
|
350
497
|
|
|
351
498
|
// src/session.ts
|
|
@@ -1106,13 +1253,19 @@ var ResponseValidator = class {
|
|
|
1106
1253
|
* @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
|
|
1107
1254
|
*/
|
|
1108
1255
|
validateSign(result, digestB64, opts = {}) {
|
|
1109
|
-
const
|
|
1256
|
+
const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
|
|
1257
|
+
const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
|
|
1110
1258
|
const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
|
|
1111
1259
|
if (expectedFlowType && result.flowType !== expectedFlowType) {
|
|
1112
1260
|
throw new ValidationError(`flowType ${result.flowType} \u2260 \u044D\u0445\u043B\u04AF\u04AF\u043B\u0441\u044D\u043D \u0443\u0440\u0441\u0433\u0430\u043B ${expectedFlowType} (relay \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)`);
|
|
1113
1261
|
}
|
|
1262
|
+
if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
|
|
1263
|
+
throw new ValidationError(
|
|
1264
|
+
`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D certificate-choice-\u043E\u043E\u0440 \u0441\u043E\u043D\u0433\u043E\u0441\u043E\u043D\u0442\u043E\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (serial ${leaf.serialNumber} \u2260 ${o.expectedCertificate.serialNumber})`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1114
1267
|
const digest = Buffer.from(digestB64, "base64");
|
|
1115
|
-
const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
|
|
1268
|
+
const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
|
|
1116
1269
|
if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1117
1270
|
if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
|
|
1118
1271
|
throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u04E9\u0433\u0441\u04E9\u043D digest-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
@@ -1126,6 +1279,24 @@ var ResponseValidator = class {
|
|
|
1126
1279
|
parsedSubject: parseSubject(leaf)
|
|
1127
1280
|
};
|
|
1128
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
|
|
1284
|
+
* documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
|
|
1285
|
+
* issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
|
|
1286
|
+
* гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
|
|
1287
|
+
*/
|
|
1288
|
+
validateCertificateChoice(result, sessionId = null) {
|
|
1289
|
+
this.sessionDone(result);
|
|
1290
|
+
const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
|
|
1291
|
+
return {
|
|
1292
|
+
documentNumber: result.documentNumber,
|
|
1293
|
+
certificate: leaf,
|
|
1294
|
+
certificateDerB64: leaf.raw.toString("base64"),
|
|
1295
|
+
certificateLevel: String(result.certificateLevel ?? ""),
|
|
1296
|
+
parsedSubject: parseSubject(leaf),
|
|
1297
|
+
certChoiceSessionId: sessionId
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1129
1300
|
/**
|
|
1130
1301
|
* Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
|
|
1131
1302
|
* (downgrade / alg confusion хаалт).
|
|
@@ -1175,13 +1346,22 @@ var ResponseValidator = class {
|
|
|
1175
1346
|
// ── дотоод ──
|
|
1176
1347
|
/** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
|
|
1177
1348
|
precheck(result, protocol) {
|
|
1178
|
-
|
|
1179
|
-
if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
|
|
1349
|
+
this.sessionDone(result);
|
|
1180
1350
|
if (result.signatureProtocol && result.signatureProtocol !== protocol) {
|
|
1181
1351
|
throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
|
|
1182
1352
|
}
|
|
1183
1353
|
if (!result.certificateDerB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
1184
1354
|
if (!result.signatureValueB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0430\u043B\u0433\u0430");
|
|
1355
|
+
return this.leafOf(result, protocol);
|
|
1356
|
+
}
|
|
1357
|
+
/** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
|
|
1358
|
+
sessionDone(result) {
|
|
1359
|
+
if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
|
|
1360
|
+
if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
|
|
1361
|
+
}
|
|
1362
|
+
/** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
|
|
1363
|
+
leafOf(result, protocol) {
|
|
1364
|
+
if (!result.certificateDerB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
1185
1365
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
1186
1366
|
const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
|
|
1187
1367
|
this.checkValidity(leaf);
|
|
@@ -1598,7 +1778,7 @@ var EidClient = class {
|
|
|
1598
1778
|
});
|
|
1599
1779
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1600
1780
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1601
|
-
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
1781
|
+
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
|
|
1602
1782
|
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1603
1783
|
certificateLevel: level,
|
|
1604
1784
|
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
@@ -1674,6 +1854,37 @@ function buildDeviceLink(p) {
|
|
|
1674
1854
|
const authCode = createHmac("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
|
|
1675
1855
|
return `${unprotected}&authCode=${authCode}`;
|
|
1676
1856
|
}
|
|
1857
|
+
function deviceLinkInput(s, deviceLinkType, lang) {
|
|
1858
|
+
const base = {
|
|
1859
|
+
deviceLinkBase: s.deviceLinkBase ?? "",
|
|
1860
|
+
deviceLinkType,
|
|
1861
|
+
sessionToken: s.sessionToken ?? "",
|
|
1862
|
+
sessionSecret: s.sessionSecret ?? "",
|
|
1863
|
+
lang,
|
|
1864
|
+
receivedAt: s.receivedAt
|
|
1865
|
+
};
|
|
1866
|
+
if (s.sessionType === "sign" && s.sign) {
|
|
1867
|
+
return {
|
|
1868
|
+
...base,
|
|
1869
|
+
sessionType: "sign",
|
|
1870
|
+
digest: s.sign.digest,
|
|
1871
|
+
relyingPartyName: s.sign.relyingPartyName,
|
|
1872
|
+
interactions: s.sign.interactions,
|
|
1873
|
+
initialCallbackUrl: s.sign.initialCallbackUrl
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (s.sessionType === "cert") {
|
|
1877
|
+
return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
|
|
1878
|
+
}
|
|
1879
|
+
return {
|
|
1880
|
+
...base,
|
|
1881
|
+
sessionType: "auth",
|
|
1882
|
+
rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
|
|
1883
|
+
relyingPartyName: s.acsp?.relyingPartyName ?? "",
|
|
1884
|
+
interactions: s.acsp?.interactions ?? "",
|
|
1885
|
+
initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1677
1888
|
function qrDeviceLinkTicker(input, onLink, opts) {
|
|
1678
1889
|
const clock = opts?.clock ?? Date.now;
|
|
1679
1890
|
const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
|
|
@@ -1710,6 +1921,16 @@ function memoryLoginStore() {
|
|
|
1710
1921
|
};
|
|
1711
1922
|
}
|
|
1712
1923
|
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1924
|
+
function forwardedClientIp(headers, trustProxy) {
|
|
1925
|
+
if (trustProxy === false || trustProxy === 0) return void 0;
|
|
1926
|
+
const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
|
|
1927
|
+
if (xff.length) {
|
|
1928
|
+
if (trustProxy === true) return xff[0];
|
|
1929
|
+
const n = Math.floor(trustProxy);
|
|
1930
|
+
return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
|
|
1931
|
+
}
|
|
1932
|
+
return header(headers, "x-real-ip").trim() || void 0;
|
|
1933
|
+
}
|
|
1713
1934
|
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1714
1935
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1715
1936
|
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -1761,11 +1982,12 @@ function safeEqualHex(a, b) {
|
|
|
1761
1982
|
const y = Buffer.from(b, "hex");
|
|
1762
1983
|
return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
|
|
1763
1984
|
}
|
|
1764
|
-
function toIdentity(r, v, flowType) {
|
|
1985
|
+
function toIdentity(sessionId, r, v, flowType) {
|
|
1765
1986
|
const s = v.parsedSubject;
|
|
1766
1987
|
const etsi = s.serialNumber ?? "";
|
|
1767
1988
|
const m = /^PNOMN-([0-9]+)$/.exec(etsi);
|
|
1768
1989
|
return {
|
|
1990
|
+
sessionId,
|
|
1769
1991
|
etsi,
|
|
1770
1992
|
civilId: m ? m[1] : null,
|
|
1771
1993
|
givenName: s.givenName ?? null,
|
|
@@ -1933,7 +2155,8 @@ function createEidLogin(opts) {
|
|
|
1933
2155
|
const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
|
|
1934
2156
|
if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
|
|
1935
2157
|
const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
|
|
1936
|
-
|
|
2158
|
+
const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
|
|
2159
|
+
if (ip && !await rateLimit(`start:${ip}`)) return limited();
|
|
1937
2160
|
if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
|
|
1938
2161
|
let r;
|
|
1939
2162
|
try {
|
|
@@ -1955,7 +2178,7 @@ function createEidLogin(opts) {
|
|
|
1955
2178
|
async function finish(req, tx, r, flow, verifier) {
|
|
1956
2179
|
const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
|
|
1957
2180
|
if (!await store.delete(tx.sessionId)) return void 0;
|
|
1958
|
-
const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
2181
|
+
const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
1959
2182
|
return out;
|
|
1960
2183
|
}
|
|
1961
2184
|
async function status(req) {
|
|
@@ -2038,11 +2261,20 @@ function createEidLogin(opts) {
|
|
|
2038
2261
|
}
|
|
2039
2262
|
return json(404, { error: "not_found" });
|
|
2040
2263
|
}
|
|
2264
|
+
let warnedNoIp = false;
|
|
2041
2265
|
async function handle(req) {
|
|
2042
2266
|
const headers = {};
|
|
2043
2267
|
req.headers.forEach((v, k) => headers[k] = v);
|
|
2044
2268
|
const body = req.method === "POST" ? await req.text() : void 0;
|
|
2045
|
-
const
|
|
2269
|
+
const ip = opts.getClientIp?.(req) || void 0;
|
|
2270
|
+
if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
|
|
2271
|
+
warnedNoIp = true;
|
|
2272
|
+
onError(new Error("eid-login: handle(Request)-\u0434 \u043A\u043B\u0438\u0435\u043D\u0442 IP \u0430\u043B\u0433\u0430 \u2014 `start:<ip>` \u0445\u044F\u0437\u0433\u0430\u0430\u0440 \u0430\u0436\u0438\u043B\u043B\u0430\u0445\u0433\u04AF\u0439; getClientIp \u044D\u0441\u0432\u044D\u043B trustProxy \u04E9\u0433"), {
|
|
2273
|
+
request: { method: req.method, url: req.url, headers },
|
|
2274
|
+
mode: "qr"
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
|
|
2046
2278
|
const h = new Headers();
|
|
2047
2279
|
for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
|
|
2048
2280
|
return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
|
|
@@ -2054,20 +2286,24 @@ export {
|
|
|
2054
2286
|
AuthApi,
|
|
2055
2287
|
AuthenticationError,
|
|
2056
2288
|
CITIZEN_ISSUING_CA_SKI,
|
|
2289
|
+
CertChoiceInvalidError,
|
|
2057
2290
|
DEFAULT_BASE_URL,
|
|
2058
2291
|
DEFAULT_PDF_MAX_BYTES,
|
|
2059
2292
|
DEFAULT_PDF_TIMEOUT_MS,
|
|
2293
|
+
DEFAULT_SIGN_SESSION_TTL_MS,
|
|
2060
2294
|
DeviceLinkError,
|
|
2061
2295
|
EidClient,
|
|
2062
2296
|
EidError,
|
|
2063
2297
|
ForbiddenError,
|
|
2064
2298
|
Http,
|
|
2065
2299
|
NetworkError,
|
|
2300
|
+
NoSigningCertificateError,
|
|
2066
2301
|
ON_BEHALF_ERROR_CODES,
|
|
2067
2302
|
OrganizationApi,
|
|
2068
2303
|
PdfApi,
|
|
2069
2304
|
PersonApi,
|
|
2070
2305
|
RETRYABLE_ERROR_CODES,
|
|
2306
|
+
RequestInvalidError,
|
|
2071
2307
|
ResponseTooLargeError,
|
|
2072
2308
|
ResponseValidator,
|
|
2073
2309
|
SessionApi,
|
|
@@ -2080,10 +2316,13 @@ export {
|
|
|
2080
2316
|
buildDeviceLink,
|
|
2081
2317
|
createEidLogin,
|
|
2082
2318
|
deviceLink,
|
|
2319
|
+
deviceLinkInput,
|
|
2320
|
+
forwardedClientIp,
|
|
2083
2321
|
interactionsToBase64,
|
|
2084
2322
|
isMobileUserAgent,
|
|
2085
2323
|
memoryLoginStore,
|
|
2086
2324
|
openOrShowQR,
|
|
2325
|
+
parseExpiresAt,
|
|
2087
2326
|
parseRetryAfter,
|
|
2088
2327
|
parseSessionResult,
|
|
2089
2328
|
parseSubject,
|
|
@@ -2091,7 +2330,9 @@ export {
|
|
|
2091
2330
|
qrDeviceLinkTicker,
|
|
2092
2331
|
randomChallenge,
|
|
2093
2332
|
resolveBaseUrl,
|
|
2333
|
+
sessionExpiry,
|
|
2094
2334
|
sha256Base64,
|
|
2335
|
+
startExpiry,
|
|
2095
2336
|
tokenAuthDigest,
|
|
2096
2337
|
tokenLimitOf,
|
|
2097
2338
|
userChallengeOf,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gerege-systems/eid-mongolia-sdk",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup src/index.ts src/browser.ts --format esm,cjs --dts --clean && tsup src/button.ts --format esm,cjs --dts --tsconfig tsconfig.button.json",
|
|
37
|
-
"test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/portal-gaps.test.ts test/chain.test.ts test/qr.test.ts test/login.test.ts test/button.test.ts",
|
|
37
|
+
"test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/portal-gaps.test.ts test/chain.test.ts test/qr.test.ts test/login.test.ts test/button.test.ts test/sign-gaps.test.ts test/session-start.test.ts",
|
|
38
38
|
"test:contract": "npm run build && node --experimental-strip-types test/contract.test.ts",
|
|
39
39
|
"lint": "tsc --noEmit && tsc --noEmit -p tsconfig.button.json"
|
|
40
40
|
},
|