@gerege-systems/eid-mongolia-sdk 0.6.1 → 0.8.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/README.md +136 -3
- package/dist/{browser-BU9ShPWO.d.cts → browser-Db82DlYN.d.cts} +80 -3
- package/dist/{browser-BU9ShPWO.d.ts → browser-Db82DlYN.d.ts} +80 -3
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/{chunk-Q5TQMCJC.js → chunk-NXDKQZIW.js} +18 -1
- package/dist/index.cjs +451 -10
- package/dist/index.d.cts +215 -4
- package/dist/index.d.ts +215 -4
- package/dist/index.js +427 -10
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
EidError,
|
|
6
6
|
ForbiddenError,
|
|
7
7
|
NetworkError,
|
|
8
|
+
ON_BEHALF_ERROR_CODES,
|
|
8
9
|
SessionFailedError,
|
|
9
10
|
ValidationError,
|
|
10
11
|
deviceLink,
|
|
11
12
|
isMobileUserAgent,
|
|
12
13
|
openOrShowQR
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-NXDKQZIW.js";
|
|
14
15
|
|
|
15
16
|
// src/http.ts
|
|
16
17
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -20,21 +21,35 @@ var Http = class {
|
|
|
20
21
|
}
|
|
21
22
|
cfg;
|
|
22
23
|
post(path, body, timeoutMs) {
|
|
23
|
-
return this.
|
|
24
|
+
return this.json("POST", path, JSON.stringify(body), "application/json", timeoutMs);
|
|
24
25
|
}
|
|
25
26
|
get(path, timeoutMs) {
|
|
26
|
-
return this.
|
|
27
|
+
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
/** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
|
|
30
|
+
postMultipart(path, form, timeoutMs) {
|
|
31
|
+
return this.json("POST", path, form, void 0, timeoutMs);
|
|
32
|
+
}
|
|
33
|
+
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
|
|
34
|
+
async getBytes(path, timeoutMs) {
|
|
35
|
+
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
36
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
37
|
+
}
|
|
38
|
+
async json(method, path, body, contentType, timeoutMs) {
|
|
39
|
+
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
return text ? JSON.parse(text) : {};
|
|
42
|
+
}
|
|
43
|
+
async send(method, path, body, contentType, timeoutMs) {
|
|
29
44
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
30
45
|
const url = this.cfg.baseUrl + path;
|
|
31
46
|
const init = {
|
|
32
47
|
method,
|
|
33
48
|
headers: {
|
|
34
49
|
Authorization: `Bearer ${this.cfg.apiSecret}`,
|
|
35
|
-
...
|
|
50
|
+
...contentType ? { "Content-Type": contentType } : {}
|
|
36
51
|
},
|
|
37
|
-
body
|
|
52
|
+
body,
|
|
38
53
|
signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
39
54
|
};
|
|
40
55
|
if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
|
|
@@ -48,13 +63,32 @@ var Http = class {
|
|
|
48
63
|
}
|
|
49
64
|
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${method} ${path}): ${err.message}`);
|
|
50
65
|
}
|
|
66
|
+
if (res.status < 300) return res;
|
|
51
67
|
const text = await res.text();
|
|
68
|
+
const { error, code } = errorBody(text);
|
|
52
69
|
if (res.status === 401) throw new AuthenticationError("API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401)");
|
|
53
|
-
if (res.status === 403)
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
if (res.status === 403) {
|
|
71
|
+
throw new ForbiddenError(
|
|
72
|
+
error ? `RP-API 403${code ? ` ${code}` : ""}: ${error}` : "IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)",
|
|
73
|
+
code
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
throw new ApiError(res.status, text, code);
|
|
56
77
|
}
|
|
57
78
|
};
|
|
79
|
+
function errorBody(text) {
|
|
80
|
+
try {
|
|
81
|
+
const v = JSON.parse(text);
|
|
82
|
+
if (!v || typeof v !== "object") return {};
|
|
83
|
+
const o = v;
|
|
84
|
+
return {
|
|
85
|
+
error: typeof o.error === "string" && o.error ? o.error : void 0,
|
|
86
|
+
code: typeof o.code === "string" && o.code ? o.code : void 0
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
58
92
|
|
|
59
93
|
// src/crypto.ts
|
|
60
94
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -266,9 +300,32 @@ function parseSessionResult(raw) {
|
|
|
266
300
|
initialCallbackUrl: typeof sig.initialCallbackUrl === "string" ? sig.initialCallbackUrl : null,
|
|
267
301
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
268
302
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
269
|
-
onBehalfOf: parseOrg(r.onBehalfOf)
|
|
303
|
+
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
304
|
+
pdf: parsePdf(r.pdf)
|
|
270
305
|
};
|
|
271
306
|
}
|
|
307
|
+
function parsePdf(raw) {
|
|
308
|
+
const p = asRecord(raw);
|
|
309
|
+
const docID = str(p.docID);
|
|
310
|
+
if (!docID) return null;
|
|
311
|
+
const out = { docID, documentStatus: String(p.documentStatus ?? "") };
|
|
312
|
+
if (p.errorCode != null) out.errorCode = String(p.errorCode);
|
|
313
|
+
if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
|
|
314
|
+
if (p.size != null) out.size = Number(p.size);
|
|
315
|
+
if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
|
|
316
|
+
if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
|
|
317
|
+
const sg = asRecord(p.signer);
|
|
318
|
+
if (sg.etsi != null) {
|
|
319
|
+
out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
|
|
320
|
+
if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
|
|
321
|
+
if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
|
|
322
|
+
}
|
|
323
|
+
const v = asRecord(p.validation);
|
|
324
|
+
if (v.indication != null) {
|
|
325
|
+
out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
272
329
|
function parseSigParams(raw) {
|
|
273
330
|
const p = asRecord(raw);
|
|
274
331
|
const hash = str(p.hashAlgorithm);
|
|
@@ -323,6 +380,76 @@ var SessionApi = class {
|
|
|
323
380
|
}
|
|
324
381
|
};
|
|
325
382
|
|
|
383
|
+
// src/pdf.ts
|
|
384
|
+
function rec3(v) {
|
|
385
|
+
return v && typeof v === "object" ? v : {};
|
|
386
|
+
}
|
|
387
|
+
var PdfApi = class {
|
|
388
|
+
constructor(http, creds, defaults) {
|
|
389
|
+
this.http = http;
|
|
390
|
+
this.creds = creds;
|
|
391
|
+
this.defaults = defaults;
|
|
392
|
+
}
|
|
393
|
+
http;
|
|
394
|
+
creds;
|
|
395
|
+
defaults;
|
|
396
|
+
/** POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. */
|
|
397
|
+
async prepare(input) {
|
|
398
|
+
const request = {
|
|
399
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
400
|
+
relyingPartyName: this.creds.rpName,
|
|
401
|
+
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
402
|
+
signer: input.signer,
|
|
403
|
+
flow: input.flow ?? "notification",
|
|
404
|
+
fileName: input.fileName
|
|
405
|
+
};
|
|
406
|
+
if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
407
|
+
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
408
|
+
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
409
|
+
const form = new FormData();
|
|
410
|
+
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
411
|
+
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
412
|
+
const r = rec3(await this.http.postMultipart("/pdf/sign/prepare", form, 6e4));
|
|
413
|
+
const receivedAt = Date.now();
|
|
414
|
+
const vc = rec3(r.vc).value;
|
|
415
|
+
const dl = rec3(r.deviceLink);
|
|
416
|
+
return {
|
|
417
|
+
sessionId: String(r.sessionID),
|
|
418
|
+
docId: String(r.docID),
|
|
419
|
+
vc: vc != null ? String(vc) : null,
|
|
420
|
+
deviceLink: dl.sessionToken ? {
|
|
421
|
+
sessionToken: String(dl.sessionToken),
|
|
422
|
+
sessionSecret: String(dl.sessionSecret),
|
|
423
|
+
deviceLinkBase: String(dl.deviceLinkBase),
|
|
424
|
+
digest: String(dl.digest),
|
|
425
|
+
interactions: String(dl.interactions)
|
|
426
|
+
} : null,
|
|
427
|
+
receivedAt
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
432
|
+
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
|
|
433
|
+
*/
|
|
434
|
+
document(sessionId) {
|
|
435
|
+
return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// src/organization.ts
|
|
440
|
+
var OrganizationApi = class {
|
|
441
|
+
constructor(http) {
|
|
442
|
+
this.http = http;
|
|
443
|
+
}
|
|
444
|
+
http;
|
|
445
|
+
/** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
|
|
446
|
+
async getRepresentations(personEtsi) {
|
|
447
|
+
const raw = await this.http.get(`/organization/representations/etsi/${encodeURIComponent(personEtsi)}`);
|
|
448
|
+
const list = Array.isArray(raw.representations) ? raw.representations : [];
|
|
449
|
+
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
326
453
|
// src/validator.ts
|
|
327
454
|
import { X509Certificate, constants, createHash as createHash2, publicDecrypt, verify } from "crypto";
|
|
328
455
|
var ResponseValidator = class {
|
|
@@ -677,6 +804,8 @@ function resolveBaseUrl(baseUrl) {
|
|
|
677
804
|
var EidClient = class {
|
|
678
805
|
auth;
|
|
679
806
|
sign;
|
|
807
|
+
pdf;
|
|
808
|
+
organization;
|
|
680
809
|
session;
|
|
681
810
|
validator;
|
|
682
811
|
constructor(cfg) {
|
|
@@ -692,6 +821,8 @@ var EidClient = class {
|
|
|
692
821
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
693
822
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
694
823
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
824
|
+
this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level });
|
|
825
|
+
this.organization = new OrganizationApi(http);
|
|
695
826
|
this.session = new SessionApi(http);
|
|
696
827
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
697
828
|
}
|
|
@@ -766,17 +897,302 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
766
897
|
t.unref?.();
|
|
767
898
|
return () => clearInterval(t);
|
|
768
899
|
}
|
|
900
|
+
|
|
901
|
+
// src/oidc.ts
|
|
902
|
+
import { timingSafeEqual } from "crypto";
|
|
903
|
+
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
904
|
+
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
905
|
+
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
906
|
+
var CLOCK_SKEW_S = 60;
|
|
907
|
+
var CACHE_MS = 60 * 60 * 1e3;
|
|
908
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
909
|
+
var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
910
|
+
"access_denied",
|
|
911
|
+
"login_required",
|
|
912
|
+
"invalid_request",
|
|
913
|
+
"invalid_scope",
|
|
914
|
+
"unauthorized_client",
|
|
915
|
+
"unsupported_response_type",
|
|
916
|
+
"request_not_supported",
|
|
917
|
+
"temporarily_unavailable",
|
|
918
|
+
"server_error",
|
|
919
|
+
"invalid_client",
|
|
920
|
+
"invalid_grant",
|
|
921
|
+
"unsupported_grant_type"
|
|
922
|
+
]);
|
|
923
|
+
var EidOidcError = class extends EidError {
|
|
924
|
+
constructor(code, message, description) {
|
|
925
|
+
super(message);
|
|
926
|
+
this.code = code;
|
|
927
|
+
this.description = description;
|
|
928
|
+
}
|
|
929
|
+
code;
|
|
930
|
+
description;
|
|
931
|
+
};
|
|
932
|
+
var EidOidcClient = class {
|
|
933
|
+
constructor(cfg) {
|
|
934
|
+
this.cfg = cfg;
|
|
935
|
+
if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
936
|
+
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
937
|
+
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
938
|
+
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
939
|
+
this.scope = cfg.scope ?? "openid";
|
|
940
|
+
}
|
|
941
|
+
cfg;
|
|
942
|
+
issuer;
|
|
943
|
+
scope;
|
|
944
|
+
discovery;
|
|
945
|
+
jwks;
|
|
946
|
+
/** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
|
|
947
|
+
discover() {
|
|
948
|
+
const c = this.discovery;
|
|
949
|
+
if (c && Date.now() - c.at < CACHE_MS) return c.d;
|
|
950
|
+
const d = this.fetchDiscovery();
|
|
951
|
+
this.discovery = { at: Date.now(), d };
|
|
952
|
+
d.catch(() => {
|
|
953
|
+
if (this.discovery?.d === d) this.discovery = void 0;
|
|
954
|
+
});
|
|
955
|
+
return d;
|
|
956
|
+
}
|
|
957
|
+
async fetchDiscovery() {
|
|
958
|
+
const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
|
|
959
|
+
if (!d || d.issuer !== this.issuer) {
|
|
960
|
+
throw new EidOidcError("discovery_failed", `OIDC discovery-\u0438\u0439\u043D issuer \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (\u0445\u04AF\u043B\u044D\u044D\u0441\u044D\u043D ${this.issuer})`);
|
|
961
|
+
}
|
|
962
|
+
for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
|
|
963
|
+
if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
|
|
964
|
+
throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return d;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
|
|
971
|
+
* хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
|
|
972
|
+
*/
|
|
973
|
+
async createAuthorization(opts = {}) {
|
|
974
|
+
const d = await this.discover();
|
|
975
|
+
const state = randomB64url(32);
|
|
976
|
+
const nonce = randomB64url(32);
|
|
977
|
+
const codeVerifier = randomB64url(32);
|
|
978
|
+
const u = new URL(d.authorization_endpoint);
|
|
979
|
+
const p = new URLSearchParams({
|
|
980
|
+
response_type: "code",
|
|
981
|
+
client_id: this.cfg.clientId,
|
|
982
|
+
redirect_uri: this.cfg.redirectUri,
|
|
983
|
+
scope: opts.scope ?? this.scope,
|
|
984
|
+
state,
|
|
985
|
+
nonce,
|
|
986
|
+
code_challenge: await pkceChallenge(codeVerifier),
|
|
987
|
+
code_challenge_method: "S256"
|
|
988
|
+
});
|
|
989
|
+
if (opts.display) p.set("display", opts.display);
|
|
990
|
+
u.search = p.toString();
|
|
991
|
+
return { url: u.toString(), state, nonce, codeVerifier };
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
|
|
995
|
+
* id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
|
|
996
|
+
* Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
|
|
997
|
+
*/
|
|
998
|
+
async handleCallback(input, checks) {
|
|
999
|
+
const q = callbackParams(input);
|
|
1000
|
+
const state = q.get("state") ?? "";
|
|
1001
|
+
if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
|
|
1002
|
+
throw new EidOidcError("state_mismatch", "OIDC state \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439 \u2014 \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u0433 \u0434\u0430\u0445\u0438\u043D \u044D\u0445\u043B\u04AF\u04AF\u043B\u043D\u044D \u04AF\u04AF");
|
|
1003
|
+
}
|
|
1004
|
+
const iss = q.get("iss");
|
|
1005
|
+
if (iss !== null && iss !== this.issuer) {
|
|
1006
|
+
throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1007
|
+
}
|
|
1008
|
+
const err = q.get("error");
|
|
1009
|
+
if (err) {
|
|
1010
|
+
const code2 = KNOWN_CODES.has(err) ? err : "server_error";
|
|
1011
|
+
throw new EidOidcError(code2, `eID \u043D\u044D\u0432\u0442\u0440\u044D\u043B\u0442 \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${code2}`, q.get("error_description") ?? void 0);
|
|
1012
|
+
}
|
|
1013
|
+
const code = q.get("code") ?? "";
|
|
1014
|
+
if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
|
|
1015
|
+
const idToken = await this.exchangeCode(code, checks.codeVerifier);
|
|
1016
|
+
const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
|
|
1017
|
+
return { claims, idToken };
|
|
1018
|
+
}
|
|
1019
|
+
/** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
|
|
1020
|
+
async exchangeCode(code, codeVerifier) {
|
|
1021
|
+
const d = await this.discover();
|
|
1022
|
+
const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
|
|
1023
|
+
const res = await this.fetch(d.token_endpoint, {
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
headers: {
|
|
1026
|
+
Authorization: "Basic " + basic,
|
|
1027
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1028
|
+
Accept: "application/json"
|
|
1029
|
+
},
|
|
1030
|
+
body: new URLSearchParams({
|
|
1031
|
+
grant_type: "authorization_code",
|
|
1032
|
+
code,
|
|
1033
|
+
redirect_uri: this.cfg.redirectUri,
|
|
1034
|
+
code_verifier: codeVerifier
|
|
1035
|
+
}).toString()
|
|
1036
|
+
});
|
|
1037
|
+
let j = {};
|
|
1038
|
+
try {
|
|
1039
|
+
j = await res.json();
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
if (!res.ok) {
|
|
1043
|
+
const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
|
|
1044
|
+
throw new EidOidcError(
|
|
1045
|
+
e,
|
|
1046
|
+
`OIDC token ${res.status}: ${e}`,
|
|
1047
|
+
typeof j.error_description === "string" ? j.error_description : void 0
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof j.id_token !== "string" || j.id_token === "") {
|
|
1051
|
+
throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
|
|
1052
|
+
}
|
|
1053
|
+
return j.id_token;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
|
|
1057
|
+
* (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
|
|
1058
|
+
*/
|
|
1059
|
+
async verifyIdToken(idToken, opts) {
|
|
1060
|
+
const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
|
|
1061
|
+
const parts = idToken.split(".");
|
|
1062
|
+
if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1063
|
+
const header = decodeJsonPart(parts[0]);
|
|
1064
|
+
const payload = decodeJsonPart(parts[1]);
|
|
1065
|
+
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1066
|
+
if (header.alg !== "ES256") throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1067
|
+
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1068
|
+
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1069
|
+
const pick = (ks) => ks.find(
|
|
1070
|
+
(k) => k.kid === kid && k.kty === "EC" && k.crv === "P-256" && (k.use === void 0 || k.use === "sig") && (k.alg === void 0 || k.alg === "ES256")
|
|
1071
|
+
);
|
|
1072
|
+
const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
|
|
1073
|
+
if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
|
|
1074
|
+
const sig = Buffer.from(parts[2], "base64url");
|
|
1075
|
+
if (sig.length !== 64) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1076
|
+
let ok = false;
|
|
1077
|
+
try {
|
|
1078
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
1079
|
+
"jwk",
|
|
1080
|
+
{ kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
|
|
1081
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1082
|
+
false,
|
|
1083
|
+
["verify"]
|
|
1084
|
+
);
|
|
1085
|
+
ok = await globalThis.crypto.subtle.verify(
|
|
1086
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1087
|
+
key,
|
|
1088
|
+
sig,
|
|
1089
|
+
new TextEncoder().encode(parts[0] + "." + parts[1])
|
|
1090
|
+
);
|
|
1091
|
+
} catch {
|
|
1092
|
+
ok = false;
|
|
1093
|
+
}
|
|
1094
|
+
if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1095
|
+
const c = payload;
|
|
1096
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1097
|
+
if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1098
|
+
const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
|
|
1099
|
+
if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1100
|
+
if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1101
|
+
if (typeof c.exp !== "number" || now > c.exp + CLOCK_SKEW_S) throw bad("\u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D");
|
|
1102
|
+
if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
|
|
1103
|
+
if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1104
|
+
if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
|
|
1105
|
+
return c;
|
|
1106
|
+
}
|
|
1107
|
+
keys(force) {
|
|
1108
|
+
const c = this.jwks;
|
|
1109
|
+
if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
|
|
1110
|
+
const keys = this.discover().then(async (d) => {
|
|
1111
|
+
const j = await this.getJson(d.jwks_uri);
|
|
1112
|
+
return Array.isArray(j?.keys) ? j.keys : [];
|
|
1113
|
+
});
|
|
1114
|
+
this.jwks = { at: Date.now(), keys };
|
|
1115
|
+
keys.catch(() => {
|
|
1116
|
+
if (this.jwks?.keys === keys) this.jwks = void 0;
|
|
1117
|
+
});
|
|
1118
|
+
return keys;
|
|
1119
|
+
}
|
|
1120
|
+
async getJson(url) {
|
|
1121
|
+
const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
1122
|
+
if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
|
|
1123
|
+
try {
|
|
1124
|
+
return await res.json();
|
|
1125
|
+
} catch {
|
|
1126
|
+
throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async fetch(url, init) {
|
|
1130
|
+
const f = this.cfg.fetchImpl ?? fetch;
|
|
1131
|
+
try {
|
|
1132
|
+
return await f(url, {
|
|
1133
|
+
...init,
|
|
1134
|
+
redirect: "error",
|
|
1135
|
+
signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1136
|
+
});
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
const err = e;
|
|
1139
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
1140
|
+
throw new NetworkError(`OIDC \u0445\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${url}`);
|
|
1141
|
+
}
|
|
1142
|
+
throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
async function pkceChallenge(codeVerifier) {
|
|
1147
|
+
const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
|
|
1148
|
+
return Buffer.from(h).toString("base64url");
|
|
1149
|
+
}
|
|
1150
|
+
function randomB64url(bytes) {
|
|
1151
|
+
return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
|
|
1152
|
+
}
|
|
1153
|
+
function formEncode(s) {
|
|
1154
|
+
return new URLSearchParams({ v: s }).toString().slice(2);
|
|
1155
|
+
}
|
|
1156
|
+
function timingSafeEqualStr(a, b) {
|
|
1157
|
+
const x = Buffer.from(a, "utf8");
|
|
1158
|
+
const y = Buffer.from(b, "utf8");
|
|
1159
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
1160
|
+
}
|
|
1161
|
+
function decodeJsonPart(s) {
|
|
1162
|
+
try {
|
|
1163
|
+
const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
|
|
1164
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function callbackParams(input) {
|
|
1170
|
+
if (input instanceof URLSearchParams) return input;
|
|
1171
|
+
if (input instanceof URL) return input.searchParams;
|
|
1172
|
+
const s = input.trim();
|
|
1173
|
+
if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
|
|
1174
|
+
const i = s.indexOf("?");
|
|
1175
|
+
return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
|
|
1176
|
+
}
|
|
769
1177
|
export {
|
|
770
1178
|
ApiError,
|
|
771
1179
|
AuthApi,
|
|
772
1180
|
AuthenticationError,
|
|
773
1181
|
DEFAULT_BASE_URL,
|
|
774
1182
|
DeviceLinkError,
|
|
1183
|
+
EID_ACR_ADVANCED,
|
|
1184
|
+
EID_ACR_QUALIFIED,
|
|
1185
|
+
EID_OIDC_ISSUER,
|
|
775
1186
|
EidClient,
|
|
776
1187
|
EidError,
|
|
1188
|
+
EidOidcClient,
|
|
1189
|
+
EidOidcError,
|
|
777
1190
|
ForbiddenError,
|
|
778
1191
|
Http,
|
|
779
1192
|
NetworkError,
|
|
1193
|
+
ON_BEHALF_ERROR_CODES,
|
|
1194
|
+
OrganizationApi,
|
|
1195
|
+
PdfApi,
|
|
780
1196
|
ResponseValidator,
|
|
781
1197
|
SessionApi,
|
|
782
1198
|
SessionFailedError,
|
|
@@ -790,6 +1206,7 @@ export {
|
|
|
790
1206
|
isMobileUserAgent,
|
|
791
1207
|
openOrShowQR,
|
|
792
1208
|
parseSessionResult,
|
|
1209
|
+
pkceChallenge,
|
|
793
1210
|
qrDeviceLinkTicker,
|
|
794
1211
|
randomChallenge,
|
|
795
1212
|
resolveBaseUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gerege-systems/eid-mongolia-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
32
|
+
"test": "npm run build && node --test --experimental-strip-types test/sdk.test.ts test/oidc.test.ts",
|
|
33
33
|
"lint": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|