@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 +105 -4
- package/dist/{browser-C6JHm0lR.d.cts → browser-CuMH5IVG.d.cts} +40 -1
- package/dist/{browser-C6JHm0lR.d.ts → browser-CuMH5IVG.d.ts} +40 -1
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +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/index.cjs +244 -26
- package/dist/index.d.cts +136 -8
- package/dist/index.d.ts +136 -8
- package/dist/index.js +240 -26
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -138,6 +138,11 @@ var RP_ROUTES = {
|
|
|
138
138
|
authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
|
|
139
139
|
signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
|
|
140
140
|
signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
|
|
141
|
+
signNotificationLinked: { method: "POST", path: "/signature/notification/linked/{documentNumber}" },
|
|
142
|
+
signDeviceLinkEtsi: { method: "POST", path: "/signature/device-link/etsi/{semanticsIdentifier}" },
|
|
143
|
+
signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
|
|
144
|
+
certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
|
|
145
|
+
certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
|
|
141
146
|
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
142
147
|
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
143
148
|
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
@@ -264,7 +269,8 @@ var AuthApi = class {
|
|
|
264
269
|
vc: vc != null ? String(vc) : null,
|
|
265
270
|
rpChallenge: acsp.rpChallenge,
|
|
266
271
|
acsp,
|
|
267
|
-
receivedAt: Date.now()
|
|
272
|
+
receivedAt: Date.now(),
|
|
273
|
+
sessionType: "auth"
|
|
268
274
|
};
|
|
269
275
|
}
|
|
270
276
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
@@ -293,10 +299,20 @@ var AuthApi = class {
|
|
|
293
299
|
}
|
|
294
300
|
};
|
|
295
301
|
|
|
302
|
+
// src/types.ts
|
|
303
|
+
var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
|
|
304
|
+
function sessionExpiry(sentAtMs, ttlMs) {
|
|
305
|
+
return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
|
|
306
|
+
}
|
|
307
|
+
|
|
296
308
|
// src/sign.ts
|
|
297
309
|
function rec2(v) {
|
|
298
310
|
return v && typeof v === "object" ? v : {};
|
|
299
311
|
}
|
|
312
|
+
var vcOf = (r) => {
|
|
313
|
+
const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
|
|
314
|
+
return vc != null ? String(vc) : null;
|
|
315
|
+
};
|
|
300
316
|
var SignApi = class {
|
|
301
317
|
constructor(http, creds, defaults) {
|
|
302
318
|
this.http = http;
|
|
@@ -306,46 +322,155 @@ var SignApi = class {
|
|
|
306
322
|
http;
|
|
307
323
|
creds;
|
|
308
324
|
defaults;
|
|
309
|
-
body(digestB64, interactions, opts) {
|
|
325
|
+
body(digestB64, interactions, opts, defAlg) {
|
|
326
|
+
const sign = {
|
|
327
|
+
digest: digestB64,
|
|
328
|
+
hashAlgorithm: opts?.hashAlgorithm ?? "SHA-256",
|
|
329
|
+
signatureAlgorithm: opts?.signatureAlgorithm ?? defAlg,
|
|
330
|
+
relyingPartyName: this.creds.rpName,
|
|
331
|
+
interactions: interactionsToBase64(interactions),
|
|
332
|
+
initialCallbackUrl: opts?.callbackUrl?.trim() ?? ""
|
|
333
|
+
};
|
|
310
334
|
const body = {
|
|
311
335
|
relyingPartyUUID: this.creds.rpUUID,
|
|
312
|
-
relyingPartyName:
|
|
336
|
+
relyingPartyName: sign.relyingPartyName,
|
|
313
337
|
certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel,
|
|
314
338
|
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
315
339
|
signatureProtocolParameters: {
|
|
316
340
|
digest: digestB64,
|
|
317
|
-
signatureAlgorithm:
|
|
318
|
-
signatureAlgorithmParameters: { hashAlgorithm:
|
|
341
|
+
signatureAlgorithm: sign.signatureAlgorithm,
|
|
342
|
+
signatureAlgorithmParameters: { hashAlgorithm: sign.hashAlgorithm }
|
|
319
343
|
},
|
|
320
|
-
interactions:
|
|
344
|
+
interactions: sign.interactions
|
|
321
345
|
};
|
|
322
|
-
if (
|
|
346
|
+
if (sign.initialCallbackUrl) body.initialCallbackUrl = sign.initialCallbackUrl;
|
|
323
347
|
if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
|
|
324
348
|
if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
|
|
325
|
-
return body;
|
|
349
|
+
return { body, sign };
|
|
350
|
+
}
|
|
351
|
+
async notification(path, body, sign) {
|
|
352
|
+
const sent = Date.now();
|
|
353
|
+
const r = rec2(await this.http.post(path, body));
|
|
354
|
+
return {
|
|
355
|
+
sessionId: String(r.sessionID),
|
|
356
|
+
vc: vcOf(r),
|
|
357
|
+
rpChallenge: "",
|
|
358
|
+
...sign ? { sign } : {},
|
|
359
|
+
expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
|
|
360
|
+
};
|
|
326
361
|
}
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
return {
|
|
362
|
+
async deviceLink(path, body, extra) {
|
|
363
|
+
const sent = Date.now();
|
|
364
|
+
const r = rec2(await this.http.post(path, body));
|
|
365
|
+
return {
|
|
366
|
+
sessionId: String(r.sessionID),
|
|
367
|
+
sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
|
|
368
|
+
sessionSecret: r.sessionSecret != null ? String(r.sessionSecret) : null,
|
|
369
|
+
deviceLinkBase: r.deviceLinkBase != null ? String(r.deviceLinkBase) : null,
|
|
370
|
+
vc: vcOf(r),
|
|
371
|
+
rpChallenge: "",
|
|
372
|
+
receivedAt: Date.now(),
|
|
373
|
+
...extra,
|
|
374
|
+
expiresAt: sessionExpiry(sent, this.defaults.sessionTtlMs)
|
|
375
|
+
};
|
|
331
376
|
}
|
|
332
377
|
/**
|
|
333
378
|
* Бэлэн digest-ээр push sign (РД/civil_id/ETSI). Баримт (PDF/файл)-ын байтын SHA-256
|
|
334
379
|
* digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
|
|
380
|
+
*
|
|
381
|
+
* ⚠️ Олон утастай иргэнд CA etsi-г ЭХНИЙ идэвхтэй төхөөрөмж рүү шийднэ. Гэрчилгээг урьдчилж мэдэх шаардлагатай бол
|
|
382
|
+
* (PAdES/CMS) {@link certificateChoiceByEtsi} → {@link digestForSigner}.
|
|
335
383
|
*/
|
|
336
384
|
async digestByEtsi(id, digestB64, interactions, opts) {
|
|
337
|
-
const
|
|
338
|
-
return this.
|
|
385
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
386
|
+
return this.notification(routePath(RP_ROUTES.signNotificationEtsi, id), body, sign);
|
|
339
387
|
}
|
|
340
|
-
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth
|
|
388
|
+
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth / certificate-choice-оос гарсан documentNumber дээр). */
|
|
341
389
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
342
|
-
const
|
|
343
|
-
return this.
|
|
390
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
391
|
+
return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
|
|
344
392
|
}
|
|
345
393
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
346
394
|
async textByEtsi(id, text, interactions, opts) {
|
|
347
395
|
return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
|
|
348
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* QR / Web2App / App2App sign — тодорхой иргэн (РД/civil_id/ETSI). Холбоосыг backend-д
|
|
399
|
+
* `buildDeviceLink(deviceLinkInput(s, type, lang))`-ээр; Web2App/App2App-д `callbackUrl` заавал.
|
|
400
|
+
* `validateSign`-д `expectedFlowType` = харуулсан холбоосын төрөл.
|
|
401
|
+
*/
|
|
402
|
+
async deviceLinkByEtsi(id, digestB64, interactions, opts) {
|
|
403
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
404
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkEtsi, id), body, { sessionType: "sign", sign });
|
|
405
|
+
}
|
|
406
|
+
/** QR / Web2App / App2App sign — баримтын дугаараар (тодорхой төхөөрөмж). */
|
|
407
|
+
async deviceLinkByDocument(documentNumber, digestB64, interactions, opts) {
|
|
408
|
+
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
409
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, documentNumber), body, { sessionType: "sign", sign });
|
|
410
|
+
}
|
|
411
|
+
// ── Certificate-choice ──
|
|
412
|
+
certChoiceBody(opts) {
|
|
413
|
+
return {
|
|
414
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
415
|
+
relyingPartyName: this.creds.rpName,
|
|
416
|
+
certificateLevel: opts?.certificateLevel ?? this.defaults.certificateLevel
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Push: иргэний (РД/civil_id/ETSI) утас руу гарын үсгийн гэрчилгээ сонгуулах хүсэлт (PIN-гүй). Үр дүнг
|
|
421
|
+
* `session.waitForResult` → `validator.validateCertificateChoice`. CERT эрх шаардана.
|
|
422
|
+
*/
|
|
423
|
+
async certificateChoiceByEtsi(id, opts) {
|
|
424
|
+
return this.notification(routePath(RP_ROUTES.certChoiceEtsi, id), this.certChoiceBody(opts));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* QR: иргэн хэн болохыг урьдчилж мэдэхгүй үед гэрчилгээ сонгуулах. CA-ийн certificate-choice хүсэлт
|
|
428
|
+
* initialCallbackUrl авдаггүй тул ЗӨВХӨН QR холбоос (`deviceLinkInput(s, "QR", lang)`). CERT эрх.
|
|
429
|
+
*/
|
|
430
|
+
async certificateChoiceDeviceLink(opts) {
|
|
431
|
+
return this.deviceLink(routePath(RP_ROUTES.certChoiceDeviceLink), this.certChoiceBody(opts), {
|
|
432
|
+
sessionType: "cert",
|
|
433
|
+
relyingPartyName: this.creds.rpName
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// ── Гэрчилгээ урьдчилж мэдэгдэх гарын үсэг (client-side PAdES / CMS) ──
|
|
437
|
+
signerBody(signer, digestB64, interactions, opts) {
|
|
438
|
+
if (!signer?.documentNumber?.trim()) {
|
|
439
|
+
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");
|
|
440
|
+
}
|
|
441
|
+
const out = this.body(digestB64, interactions, opts, "sha256WithRSAEncryption");
|
|
442
|
+
const want = hashLen(out.sign.hashAlgorithm);
|
|
443
|
+
const d = /^[A-Za-z0-9+/]*={0,2}$/.test(digestB64) ? Buffer.from(digestB64, "base64") : null;
|
|
444
|
+
if (!d || d.length !== want || d.toString("base64") !== digestB64) {
|
|
445
|
+
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`);
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Certificate-choice-оор сонгосон ЯГ ТЭР төхөөрөмж (`signer.documentNumber`) рүү push sign. Олон утастай иргэнд
|
|
451
|
+
* etsi-ээр эхлүүлбэл өөр утас (өөр гэрчилгээ) зурж, гэрчилгээг урьдчилж оруулсан CMS (PAdES signingCertificateV2)
|
|
452
|
+
* хүчингүй болдог — тиймээс digest-ийг ҮРГЭЛЖ documentNumber-аар зуруулна.
|
|
453
|
+
*
|
|
454
|
+
* `signer.certChoiceSessionId` байвал `/signature/notification/linked/{documentNumber}` (`linkedSessionID`), эс
|
|
455
|
+
* бөгөөс `/signature/notification/document/{documentNumber}`. Default `sha256WithRSAEncryption` + SHA-256 (digest =
|
|
456
|
+
* 32 байт DTBS); digest-ийн урт буруу бол хүсэлт явахгүй. Хариуг:
|
|
457
|
+
* `validator.validateSign(r, s.sign.digest, { hashAlgorithm: s.sign.hashAlgorithm, expectedAlgorithm:
|
|
458
|
+
* s.sign.signatureAlgorithm, expectedCertificate: signer.certificate, expectedFlowType: "Notification" })`.
|
|
459
|
+
*/
|
|
460
|
+
async digestForSigner(signer, digestB64, interactions, opts) {
|
|
461
|
+
const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
|
|
462
|
+
const linked = signer.certChoiceSessionId?.trim();
|
|
463
|
+
if (linked) {
|
|
464
|
+
body.linkedSessionID = linked;
|
|
465
|
+
return this.notification(routePath(RP_ROUTES.signNotificationLinked, signer.documentNumber), body, sign);
|
|
466
|
+
}
|
|
467
|
+
return this.notification(routePath(RP_ROUTES.signNotificationDocument, signer.documentNumber), body, sign);
|
|
468
|
+
}
|
|
469
|
+
/** {@link digestForSigner}-ийн QR / Web2App / App2App хувилбар: `/signature/device-link/document/{documentNumber}`. */
|
|
470
|
+
async deviceLinkForSigner(signer, digestB64, interactions, opts) {
|
|
471
|
+
const { body, sign } = this.signerBody(signer, digestB64, interactions, opts);
|
|
472
|
+
return this.deviceLink(routePath(RP_ROUTES.signDeviceLinkDocument, signer.documentNumber), body, { sessionType: "sign", sign });
|
|
473
|
+
}
|
|
349
474
|
};
|
|
350
475
|
|
|
351
476
|
// src/session.ts
|
|
@@ -1106,13 +1231,19 @@ var ResponseValidator = class {
|
|
|
1106
1231
|
* @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
|
|
1107
1232
|
*/
|
|
1108
1233
|
validateSign(result, digestB64, opts = {}) {
|
|
1109
|
-
const
|
|
1234
|
+
const o = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
|
|
1235
|
+
const { hashAlgorithm = "SHA-256", expectedFlowType } = o;
|
|
1110
1236
|
const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
|
|
1111
1237
|
if (expectedFlowType && result.flowType !== expectedFlowType) {
|
|
1112
1238
|
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
1239
|
}
|
|
1240
|
+
if (o.expectedCertificate && !leaf.raw.equals(o.expectedCertificate.raw)) {
|
|
1241
|
+
throw new ValidationError(
|
|
1242
|
+
`\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})`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1114
1245
|
const digest = Buffer.from(digestB64, "base64");
|
|
1115
|
-
const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
|
|
1246
|
+
const { alg, hash } = this.algorithmOf(result, leaf, o.expectedAlgorithm ?? null, hashAlgorithm);
|
|
1116
1247
|
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
1248
|
if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
|
|
1118
1249
|
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 +1257,24 @@ var ResponseValidator = class {
|
|
|
1126
1257
|
parsedSubject: parseSubject(leaf)
|
|
1127
1258
|
};
|
|
1128
1259
|
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Certificate-choice session-ий хариуг шалгана: COMPLETE/OK (эс бөгөөс `SessionFailedError`), гэрчилгээ ба
|
|
1262
|
+
* documentNumber байгаа, хугацаа, certLevel, зориулалт (гарын үсгийн — contentCommitment, `checkKeyUsage` үед), гинж,
|
|
1263
|
+
* issuer. Энэ хариунд гарын үсэг БАЙХГҮЙ — иргэнийг таниагүй (нэвтрэлт биш); зөвхөн дараагийн sign session-ий
|
|
1264
|
+
* гэрчилгээг урьдчилж мэдэхэд. `sessionId` — certificate-choice session.
|
|
1265
|
+
*/
|
|
1266
|
+
validateCertificateChoice(result, sessionId = null) {
|
|
1267
|
+
this.sessionDone(result);
|
|
1268
|
+
const leaf = this.leafOf(result, "RAW_DIGEST_SIGNATURE");
|
|
1269
|
+
return {
|
|
1270
|
+
documentNumber: result.documentNumber,
|
|
1271
|
+
certificate: leaf,
|
|
1272
|
+
certificateDerB64: leaf.raw.toString("base64"),
|
|
1273
|
+
certificateLevel: String(result.certificateLevel ?? ""),
|
|
1274
|
+
parsedSubject: parseSubject(leaf),
|
|
1275
|
+
certChoiceSessionId: sessionId
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1129
1278
|
/**
|
|
1130
1279
|
* Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
|
|
1131
1280
|
* (downgrade / alg confusion хаалт).
|
|
@@ -1175,13 +1324,22 @@ var ResponseValidator = class {
|
|
|
1175
1324
|
// ── дотоод ──
|
|
1176
1325
|
/** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
|
|
1177
1326
|
precheck(result, protocol) {
|
|
1178
|
-
|
|
1179
|
-
if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
|
|
1327
|
+
this.sessionDone(result);
|
|
1180
1328
|
if (result.signatureProtocol && result.signatureProtocol !== protocol) {
|
|
1181
1329
|
throw new ValidationError(`signatureProtocol ${result.signatureProtocol} \u2260 ${protocol}`);
|
|
1182
1330
|
}
|
|
1183
1331
|
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
1332
|
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");
|
|
1333
|
+
return this.leafOf(result, protocol);
|
|
1334
|
+
}
|
|
1335
|
+
/** COMPLETE + OK; амжилтгүй endResult → SessionFailedError. */
|
|
1336
|
+
sessionDone(result) {
|
|
1337
|
+
if (result.state !== "COMPLETE") throw new ValidationError(`Session \u0434\u0443\u0443\u0441\u0430\u0430\u0433\u04AF\u0439 (state=${result.state})`);
|
|
1338
|
+
if (result.endResult !== "OK") throw new SessionFailedError(result.endResult ?? "UNKNOWN");
|
|
1339
|
+
}
|
|
1340
|
+
/** Хариуны гэрчилгээ: documentNumber, задлалт, хугацаа, certLevel, зориулалт (protocol), гинж, issuer. */
|
|
1341
|
+
leafOf(result, protocol) {
|
|
1342
|
+
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
1343
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
1186
1344
|
const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
|
|
1187
1345
|
this.checkValidity(leaf);
|
|
@@ -1598,7 +1756,7 @@ var EidClient = class {
|
|
|
1598
1756
|
});
|
|
1599
1757
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1600
1758
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1601
|
-
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
1759
|
+
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
|
|
1602
1760
|
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1603
1761
|
certificateLevel: level,
|
|
1604
1762
|
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
@@ -1674,6 +1832,37 @@ function buildDeviceLink(p) {
|
|
|
1674
1832
|
const authCode = createHmac("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
|
|
1675
1833
|
return `${unprotected}&authCode=${authCode}`;
|
|
1676
1834
|
}
|
|
1835
|
+
function deviceLinkInput(s, deviceLinkType, lang) {
|
|
1836
|
+
const base = {
|
|
1837
|
+
deviceLinkBase: s.deviceLinkBase ?? "",
|
|
1838
|
+
deviceLinkType,
|
|
1839
|
+
sessionToken: s.sessionToken ?? "",
|
|
1840
|
+
sessionSecret: s.sessionSecret ?? "",
|
|
1841
|
+
lang,
|
|
1842
|
+
receivedAt: s.receivedAt
|
|
1843
|
+
};
|
|
1844
|
+
if (s.sessionType === "sign" && s.sign) {
|
|
1845
|
+
return {
|
|
1846
|
+
...base,
|
|
1847
|
+
sessionType: "sign",
|
|
1848
|
+
digest: s.sign.digest,
|
|
1849
|
+
relyingPartyName: s.sign.relyingPartyName,
|
|
1850
|
+
interactions: s.sign.interactions,
|
|
1851
|
+
initialCallbackUrl: s.sign.initialCallbackUrl
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
if (s.sessionType === "cert") {
|
|
1855
|
+
return { ...base, sessionType: "cert", relyingPartyName: s.relyingPartyName ?? "", interactions: "" };
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
...base,
|
|
1859
|
+
sessionType: "auth",
|
|
1860
|
+
rpChallenge: s.acsp?.rpChallenge ?? s.rpChallenge,
|
|
1861
|
+
relyingPartyName: s.acsp?.relyingPartyName ?? "",
|
|
1862
|
+
interactions: s.acsp?.interactions ?? "",
|
|
1863
|
+
initialCallbackUrl: s.acsp?.initialCallbackUrl ?? ""
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1677
1866
|
function qrDeviceLinkTicker(input, onLink, opts) {
|
|
1678
1867
|
const clock = opts?.clock ?? Date.now;
|
|
1679
1868
|
const tick = () => onLink(buildDeviceLink({ ...input, deviceLinkType: "QR", now: clock() }));
|
|
@@ -1710,6 +1899,16 @@ function memoryLoginStore() {
|
|
|
1710
1899
|
};
|
|
1711
1900
|
}
|
|
1712
1901
|
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1902
|
+
function forwardedClientIp(headers, trustProxy) {
|
|
1903
|
+
if (trustProxy === false || trustProxy === 0) return void 0;
|
|
1904
|
+
const xff = header(headers, "x-forwarded-for").split(",").map((x) => x.trim()).filter(Boolean);
|
|
1905
|
+
if (xff.length) {
|
|
1906
|
+
if (trustProxy === true) return xff[0];
|
|
1907
|
+
const n = Math.floor(trustProxy);
|
|
1908
|
+
return n >= 1 && n <= xff.length ? xff[xff.length - n] : void 0;
|
|
1909
|
+
}
|
|
1910
|
+
return header(headers, "x-real-ip").trim() || void 0;
|
|
1911
|
+
}
|
|
1713
1912
|
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1714
1913
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1715
1914
|
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -1761,11 +1960,12 @@ function safeEqualHex(a, b) {
|
|
|
1761
1960
|
const y = Buffer.from(b, "hex");
|
|
1762
1961
|
return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
|
|
1763
1962
|
}
|
|
1764
|
-
function toIdentity(r, v, flowType) {
|
|
1963
|
+
function toIdentity(sessionId, r, v, flowType) {
|
|
1765
1964
|
const s = v.parsedSubject;
|
|
1766
1965
|
const etsi = s.serialNumber ?? "";
|
|
1767
1966
|
const m = /^PNOMN-([0-9]+)$/.exec(etsi);
|
|
1768
1967
|
return {
|
|
1968
|
+
sessionId,
|
|
1769
1969
|
etsi,
|
|
1770
1970
|
civilId: m ? m[1] : null,
|
|
1771
1971
|
givenName: s.givenName ?? null,
|
|
@@ -1933,7 +2133,8 @@ function createEidLogin(opts) {
|
|
|
1933
2133
|
const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
|
|
1934
2134
|
if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
|
|
1935
2135
|
const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
|
|
1936
|
-
|
|
2136
|
+
const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
|
|
2137
|
+
if (ip && !await rateLimit(`start:${ip}`)) return limited();
|
|
1937
2138
|
if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
|
|
1938
2139
|
let r;
|
|
1939
2140
|
try {
|
|
@@ -1955,7 +2156,7 @@ function createEidLogin(opts) {
|
|
|
1955
2156
|
async function finish(req, tx, r, flow, verifier) {
|
|
1956
2157
|
const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
|
|
1957
2158
|
if (!await store.delete(tx.sessionId)) return void 0;
|
|
1958
|
-
const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
2159
|
+
const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
1959
2160
|
return out;
|
|
1960
2161
|
}
|
|
1961
2162
|
async function status(req) {
|
|
@@ -2038,11 +2239,20 @@ function createEidLogin(opts) {
|
|
|
2038
2239
|
}
|
|
2039
2240
|
return json(404, { error: "not_found" });
|
|
2040
2241
|
}
|
|
2242
|
+
let warnedNoIp = false;
|
|
2041
2243
|
async function handle(req) {
|
|
2042
2244
|
const headers = {};
|
|
2043
2245
|
req.headers.forEach((v, k) => headers[k] = v);
|
|
2044
2246
|
const body = req.method === "POST" ? await req.text() : void 0;
|
|
2045
|
-
const
|
|
2247
|
+
const ip = opts.getClientIp?.(req) || void 0;
|
|
2248
|
+
if (!ip && !opts.getClientIp && !opts.trustProxy && !warnedNoIp) {
|
|
2249
|
+
warnedNoIp = true;
|
|
2250
|
+
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"), {
|
|
2251
|
+
request: { method: req.method, url: req.url, headers },
|
|
2252
|
+
mode: "qr"
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
|
|
2046
2256
|
const h = new Headers();
|
|
2047
2257
|
for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
|
|
2048
2258
|
return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
|
|
@@ -2057,6 +2267,7 @@ export {
|
|
|
2057
2267
|
DEFAULT_BASE_URL,
|
|
2058
2268
|
DEFAULT_PDF_MAX_BYTES,
|
|
2059
2269
|
DEFAULT_PDF_TIMEOUT_MS,
|
|
2270
|
+
DEFAULT_SIGN_SESSION_TTL_MS,
|
|
2060
2271
|
DeviceLinkError,
|
|
2061
2272
|
EidClient,
|
|
2062
2273
|
EidError,
|
|
@@ -2080,6 +2291,8 @@ export {
|
|
|
2080
2291
|
buildDeviceLink,
|
|
2081
2292
|
createEidLogin,
|
|
2082
2293
|
deviceLink,
|
|
2294
|
+
deviceLinkInput,
|
|
2295
|
+
forwardedClientIp,
|
|
2083
2296
|
interactionsToBase64,
|
|
2084
2297
|
isMobileUserAgent,
|
|
2085
2298
|
memoryLoginStore,
|
|
@@ -2091,6 +2304,7 @@ export {
|
|
|
2091
2304
|
qrDeviceLinkTicker,
|
|
2092
2305
|
randomChallenge,
|
|
2093
2306
|
resolveBaseUrl,
|
|
2307
|
+
sessionExpiry,
|
|
2094
2308
|
sha256Base64,
|
|
2095
2309
|
tokenAuthDigest,
|
|
2096
2310
|
tokenLimitOf,
|
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.1",
|
|
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",
|
|
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
|
},
|