@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.cjs
CHANGED
|
@@ -25,11 +25,19 @@ __export(index_exports, {
|
|
|
25
25
|
AuthenticationError: () => AuthenticationError,
|
|
26
26
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
27
27
|
DeviceLinkError: () => DeviceLinkError,
|
|
28
|
+
EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
|
|
29
|
+
EID_ACR_QUALIFIED: () => EID_ACR_QUALIFIED,
|
|
30
|
+
EID_OIDC_ISSUER: () => EID_OIDC_ISSUER,
|
|
28
31
|
EidClient: () => EidClient,
|
|
29
32
|
EidError: () => EidError,
|
|
33
|
+
EidOidcClient: () => EidOidcClient,
|
|
34
|
+
EidOidcError: () => EidOidcError,
|
|
30
35
|
ForbiddenError: () => ForbiddenError,
|
|
31
36
|
Http: () => Http,
|
|
32
37
|
NetworkError: () => NetworkError,
|
|
38
|
+
ON_BEHALF_ERROR_CODES: () => ON_BEHALF_ERROR_CODES,
|
|
39
|
+
OrganizationApi: () => OrganizationApi,
|
|
40
|
+
PdfApi: () => PdfApi,
|
|
33
41
|
ResponseValidator: () => ResponseValidator,
|
|
34
42
|
SessionApi: () => SessionApi,
|
|
35
43
|
SessionFailedError: () => SessionFailedError,
|
|
@@ -43,6 +51,7 @@ __export(index_exports, {
|
|
|
43
51
|
isMobileUserAgent: () => isMobileUserAgent,
|
|
44
52
|
openOrShowQR: () => openOrShowQR,
|
|
45
53
|
parseSessionResult: () => parseSessionResult,
|
|
54
|
+
pkceChallenge: () => pkceChallenge,
|
|
46
55
|
qrDeviceLinkTicker: () => qrDeviceLinkTicker,
|
|
47
56
|
randomChallenge: () => randomChallenge,
|
|
48
57
|
resolveBaseUrl: () => resolveBaseUrl,
|
|
@@ -63,16 +72,32 @@ var EidError = class extends Error {
|
|
|
63
72
|
var AuthenticationError = class extends EidError {
|
|
64
73
|
};
|
|
65
74
|
var ForbiddenError = class extends EidError {
|
|
75
|
+
constructor(message, code) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.code = code;
|
|
78
|
+
}
|
|
79
|
+
code;
|
|
66
80
|
};
|
|
67
81
|
var ApiError = class extends EidError {
|
|
68
|
-
constructor(status, body) {
|
|
82
|
+
constructor(status, body, code) {
|
|
69
83
|
super(`RP-API ${status}: ${body}`);
|
|
70
84
|
this.status = status;
|
|
71
85
|
this.body = body;
|
|
86
|
+
this.code = code;
|
|
72
87
|
}
|
|
73
88
|
status;
|
|
74
89
|
body;
|
|
90
|
+
code;
|
|
75
91
|
};
|
|
92
|
+
var ON_BEHALF_ERROR_CODES = [
|
|
93
|
+
"REPRESENTATION_DENIED",
|
|
94
|
+
"REPRESENTATION_PENDING",
|
|
95
|
+
"REPRESENTATION_EXPIRED",
|
|
96
|
+
"ORG_NOT_ACTIVE",
|
|
97
|
+
"SIGNER_UNIDENTIFIED",
|
|
98
|
+
"ORG_NOT_FOUND",
|
|
99
|
+
"REPRESENTATION_REVOKED"
|
|
100
|
+
];
|
|
76
101
|
var NetworkError = class extends EidError {
|
|
77
102
|
};
|
|
78
103
|
var SessionFailedError = class extends EidError {
|
|
@@ -95,21 +120,35 @@ var Http = class {
|
|
|
95
120
|
}
|
|
96
121
|
cfg;
|
|
97
122
|
post(path, body, timeoutMs) {
|
|
98
|
-
return this.
|
|
123
|
+
return this.json("POST", path, JSON.stringify(body), "application/json", timeoutMs);
|
|
99
124
|
}
|
|
100
125
|
get(path, timeoutMs) {
|
|
101
|
-
return this.
|
|
126
|
+
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
127
|
+
}
|
|
128
|
+
/** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
|
|
129
|
+
postMultipart(path, form, timeoutMs) {
|
|
130
|
+
return this.json("POST", path, form, void 0, timeoutMs);
|
|
131
|
+
}
|
|
132
|
+
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
|
|
133
|
+
async getBytes(path, timeoutMs) {
|
|
134
|
+
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
135
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
102
136
|
}
|
|
103
|
-
async
|
|
137
|
+
async json(method, path, body, contentType, timeoutMs) {
|
|
138
|
+
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
139
|
+
const text = await res.text();
|
|
140
|
+
return text ? JSON.parse(text) : {};
|
|
141
|
+
}
|
|
142
|
+
async send(method, path, body, contentType, timeoutMs) {
|
|
104
143
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
105
144
|
const url = this.cfg.baseUrl + path;
|
|
106
145
|
const init = {
|
|
107
146
|
method,
|
|
108
147
|
headers: {
|
|
109
148
|
Authorization: `Bearer ${this.cfg.apiSecret}`,
|
|
110
|
-
...
|
|
149
|
+
...contentType ? { "Content-Type": contentType } : {}
|
|
111
150
|
},
|
|
112
|
-
body
|
|
151
|
+
body,
|
|
113
152
|
signal: AbortSignal.timeout(timeoutMs ?? this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
114
153
|
};
|
|
115
154
|
if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
|
|
@@ -123,13 +162,32 @@ var Http = class {
|
|
|
123
162
|
}
|
|
124
163
|
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${method} ${path}): ${err.message}`);
|
|
125
164
|
}
|
|
165
|
+
if (res.status < 300) return res;
|
|
126
166
|
const text = await res.text();
|
|
167
|
+
const { error, code } = errorBody(text);
|
|
127
168
|
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)");
|
|
128
|
-
if (res.status === 403)
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
if (res.status === 403) {
|
|
170
|
+
throw new ForbiddenError(
|
|
171
|
+
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)",
|
|
172
|
+
code
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
throw new ApiError(res.status, text, code);
|
|
131
176
|
}
|
|
132
177
|
};
|
|
178
|
+
function errorBody(text) {
|
|
179
|
+
try {
|
|
180
|
+
const v = JSON.parse(text);
|
|
181
|
+
if (!v || typeof v !== "object") return {};
|
|
182
|
+
const o = v;
|
|
183
|
+
return {
|
|
184
|
+
error: typeof o.error === "string" && o.error ? o.error : void 0,
|
|
185
|
+
code: typeof o.code === "string" && o.code ? o.code : void 0
|
|
186
|
+
};
|
|
187
|
+
} catch {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
133
191
|
|
|
134
192
|
// src/crypto.ts
|
|
135
193
|
var import_node_crypto = require("crypto");
|
|
@@ -341,9 +399,32 @@ function parseSessionResult(raw) {
|
|
|
341
399
|
initialCallbackUrl: typeof sig.initialCallbackUrl === "string" ? sig.initialCallbackUrl : null,
|
|
342
400
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
343
401
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
344
|
-
onBehalfOf: parseOrg(r.onBehalfOf)
|
|
402
|
+
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
403
|
+
pdf: parsePdf(r.pdf)
|
|
345
404
|
};
|
|
346
405
|
}
|
|
406
|
+
function parsePdf(raw) {
|
|
407
|
+
const p = asRecord(raw);
|
|
408
|
+
const docID = str(p.docID);
|
|
409
|
+
if (!docID) return null;
|
|
410
|
+
const out = { docID, documentStatus: String(p.documentStatus ?? "") };
|
|
411
|
+
if (p.errorCode != null) out.errorCode = String(p.errorCode);
|
|
412
|
+
if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
|
|
413
|
+
if (p.size != null) out.size = Number(p.size);
|
|
414
|
+
if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
|
|
415
|
+
if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
|
|
416
|
+
const sg = asRecord(p.signer);
|
|
417
|
+
if (sg.etsi != null) {
|
|
418
|
+
out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
|
|
419
|
+
if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
|
|
420
|
+
if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
|
|
421
|
+
}
|
|
422
|
+
const v = asRecord(p.validation);
|
|
423
|
+
if (v.indication != null) {
|
|
424
|
+
out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
347
428
|
function parseSigParams(raw) {
|
|
348
429
|
const p = asRecord(raw);
|
|
349
430
|
const hash = str(p.hashAlgorithm);
|
|
@@ -398,6 +479,76 @@ var SessionApi = class {
|
|
|
398
479
|
}
|
|
399
480
|
};
|
|
400
481
|
|
|
482
|
+
// src/pdf.ts
|
|
483
|
+
function rec3(v) {
|
|
484
|
+
return v && typeof v === "object" ? v : {};
|
|
485
|
+
}
|
|
486
|
+
var PdfApi = class {
|
|
487
|
+
constructor(http, creds, defaults) {
|
|
488
|
+
this.http = http;
|
|
489
|
+
this.creds = creds;
|
|
490
|
+
this.defaults = defaults;
|
|
491
|
+
}
|
|
492
|
+
http;
|
|
493
|
+
creds;
|
|
494
|
+
defaults;
|
|
495
|
+
/** POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. */
|
|
496
|
+
async prepare(input) {
|
|
497
|
+
const request = {
|
|
498
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
499
|
+
relyingPartyName: this.creds.rpName,
|
|
500
|
+
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
501
|
+
signer: input.signer,
|
|
502
|
+
flow: input.flow ?? "notification",
|
|
503
|
+
fileName: input.fileName
|
|
504
|
+
};
|
|
505
|
+
if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
506
|
+
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
507
|
+
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
508
|
+
const form = new FormData();
|
|
509
|
+
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
510
|
+
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
511
|
+
const r = rec3(await this.http.postMultipart("/pdf/sign/prepare", form, 6e4));
|
|
512
|
+
const receivedAt = Date.now();
|
|
513
|
+
const vc = rec3(r.vc).value;
|
|
514
|
+
const dl = rec3(r.deviceLink);
|
|
515
|
+
return {
|
|
516
|
+
sessionId: String(r.sessionID),
|
|
517
|
+
docId: String(r.docID),
|
|
518
|
+
vc: vc != null ? String(vc) : null,
|
|
519
|
+
deviceLink: dl.sessionToken ? {
|
|
520
|
+
sessionToken: String(dl.sessionToken),
|
|
521
|
+
sessionSecret: String(dl.sessionSecret),
|
|
522
|
+
deviceLinkBase: String(dl.deviceLinkBase),
|
|
523
|
+
digest: String(dl.digest),
|
|
524
|
+
interactions: String(dl.interactions)
|
|
525
|
+
} : null,
|
|
526
|
+
receivedAt
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
531
|
+
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
|
|
532
|
+
*/
|
|
533
|
+
document(sessionId) {
|
|
534
|
+
return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/organization.ts
|
|
539
|
+
var OrganizationApi = class {
|
|
540
|
+
constructor(http) {
|
|
541
|
+
this.http = http;
|
|
542
|
+
}
|
|
543
|
+
http;
|
|
544
|
+
/** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
|
|
545
|
+
async getRepresentations(personEtsi) {
|
|
546
|
+
const raw = await this.http.get(`/organization/representations/etsi/${encodeURIComponent(personEtsi)}`);
|
|
547
|
+
const list = Array.isArray(raw.representations) ? raw.representations : [];
|
|
548
|
+
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
401
552
|
// src/validator.ts
|
|
402
553
|
var import_node_crypto2 = require("crypto");
|
|
403
554
|
var ResponseValidator = class {
|
|
@@ -752,6 +903,8 @@ function resolveBaseUrl(baseUrl) {
|
|
|
752
903
|
var EidClient = class {
|
|
753
904
|
auth;
|
|
754
905
|
sign;
|
|
906
|
+
pdf;
|
|
907
|
+
organization;
|
|
755
908
|
session;
|
|
756
909
|
validator;
|
|
757
910
|
constructor(cfg) {
|
|
@@ -767,6 +920,8 @@ var EidClient = class {
|
|
|
767
920
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
768
921
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
769
922
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
923
|
+
this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level });
|
|
924
|
+
this.organization = new OrganizationApi(http);
|
|
770
925
|
this.session = new SessionApi(http);
|
|
771
926
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
772
927
|
}
|
|
@@ -883,6 +1038,283 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
883
1038
|
t.unref?.();
|
|
884
1039
|
return () => clearInterval(t);
|
|
885
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
// src/oidc.ts
|
|
1043
|
+
var import_node_crypto4 = require("crypto");
|
|
1044
|
+
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
1045
|
+
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
1046
|
+
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
1047
|
+
var CLOCK_SKEW_S = 60;
|
|
1048
|
+
var CACHE_MS = 60 * 60 * 1e3;
|
|
1049
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
1050
|
+
var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
1051
|
+
"access_denied",
|
|
1052
|
+
"login_required",
|
|
1053
|
+
"invalid_request",
|
|
1054
|
+
"invalid_scope",
|
|
1055
|
+
"unauthorized_client",
|
|
1056
|
+
"unsupported_response_type",
|
|
1057
|
+
"request_not_supported",
|
|
1058
|
+
"temporarily_unavailable",
|
|
1059
|
+
"server_error",
|
|
1060
|
+
"invalid_client",
|
|
1061
|
+
"invalid_grant",
|
|
1062
|
+
"unsupported_grant_type"
|
|
1063
|
+
]);
|
|
1064
|
+
var EidOidcError = class extends EidError {
|
|
1065
|
+
constructor(code, message, description) {
|
|
1066
|
+
super(message);
|
|
1067
|
+
this.code = code;
|
|
1068
|
+
this.description = description;
|
|
1069
|
+
}
|
|
1070
|
+
code;
|
|
1071
|
+
description;
|
|
1072
|
+
};
|
|
1073
|
+
var EidOidcClient = class {
|
|
1074
|
+
constructor(cfg) {
|
|
1075
|
+
this.cfg = cfg;
|
|
1076
|
+
if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1077
|
+
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1078
|
+
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1079
|
+
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1080
|
+
this.scope = cfg.scope ?? "openid";
|
|
1081
|
+
}
|
|
1082
|
+
cfg;
|
|
1083
|
+
issuer;
|
|
1084
|
+
scope;
|
|
1085
|
+
discovery;
|
|
1086
|
+
jwks;
|
|
1087
|
+
/** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
|
|
1088
|
+
discover() {
|
|
1089
|
+
const c = this.discovery;
|
|
1090
|
+
if (c && Date.now() - c.at < CACHE_MS) return c.d;
|
|
1091
|
+
const d = this.fetchDiscovery();
|
|
1092
|
+
this.discovery = { at: Date.now(), d };
|
|
1093
|
+
d.catch(() => {
|
|
1094
|
+
if (this.discovery?.d === d) this.discovery = void 0;
|
|
1095
|
+
});
|
|
1096
|
+
return d;
|
|
1097
|
+
}
|
|
1098
|
+
async fetchDiscovery() {
|
|
1099
|
+
const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
|
|
1100
|
+
if (!d || d.issuer !== this.issuer) {
|
|
1101
|
+
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})`);
|
|
1102
|
+
}
|
|
1103
|
+
for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
|
|
1104
|
+
if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
|
|
1105
|
+
throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return d;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Authorize URL + санамсаргүй state / nonce / PKCE codeVerifier. RP буцаасан `state`, `nonce`, `codeVerifier`-ийг
|
|
1112
|
+
* хөтчийн session-тэй холбож (httpOnly cookie эсвэл server session) хадгалаад хөтчийг `url` руу 302 хийнэ.
|
|
1113
|
+
*/
|
|
1114
|
+
async createAuthorization(opts = {}) {
|
|
1115
|
+
const d = await this.discover();
|
|
1116
|
+
const state = randomB64url(32);
|
|
1117
|
+
const nonce = randomB64url(32);
|
|
1118
|
+
const codeVerifier = randomB64url(32);
|
|
1119
|
+
const u = new URL(d.authorization_endpoint);
|
|
1120
|
+
const p = new URLSearchParams({
|
|
1121
|
+
response_type: "code",
|
|
1122
|
+
client_id: this.cfg.clientId,
|
|
1123
|
+
redirect_uri: this.cfg.redirectUri,
|
|
1124
|
+
scope: opts.scope ?? this.scope,
|
|
1125
|
+
state,
|
|
1126
|
+
nonce,
|
|
1127
|
+
code_challenge: await pkceChallenge(codeVerifier),
|
|
1128
|
+
code_challenge_method: "S256"
|
|
1129
|
+
});
|
|
1130
|
+
if (opts.display) p.set("display", opts.display);
|
|
1131
|
+
u.search = p.toString();
|
|
1132
|
+
return { url: u.toString(), state, nonce, codeVerifier };
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* redirect_uri руу ирсэн callback-ийг шийднэ: state тулгах → `error` бол {@link EidOidcError} → code солих →
|
|
1136
|
+
* id_token шалгах. `input` — бүтэн callback URL, харьцангуй зам (`/cb?code=…`) эсвэл query (`URLSearchParams` / `code=…&state=…`); зөвхөн query-г уншина.
|
|
1137
|
+
* Амжилтгүй бүх тохиолдолд {@link EidOidcError} (эсвэл {@link NetworkError}) шиднэ.
|
|
1138
|
+
*/
|
|
1139
|
+
async handleCallback(input, checks) {
|
|
1140
|
+
const q = callbackParams(input);
|
|
1141
|
+
const state = q.get("state") ?? "";
|
|
1142
|
+
if (!checks.state || !timingSafeEqualStr(state, checks.state)) {
|
|
1143
|
+
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");
|
|
1144
|
+
}
|
|
1145
|
+
const iss = q.get("iss");
|
|
1146
|
+
if (iss !== null && iss !== this.issuer) {
|
|
1147
|
+
throw new EidOidcError("invalid_callback", "OIDC callback-\u0438\u0439\u043D iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1148
|
+
}
|
|
1149
|
+
const err = q.get("error");
|
|
1150
|
+
if (err) {
|
|
1151
|
+
const code2 = KNOWN_CODES.has(err) ? err : "server_error";
|
|
1152
|
+
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);
|
|
1153
|
+
}
|
|
1154
|
+
const code = q.get("code") ?? "";
|
|
1155
|
+
if (code === "" || code.length > 512) throw new EidOidcError("invalid_callback", "OIDC callback-\u0434 code \u0430\u043B\u0433\u0430");
|
|
1156
|
+
const idToken = await this.exchangeCode(code, checks.codeVerifier);
|
|
1157
|
+
const claims = await this.verifyIdToken(idToken, { nonce: checks.nonce });
|
|
1158
|
+
return { claims, idToken };
|
|
1159
|
+
}
|
|
1160
|
+
/** `POST token_endpoint` (client_secret_basic, RFC 6749 §2.3.1) → id_token. */
|
|
1161
|
+
async exchangeCode(code, codeVerifier) {
|
|
1162
|
+
const d = await this.discover();
|
|
1163
|
+
const basic = Buffer.from(formEncode(this.cfg.clientId) + ":" + formEncode(this.cfg.clientSecret)).toString("base64");
|
|
1164
|
+
const res = await this.fetch(d.token_endpoint, {
|
|
1165
|
+
method: "POST",
|
|
1166
|
+
headers: {
|
|
1167
|
+
Authorization: "Basic " + basic,
|
|
1168
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1169
|
+
Accept: "application/json"
|
|
1170
|
+
},
|
|
1171
|
+
body: new URLSearchParams({
|
|
1172
|
+
grant_type: "authorization_code",
|
|
1173
|
+
code,
|
|
1174
|
+
redirect_uri: this.cfg.redirectUri,
|
|
1175
|
+
code_verifier: codeVerifier
|
|
1176
|
+
}).toString()
|
|
1177
|
+
});
|
|
1178
|
+
let j = {};
|
|
1179
|
+
try {
|
|
1180
|
+
j = await res.json();
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
if (!res.ok) {
|
|
1184
|
+
const e = typeof j.error === "string" && KNOWN_CODES.has(j.error) ? j.error : "server_error";
|
|
1185
|
+
throw new EidOidcError(
|
|
1186
|
+
e,
|
|
1187
|
+
`OIDC token ${res.status}: ${e}`,
|
|
1188
|
+
typeof j.error_description === "string" ? j.error_description : void 0
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof j.id_token !== "string" || j.id_token === "") {
|
|
1192
|
+
throw new EidOidcError("server_error", "OIDC token \u0445\u0430\u0440\u0438\u0443\u043D\u0434 id_token \u0430\u043B\u0433\u0430");
|
|
1193
|
+
}
|
|
1194
|
+
return j.id_token;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
|
|
1198
|
+
* (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
|
|
1199
|
+
*/
|
|
1200
|
+
async verifyIdToken(idToken, opts) {
|
|
1201
|
+
const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
|
|
1202
|
+
const parts = idToken.split(".");
|
|
1203
|
+
if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1204
|
+
const header = decodeJsonPart(parts[0]);
|
|
1205
|
+
const payload = decodeJsonPart(parts[1]);
|
|
1206
|
+
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1207
|
+
if (header.alg !== "ES256") throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1208
|
+
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1209
|
+
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1210
|
+
const pick = (ks) => ks.find(
|
|
1211
|
+
(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")
|
|
1212
|
+
);
|
|
1213
|
+
const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
|
|
1214
|
+
if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
|
|
1215
|
+
const sig = Buffer.from(parts[2], "base64url");
|
|
1216
|
+
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");
|
|
1217
|
+
let ok = false;
|
|
1218
|
+
try {
|
|
1219
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
1220
|
+
"jwk",
|
|
1221
|
+
{ kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y },
|
|
1222
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1223
|
+
false,
|
|
1224
|
+
["verify"]
|
|
1225
|
+
);
|
|
1226
|
+
ok = await globalThis.crypto.subtle.verify(
|
|
1227
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1228
|
+
key,
|
|
1229
|
+
sig,
|
|
1230
|
+
new TextEncoder().encode(parts[0] + "." + parts[1])
|
|
1231
|
+
);
|
|
1232
|
+
} catch {
|
|
1233
|
+
ok = false;
|
|
1234
|
+
}
|
|
1235
|
+
if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1236
|
+
const c = payload;
|
|
1237
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1238
|
+
if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1239
|
+
const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
|
|
1240
|
+
if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1241
|
+
if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1242
|
+
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");
|
|
1243
|
+
if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
|
|
1244
|
+
if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1245
|
+
if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
|
|
1246
|
+
return c;
|
|
1247
|
+
}
|
|
1248
|
+
keys(force) {
|
|
1249
|
+
const c = this.jwks;
|
|
1250
|
+
if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
|
|
1251
|
+
const keys = this.discover().then(async (d) => {
|
|
1252
|
+
const j = await this.getJson(d.jwks_uri);
|
|
1253
|
+
return Array.isArray(j?.keys) ? j.keys : [];
|
|
1254
|
+
});
|
|
1255
|
+
this.jwks = { at: Date.now(), keys };
|
|
1256
|
+
keys.catch(() => {
|
|
1257
|
+
if (this.jwks?.keys === keys) this.jwks = void 0;
|
|
1258
|
+
});
|
|
1259
|
+
return keys;
|
|
1260
|
+
}
|
|
1261
|
+
async getJson(url) {
|
|
1262
|
+
const res = await this.fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
1263
|
+
if (!res.ok) throw new EidOidcError("discovery_failed", `OIDC ${res.status}: ${url}`);
|
|
1264
|
+
try {
|
|
1265
|
+
return await res.json();
|
|
1266
|
+
} catch {
|
|
1267
|
+
throw new EidOidcError("discovery_failed", `OIDC JSON \u0431\u0438\u0448: ${url}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async fetch(url, init) {
|
|
1271
|
+
const f = this.cfg.fetchImpl ?? fetch;
|
|
1272
|
+
try {
|
|
1273
|
+
return await f(url, {
|
|
1274
|
+
...init,
|
|
1275
|
+
redirect: "error",
|
|
1276
|
+
signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1277
|
+
});
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
const err = e;
|
|
1280
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
1281
|
+
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}`);
|
|
1282
|
+
}
|
|
1283
|
+
throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
async function pkceChallenge(codeVerifier) {
|
|
1288
|
+
const h = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
|
|
1289
|
+
return Buffer.from(h).toString("base64url");
|
|
1290
|
+
}
|
|
1291
|
+
function randomB64url(bytes) {
|
|
1292
|
+
return Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
|
|
1293
|
+
}
|
|
1294
|
+
function formEncode(s) {
|
|
1295
|
+
return new URLSearchParams({ v: s }).toString().slice(2);
|
|
1296
|
+
}
|
|
1297
|
+
function timingSafeEqualStr(a, b) {
|
|
1298
|
+
const x = Buffer.from(a, "utf8");
|
|
1299
|
+
const y = Buffer.from(b, "utf8");
|
|
1300
|
+
return x.length === y.length && (0, import_node_crypto4.timingSafeEqual)(x, y);
|
|
1301
|
+
}
|
|
1302
|
+
function decodeJsonPart(s) {
|
|
1303
|
+
try {
|
|
1304
|
+
const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
|
|
1305
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1306
|
+
} catch {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function callbackParams(input) {
|
|
1311
|
+
if (input instanceof URLSearchParams) return input;
|
|
1312
|
+
if (input instanceof URL) return input.searchParams;
|
|
1313
|
+
const s = input.trim();
|
|
1314
|
+
if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
|
|
1315
|
+
const i = s.indexOf("?");
|
|
1316
|
+
return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
|
|
1317
|
+
}
|
|
886
1318
|
// Annotate the CommonJS export names for ESM import in node:
|
|
887
1319
|
0 && (module.exports = {
|
|
888
1320
|
ApiError,
|
|
@@ -890,11 +1322,19 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
890
1322
|
AuthenticationError,
|
|
891
1323
|
DEFAULT_BASE_URL,
|
|
892
1324
|
DeviceLinkError,
|
|
1325
|
+
EID_ACR_ADVANCED,
|
|
1326
|
+
EID_ACR_QUALIFIED,
|
|
1327
|
+
EID_OIDC_ISSUER,
|
|
893
1328
|
EidClient,
|
|
894
1329
|
EidError,
|
|
1330
|
+
EidOidcClient,
|
|
1331
|
+
EidOidcError,
|
|
895
1332
|
ForbiddenError,
|
|
896
1333
|
Http,
|
|
897
1334
|
NetworkError,
|
|
1335
|
+
ON_BEHALF_ERROR_CODES,
|
|
1336
|
+
OrganizationApi,
|
|
1337
|
+
PdfApi,
|
|
898
1338
|
ResponseValidator,
|
|
899
1339
|
SessionApi,
|
|
900
1340
|
SessionFailedError,
|
|
@@ -908,6 +1348,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
908
1348
|
isMobileUserAgent,
|
|
909
1349
|
openOrShowQR,
|
|
910
1350
|
parseSessionResult,
|
|
1351
|
+
pkceChallenge,
|
|
911
1352
|
qrDeviceLinkTicker,
|
|
912
1353
|
randomChallenge,
|
|
913
1354
|
resolveBaseUrl,
|