// @ts-check
((exports, window) => { // eslint-disable-line max-classes-per-file
/**
* Класс ошибок QRSigningError.
*/
class QRSigningError extends Error {
constructor(message, details) {
super(message);
this.name = 'QRSigningError';
this.details = details;
}
}
/**
* Класс клиента подписания через QR для произвольных данных (формирует CMS подписи).
*/
class QRSigningClientCMS {
/**
* Конструктор.
*
* @param {String} description описание подписываемых данных.
*
* @param {Boolean} [attach = false] следует ли включить в подпись подписываемые данные.
*
* @param {String} [baseUrl = 'https://sigex.kz'] базовый URL сервиса SIGEX.
*/
constructor(description, attach = false, baseUrl = 'https://sigex.kz') {
this.description = description;
this.attach = attach;
this.baseUrl = baseUrl;
this.retries = 25;
this.documentsToSign = [];
this.expireAt = null;
this.dataURL = null;
this.signURL = null;
this.qrCode = null;
this.eGovMobileLaunchLink = null;
this.eGovBusinessLaunchLink = null;
}
/**
* Добавить блок данных для подписания, зачастую речь идет о файле.
*
* @param {String[]} names массив имен подписываемого блока данных на разных языках
* [ru, kk, en]. Массив должен сожердать как минимум одну строку, в этом случае она будет
* использоваться для всех языков.
*
* @param {String | ArrayBuffer | Blob | File} data данные, которые нужно подписать,
* в виде строки Base64 либо ArrayBuffer, Blob или File.
*
* @param {Object[]} [meta = []] опциональный массив объектов метаданных, содержащих поля
* `"name"` и `"value"` со строковыми значениями.
*
* @param {Boolean} [isPDF = false] опциональная подсказка для приложения eGov mobile помогающая
* ему лучше подобрать приложение для отображения данных перед подписанием.
*
* @throws QRSigningError
*/
async addDataToSign(names, data, meta = [], isPDF = false) {
if (names.length === 0) {
throw new QRSigningError('Данные на подписание предоставлены не корректно.', 'Необходимо указать хотябы одно имя для подписываемых данных.');
}
let dataB64 = data;
if (typeof data !== 'string') {
let dataArrayBuffer = data;
if (data instanceof Blob) {
dataArrayBuffer = await data.arrayBuffer();
}
dataB64 = QRSigningClientCMS.arrayBufferToB64(dataArrayBuffer);
}
const documentToSign = {
id: this.documentsToSign.length + 1,
nameRu: names[0],
nameKz: names[1] ? names[1] : names[0],
nameEn: names[2] ? names[2] : names[0],
meta,
document: {
file: {
mime: isPDF ? '@file/pdf' : '',
data: dataB64,
},
},
};
this.documentsToSign.push(documentToSign);
}
/**
* Зарегистрировать процедуру QR подписания.
*
* @returns {Promise<String>} изображение QR кода в Base64 кодировке.
*
* @throws QRSigningError
*/
async registerQRSinging() {
try {
const data = {
description: this.description,
};
const response = await fetch(
`${this.baseUrl}/api/egovQr`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
},
);
if (!response.ok) {
throw new Error(`Сервер вернул статус '${response.status}: ${response.statusText}'`);
}
const responseJson = await response.json();
if (responseJson.message) {
throw new Error(responseJson.message);
}
this.qrCode = responseJson.qrCode;
this.dataURL = responseJson.dataURL;
this.signURL = responseJson.signURL;
this.eGovMobileLaunchLink = responseJson.eGovMobileLaunchLink;
this.eGovBusinessLaunchLink = responseJson.eGovBusinessLaunchLink;
return this.qrCode;
} catch (err) {
throw new QRSigningError('Не удалось зарегистрировать новый идентификатор QR.', err);
}
}
/**
* Получить QR код (необходимо предварительно выполнить регистрацию).
*
* @returns {String} изображение QR кода в Base64 кодировке.
*/
getQR() {
return this.qrCode;
}
/**
* Получить ссылку для запуска процедуры подписания в eGov mobile (кросс подписание)
* - для тех случаев, когда нужно выполнять подписание на том же самом устройстве, без
* необходимости сканировать QR код (необходимо предварительно выполнить регистрацию).
*
* @returns {String} ссылка для запуска процедуры подписания в eGov mobile.
*/
getEGovMobileLaunchLink() {
return this.eGovMobileLaunchLink;
}
/**
* Получить ссылку для запуска процедуры подписания в eGov Business (кросс подписание)
* - для тех случаев, когда нужно выполнять подписание на том же самом устройстве, без
* необходимости сканировать QR код (необходимо предварительно выполнить регистрацию).
*
* @returns {String} ссылка для запуска процедуры подписания в eGov Business.
*/
getEGovBusinessLaunchLink() {
return this.eGovBusinessLaunchLink;
}
/**
* Получить подписи под данными. Это может занять много времени, так как в
* процессе выполнения данные будут отправлены в eGov mobile, далее нужно
* будет дождаться пока пользователь подпишет данные и подписи будут выкачены
* обратно.
*
* @param {Function} [dataSentCallback] опциональная функция, которая будет вызвана после
* того, как данные для подписания будут переданы на сервер. Может быть использована для
* того, чтобы перестать отображать QR код, так как он больше не действителен.
*
* @param {Function} [debugErrorsCallback] опциональная функция, которая будет вызвана для
* каждого игнорируемого исключения полученного при обработке. Функция предназначена для
* отладки.
*
* @returns {Promise<String[]>} массив подписей под зарегистрированными блоками данных.
*
* @throws QRSigningError
*/
async getSignatures(dataSentCallback, debugErrorsCallback) {
if (this.documentsToSign.length === 0) {
throw new QRSigningError('Данные на подписание предоставлены не корректно.', 'Не зарегистрировано ни одного блока данных для подписания.');
}
// Отправка данных
try {
const data = {
signMethod: this.attach ? 'CMS_WITH_DATA' : 'CMS_SIGN_ONLY',
documentsToSign: this.documentsToSign,
};
let response;
for (let i = 0; i < this.retries; i += 1) {
try {
response = await fetch( // eslint-disable-line no-await-in-loop
this.dataURL,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify(data),
},
);
// Будем пытаться отправлять запросы до тех пор, пока не получим ответа.
break;
} catch (err) {
// Игнорируем исключение и пробуем снова.
if (typeof debugErrorsCallback === 'function') {
debugErrorsCallback(err);
}
}
}
if (!response) {
throw new Error(`Не удалось получить ответа от сервера ${this.dataURL}.`);
}
if (!response.ok) {
throw new Error(`Сервер вернул статус '${response.status}: ${response.statusText}'`);
}
const responseJson = await response.json();
if (responseJson.message) {
throw new Error(responseJson.message);
}
} catch (err) {
throw new QRSigningError('Не удалось отправить данные.', err);
}
try {
if (typeof dataSentCallback === 'function') {
dataSentCallback();
}
} catch (err) {
// Игнорируем проблемы внешнего кода.
}
// Получение подписей
try {
let response;
for (let i = 0; i < this.retries; i += 1) {
try {
response = await fetch( // eslint-disable-line no-await-in-loop
this.signURL,
{
cache: 'no-store',
},
);
// Будем пытаться отправлять запросы до тех пор, пока не получим ответа.
break;
} catch (err) {
// Игнорируем исключение и пробуем снова.
if (typeof debugErrorsCallback === 'function') {
debugErrorsCallback(err);
}
}
}
if (!response) {
throw new Error(`Не удалось получить ответа от сервера ${this.signURL}.`);
}
if (!response.ok) {
throw new Error(`Сервер вернул статус '${response.status}: ${response.statusText}'`);
}
const responseJson = await response.json();
if (responseJson.message) {
throw new Error(responseJson.message);
}
const signatures = responseJson.documentsToSign.map(
(documentToSign) => documentToSign.document.file.data,
);
return signatures;
} catch (err) {
throw new QRSigningError('Не удалось получить подписи.', err);
}
}
static arrayBufferToB64(arrayBuffer) {
let binary = '';
const bytes = new Uint8Array(arrayBuffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
}
exports.QRSigningClientCMS = QRSigningClientCMS; // eslint-disable-line no-param-reassign
})(
typeof exports === 'undefined' ? this : exports,
typeof window === 'undefined' ? { btoa(x) { return x; } } : window,
); // Заглушка для NodeJS