
Some checks failed
CI - 4NK Node / Integration Tests (push) Failing after 9s
CI - 4NK Node / Docker Build & Test (push) Failing after 8s
CI - 4NK Node / Documentation Tests (push) Failing after 3s
CI - 4NK Node / Release Guard (push) Has been skipped
CI - 4NK Node / Performance Tests (push) Failing after 29s
CI - 4NK Node / Code Quality (push) Failing after 32s
CI - 4NK Node / Unit Tests (push) Failing after 30s
CI - 4NK Node / Security Tests (push) Failing after 28s
CI - 4NK Node / Notify (push) Failing after 1s
11828 lines
385 KiB
JavaScript
11828 lines
385 KiB
JavaScript
true&&(function polyfill() {
|
|
const relList = document.createElement("link").relList;
|
|
if (relList && relList.supports && relList.supports("modulepreload")) {
|
|
return;
|
|
}
|
|
for (const link of document.querySelectorAll('link[rel="modulepreload"]')) {
|
|
processPreload(link);
|
|
}
|
|
new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type !== "childList") {
|
|
continue;
|
|
}
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.tagName === "LINK" && node.rel === "modulepreload")
|
|
processPreload(node);
|
|
}
|
|
}
|
|
}).observe(document, { childList: true, subtree: true });
|
|
function getFetchOpts(link) {
|
|
const fetchOpts = {};
|
|
if (link.integrity) fetchOpts.integrity = link.integrity;
|
|
if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
|
|
if (link.crossOrigin === "use-credentials")
|
|
fetchOpts.credentials = "include";
|
|
else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
|
|
else fetchOpts.credentials = "same-origin";
|
|
return fetchOpts;
|
|
}
|
|
function processPreload(link) {
|
|
if (link.ep)
|
|
return;
|
|
link.ep = true;
|
|
const fetchOpts = getFetchOpts(link);
|
|
fetch(link.href, fetchOpts);
|
|
}
|
|
}());
|
|
|
|
const scriptRel = 'modulepreload';const assetsURL = function(dep) { return "/"+dep };const seen = {};const __vitePreload = function preload(baseModule, deps, importerUrl) {
|
|
let promise = Promise.resolve();
|
|
if (true && deps && deps.length > 0) {
|
|
document.getElementsByTagName("link");
|
|
const cspNonceMeta = document.querySelector(
|
|
"meta[property=csp-nonce]"
|
|
);
|
|
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
|
|
promise = Promise.allSettled(
|
|
deps.map((dep) => {
|
|
dep = assetsURL(dep);
|
|
if (dep in seen) return;
|
|
seen[dep] = true;
|
|
const isCss = dep.endsWith(".css");
|
|
const cssSelector = isCss ? '[rel="stylesheet"]' : "";
|
|
if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
|
|
return;
|
|
}
|
|
const link = document.createElement("link");
|
|
link.rel = isCss ? "stylesheet" : scriptRel;
|
|
if (!isCss) {
|
|
link.as = "script";
|
|
}
|
|
link.crossOrigin = "";
|
|
link.href = dep;
|
|
if (cspNonce) {
|
|
link.setAttribute("nonce", cspNonce);
|
|
}
|
|
document.head.appendChild(link);
|
|
if (isCss) {
|
|
return new Promise((res, rej) => {
|
|
link.addEventListener("load", res);
|
|
link.addEventListener(
|
|
"error",
|
|
() => rej(new Error(`Unable to preload CSS for ${dep}`))
|
|
);
|
|
});
|
|
}
|
|
})
|
|
);
|
|
}
|
|
function handlePreloadError(err) {
|
|
const e = new Event("vite:preloadError", {
|
|
cancelable: true
|
|
});
|
|
e.payload = err;
|
|
window.dispatchEvent(e);
|
|
if (!e.defaultPrevented) {
|
|
throw err;
|
|
}
|
|
}
|
|
return promise.then((res) => {
|
|
for (const item of res || []) {
|
|
if (item.status !== "rejected") continue;
|
|
handlePreloadError(item.reason);
|
|
}
|
|
return baseModule().catch(handlePreloadError);
|
|
});
|
|
};
|
|
|
|
const modalHtml = "<div id=\"login-modal\" class=\"modal\">\r\n <div class=\"modal-content\">\r\n <div class=\"modal-title\">Login</div>\r\n <div class=\"confirmation-box\">\r\n <div class=\"message\">\r\n Attempting to pair device with address\r\n <strong>{{device1}}</strong>\r\n with device with address\r\n <strong>{{device2}}</strong>\r\n </div>\r\n <div>Awaiting pairing validation...</div>\r\n </div>\r\n </div>\r\n</div>\r\n";
|
|
|
|
const modalScript = "import Routing from '/src/services/routing.service.ts';\r\n\r\nconst router = await Routing.getInstance();\r\nexport async function confirmLogin() {\r\n router.confirmLogin();\r\n}\r\n\r\nexport async function closeLoginModal() {\r\n router.closeLoginModal();\r\n}\r\n\r\nwindow.confirmLogin = confirmLogin;\r\nwindow.closeLoginModal = closeLoginModal;\r\n";
|
|
|
|
const validationModalStyle = ".validation-modal {\r\n display: block; /* Show the modal for demo purposes */\r\n position: fixed;\r\n z-index: 1;\r\n left: 0;\r\n top: 0;\r\n width: 100%;\r\n height: 100%;\r\n overflow: auto;\r\n background-color: rgb(0, 0, 0);\r\n background-color: rgba(0, 0, 0, 0.4);\r\n padding-top: 60px;\r\n}\r\n.modal-content {\r\n background-color: #fefefe;\r\n margin: 5% auto;\r\n padding: 20px;\r\n border: 1px solid #888;\r\n width: 80%;\r\n height: fit-content;\r\n}\r\n.modal-title {\r\n font-size: 24px;\r\n font-weight: bold;\r\n margin-bottom: 20px;\r\n}\r\n.validation-box {\r\n margin-bottom: 15px;\r\n width: 100%;\r\n}\r\n.expansion-panel-header {\r\n background-color: #e0e0e0;\r\n padding: 10px;\r\n cursor: pointer;\r\n}\r\n.expansion-panel-body {\r\n display: none;\r\n background-color: #fafafa;\r\n padding: 10px;\r\n border-top: 1px solid #ddd;\r\n}\r\n.expansion-panel-body pre {\r\n background-color: #f6f8fa;\r\n padding: 10px;\r\n border-left: 4px solid #d1d5da;\r\n overflow-x: auto;\r\n}\r\n.diff {\r\n display: flex;\r\n justify-content: space-between;\r\n margin-bottom: 10px;\r\n}\r\n.diff-side {\r\n width: 48%;\r\n padding: 10px;\r\n}\r\n.diff-old {\r\n background-color: #fee;\r\n border: 1px solid #f00;\r\n}\r\n.diff-new {\r\n background-color: #e6ffe6;\r\n border: 1px solid #0f0;\r\n}\r\n.radio-buttons {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 10px;\r\n}\r\n";
|
|
|
|
let ws;
|
|
let messageQueue = [];
|
|
async function initWebsocket(url) {
|
|
ws = new WebSocket(url);
|
|
if (ws !== null) {
|
|
ws.onopen = async (event) => {
|
|
console.log("WebSocket connection established");
|
|
while (messageQueue.length > 0) {
|
|
const message = messageQueue.shift();
|
|
if (message) {
|
|
ws.send(message);
|
|
}
|
|
}
|
|
};
|
|
ws.onmessage = (event) => {
|
|
const msgData = event.data;
|
|
(async () => {
|
|
if (typeof msgData === "string") {
|
|
try {
|
|
const parsedMessage = JSON.parse(msgData);
|
|
const services = await Services.getInstance();
|
|
switch (parsedMessage.flag) {
|
|
case "Handshake":
|
|
await services.handleHandshakeMsg(url, parsedMessage.content);
|
|
break;
|
|
case "NewTx":
|
|
await services.parseNewTx(parsedMessage.content);
|
|
break;
|
|
case "Cipher":
|
|
await services.parseCipher(parsedMessage.content);
|
|
break;
|
|
case "Commit":
|
|
await services.handleCommitError(parsedMessage.content);
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.error("Received an invalid message:", error);
|
|
}
|
|
} else {
|
|
console.error("Received a non-string message");
|
|
}
|
|
})();
|
|
};
|
|
ws.onerror = (event) => {
|
|
console.error("WebSocket error:", event);
|
|
};
|
|
ws.onclose = (event) => {
|
|
console.log("WebSocket is closed now.");
|
|
};
|
|
}
|
|
}
|
|
function sendMessage(flag, message) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
const networkMessage = {
|
|
flag,
|
|
content: message
|
|
};
|
|
console.log("Sending message of type:", flag);
|
|
ws.send(JSON.stringify(networkMessage));
|
|
} else {
|
|
console.error("WebSocket is not open. ReadyState:", ws.readyState);
|
|
messageQueue.push(message);
|
|
}
|
|
}
|
|
|
|
function getCorrectDOM(componentTag) {
|
|
const dom = document?.querySelector(componentTag)?.shadowRoot || document;
|
|
return dom;
|
|
}
|
|
|
|
let subscriptions = [];
|
|
function cleanSubscriptions() {
|
|
console.log("🚀 ~ cleanSubscriptions ~ sub:", subscriptions);
|
|
for (const sub of subscriptions) {
|
|
const el = sub.element;
|
|
const eventHandler = sub.eventHandler;
|
|
if (el) {
|
|
el.removeEventListener(sub.event, eventHandler);
|
|
}
|
|
}
|
|
subscriptions = [];
|
|
}
|
|
function addSubscription(element, event, eventHandler) {
|
|
if (!element)
|
|
return;
|
|
subscriptions.push({ element, event, eventHandler });
|
|
element.addEventListener(event, eventHandler);
|
|
}
|
|
|
|
// can-promise has a crash in some versions of react native that dont have
|
|
// standard global objects
|
|
// https://github.com/soldair/node-qrcode/issues/157
|
|
|
|
var canPromise$1 = function () {
|
|
return typeof Promise === 'function' && Promise.prototype && Promise.prototype.then
|
|
};
|
|
|
|
var qrcode = {};
|
|
|
|
var utils$3 = {};
|
|
|
|
let toSJISFunction;
|
|
const CODEWORDS_COUNT = [
|
|
0, // Not used
|
|
26, 44, 70, 100, 134, 172, 196, 242, 292, 346,
|
|
404, 466, 532, 581, 655, 733, 815, 901, 991, 1085,
|
|
1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185,
|
|
2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706
|
|
];
|
|
|
|
/**
|
|
* Returns the QR Code size for the specified version
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Number} size of QR code
|
|
*/
|
|
utils$3.getSymbolSize = function getSymbolSize (version) {
|
|
if (!version) throw new Error('"version" cannot be null or undefined')
|
|
if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40')
|
|
return version * 4 + 17
|
|
};
|
|
|
|
/**
|
|
* Returns the total number of codewords used to store data and EC information.
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Number} Data length in bits
|
|
*/
|
|
utils$3.getSymbolTotalCodewords = function getSymbolTotalCodewords (version) {
|
|
return CODEWORDS_COUNT[version]
|
|
};
|
|
|
|
/**
|
|
* Encode data with Bose-Chaudhuri-Hocquenghem
|
|
*
|
|
* @param {Number} data Value to encode
|
|
* @return {Number} Encoded value
|
|
*/
|
|
utils$3.getBCHDigit = function (data) {
|
|
let digit = 0;
|
|
|
|
while (data !== 0) {
|
|
digit++;
|
|
data >>>= 1;
|
|
}
|
|
|
|
return digit
|
|
};
|
|
|
|
utils$3.setToSJISFunction = function setToSJISFunction (f) {
|
|
if (typeof f !== 'function') {
|
|
throw new Error('"toSJISFunc" is not a valid function.')
|
|
}
|
|
|
|
toSJISFunction = f;
|
|
};
|
|
|
|
utils$3.isKanjiModeEnabled = function () {
|
|
return typeof toSJISFunction !== 'undefined'
|
|
};
|
|
|
|
utils$3.toSJIS = function toSJIS (kanji) {
|
|
return toSJISFunction(kanji)
|
|
};
|
|
|
|
var errorCorrectionLevel = {};
|
|
|
|
(function (exports) {
|
|
exports.L = { bit: 1 };
|
|
exports.M = { bit: 0 };
|
|
exports.Q = { bit: 3 };
|
|
exports.H = { bit: 2 };
|
|
|
|
function fromString (string) {
|
|
if (typeof string !== 'string') {
|
|
throw new Error('Param is not a string')
|
|
}
|
|
|
|
const lcStr = string.toLowerCase();
|
|
|
|
switch (lcStr) {
|
|
case 'l':
|
|
case 'low':
|
|
return exports.L
|
|
|
|
case 'm':
|
|
case 'medium':
|
|
return exports.M
|
|
|
|
case 'q':
|
|
case 'quartile':
|
|
return exports.Q
|
|
|
|
case 'h':
|
|
case 'high':
|
|
return exports.H
|
|
|
|
default:
|
|
throw new Error('Unknown EC Level: ' + string)
|
|
}
|
|
}
|
|
|
|
exports.isValid = function isValid (level) {
|
|
return level && typeof level.bit !== 'undefined' &&
|
|
level.bit >= 0 && level.bit < 4
|
|
};
|
|
|
|
exports.from = function from (value, defaultValue) {
|
|
if (exports.isValid(value)) {
|
|
return value
|
|
}
|
|
|
|
try {
|
|
return fromString(value)
|
|
} catch (e) {
|
|
return defaultValue
|
|
}
|
|
};
|
|
} (errorCorrectionLevel));
|
|
|
|
function BitBuffer$1 () {
|
|
this.buffer = [];
|
|
this.length = 0;
|
|
}
|
|
|
|
BitBuffer$1.prototype = {
|
|
|
|
get: function (index) {
|
|
const bufIndex = Math.floor(index / 8);
|
|
return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1
|
|
},
|
|
|
|
put: function (num, length) {
|
|
for (let i = 0; i < length; i++) {
|
|
this.putBit(((num >>> (length - i - 1)) & 1) === 1);
|
|
}
|
|
},
|
|
|
|
getLengthInBits: function () {
|
|
return this.length
|
|
},
|
|
|
|
putBit: function (bit) {
|
|
const bufIndex = Math.floor(this.length / 8);
|
|
if (this.buffer.length <= bufIndex) {
|
|
this.buffer.push(0);
|
|
}
|
|
|
|
if (bit) {
|
|
this.buffer[bufIndex] |= (0x80 >>> (this.length % 8));
|
|
}
|
|
|
|
this.length++;
|
|
}
|
|
};
|
|
|
|
var bitBuffer = BitBuffer$1;
|
|
|
|
/**
|
|
* Helper class to handle QR Code symbol modules
|
|
*
|
|
* @param {Number} size Symbol size
|
|
*/
|
|
|
|
function BitMatrix$1 (size) {
|
|
if (!size || size < 1) {
|
|
throw new Error('BitMatrix size must be defined and greater than 0')
|
|
}
|
|
|
|
this.size = size;
|
|
this.data = new Uint8Array(size * size);
|
|
this.reservedBit = new Uint8Array(size * size);
|
|
}
|
|
|
|
/**
|
|
* Set bit value at specified location
|
|
* If reserved flag is set, this bit will be ignored during masking process
|
|
*
|
|
* @param {Number} row
|
|
* @param {Number} col
|
|
* @param {Boolean} value
|
|
* @param {Boolean} reserved
|
|
*/
|
|
BitMatrix$1.prototype.set = function (row, col, value, reserved) {
|
|
const index = row * this.size + col;
|
|
this.data[index] = value;
|
|
if (reserved) this.reservedBit[index] = true;
|
|
};
|
|
|
|
/**
|
|
* Returns bit value at specified location
|
|
*
|
|
* @param {Number} row
|
|
* @param {Number} col
|
|
* @return {Boolean}
|
|
*/
|
|
BitMatrix$1.prototype.get = function (row, col) {
|
|
return this.data[row * this.size + col]
|
|
};
|
|
|
|
/**
|
|
* Applies xor operator at specified location
|
|
* (used during masking process)
|
|
*
|
|
* @param {Number} row
|
|
* @param {Number} col
|
|
* @param {Boolean} value
|
|
*/
|
|
BitMatrix$1.prototype.xor = function (row, col, value) {
|
|
this.data[row * this.size + col] ^= value;
|
|
};
|
|
|
|
/**
|
|
* Check if bit at specified location is reserved
|
|
*
|
|
* @param {Number} row
|
|
* @param {Number} col
|
|
* @return {Boolean}
|
|
*/
|
|
BitMatrix$1.prototype.isReserved = function (row, col) {
|
|
return this.reservedBit[row * this.size + col]
|
|
};
|
|
|
|
var bitMatrix = BitMatrix$1;
|
|
|
|
var alignmentPattern = {};
|
|
|
|
/**
|
|
* Alignment pattern are fixed reference pattern in defined positions
|
|
* in a matrix symbology, which enables the decode software to re-synchronise
|
|
* the coordinate mapping of the image modules in the event of moderate amounts
|
|
* of distortion of the image.
|
|
*
|
|
* Alignment patterns are present only in QR Code symbols of version 2 or larger
|
|
* and their number depends on the symbol version.
|
|
*/
|
|
|
|
(function (exports) {
|
|
const getSymbolSize = utils$3.getSymbolSize;
|
|
|
|
/**
|
|
* Calculate the row/column coordinates of the center module of each alignment pattern
|
|
* for the specified QR Code version.
|
|
*
|
|
* The alignment patterns are positioned symmetrically on either side of the diagonal
|
|
* running from the top left corner of the symbol to the bottom right corner.
|
|
*
|
|
* Since positions are simmetrical only half of the coordinates are returned.
|
|
* Each item of the array will represent in turn the x and y coordinate.
|
|
* @see {@link getPositions}
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Array} Array of coordinate
|
|
*/
|
|
exports.getRowColCoords = function getRowColCoords (version) {
|
|
if (version === 1) return []
|
|
|
|
const posCount = Math.floor(version / 7) + 2;
|
|
const size = getSymbolSize(version);
|
|
const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2;
|
|
const positions = [size - 7]; // Last coord is always (size - 7)
|
|
|
|
for (let i = 1; i < posCount - 1; i++) {
|
|
positions[i] = positions[i - 1] - intervals;
|
|
}
|
|
|
|
positions.push(6); // First coord is always 6
|
|
|
|
return positions.reverse()
|
|
};
|
|
|
|
/**
|
|
* Returns an array containing the positions of each alignment pattern.
|
|
* Each array's element represent the center point of the pattern as (x, y) coordinates
|
|
*
|
|
* Coordinates are calculated expanding the row/column coordinates returned by {@link getRowColCoords}
|
|
* and filtering out the items that overlaps with finder pattern
|
|
*
|
|
* @example
|
|
* For a Version 7 symbol {@link getRowColCoords} returns values 6, 22 and 38.
|
|
* The alignment patterns, therefore, are to be centered on (row, column)
|
|
* positions (6,22), (22,6), (22,22), (22,38), (38,22), (38,38).
|
|
* Note that the coordinates (6,6), (6,38), (38,6) are occupied by finder patterns
|
|
* and are not therefore used for alignment patterns.
|
|
*
|
|
* let pos = getPositions(7)
|
|
* // [[6,22], [22,6], [22,22], [22,38], [38,22], [38,38]]
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Array} Array of coordinates
|
|
*/
|
|
exports.getPositions = function getPositions (version) {
|
|
const coords = [];
|
|
const pos = exports.getRowColCoords(version);
|
|
const posLength = pos.length;
|
|
|
|
for (let i = 0; i < posLength; i++) {
|
|
for (let j = 0; j < posLength; j++) {
|
|
// Skip if position is occupied by finder patterns
|
|
if ((i === 0 && j === 0) || // top-left
|
|
(i === 0 && j === posLength - 1) || // bottom-left
|
|
(i === posLength - 1 && j === 0)) { // top-right
|
|
continue
|
|
}
|
|
|
|
coords.push([pos[i], pos[j]]);
|
|
}
|
|
}
|
|
|
|
return coords
|
|
};
|
|
} (alignmentPattern));
|
|
|
|
var finderPattern = {};
|
|
|
|
const getSymbolSize = utils$3.getSymbolSize;
|
|
const FINDER_PATTERN_SIZE = 7;
|
|
|
|
/**
|
|
* Returns an array containing the positions of each finder pattern.
|
|
* Each array's element represent the top-left point of the pattern as (x, y) coordinates
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Array} Array of coordinates
|
|
*/
|
|
finderPattern.getPositions = function getPositions (version) {
|
|
const size = getSymbolSize(version);
|
|
|
|
return [
|
|
// top-left
|
|
[0, 0],
|
|
// top-right
|
|
[size - FINDER_PATTERN_SIZE, 0],
|
|
// bottom-left
|
|
[0, size - FINDER_PATTERN_SIZE]
|
|
]
|
|
};
|
|
|
|
var maskPattern = {};
|
|
|
|
/**
|
|
* Data mask pattern reference
|
|
* @type {Object}
|
|
*/
|
|
|
|
(function (exports) {
|
|
exports.Patterns = {
|
|
PATTERN000: 0,
|
|
PATTERN001: 1,
|
|
PATTERN010: 2,
|
|
PATTERN011: 3,
|
|
PATTERN100: 4,
|
|
PATTERN101: 5,
|
|
PATTERN110: 6,
|
|
PATTERN111: 7
|
|
};
|
|
|
|
/**
|
|
* Weighted penalty scores for the undesirable features
|
|
* @type {Object}
|
|
*/
|
|
const PenaltyScores = {
|
|
N1: 3,
|
|
N2: 3,
|
|
N3: 40,
|
|
N4: 10
|
|
};
|
|
|
|
/**
|
|
* Check if mask pattern value is valid
|
|
*
|
|
* @param {Number} mask Mask pattern
|
|
* @return {Boolean} true if valid, false otherwise
|
|
*/
|
|
exports.isValid = function isValid (mask) {
|
|
return mask != null && mask !== '' && !isNaN(mask) && mask >= 0 && mask <= 7
|
|
};
|
|
|
|
/**
|
|
* Returns mask pattern from a value.
|
|
* If value is not valid, returns undefined
|
|
*
|
|
* @param {Number|String} value Mask pattern value
|
|
* @return {Number} Valid mask pattern or undefined
|
|
*/
|
|
exports.from = function from (value) {
|
|
return exports.isValid(value) ? parseInt(value, 10) : undefined
|
|
};
|
|
|
|
/**
|
|
* Find adjacent modules in row/column with the same color
|
|
* and assign a penalty value.
|
|
*
|
|
* Points: N1 + i
|
|
* i is the amount by which the number of adjacent modules of the same color exceeds 5
|
|
*/
|
|
exports.getPenaltyN1 = function getPenaltyN1 (data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
let sameCountCol = 0;
|
|
let sameCountRow = 0;
|
|
let lastCol = null;
|
|
let lastRow = null;
|
|
|
|
for (let row = 0; row < size; row++) {
|
|
sameCountCol = sameCountRow = 0;
|
|
lastCol = lastRow = null;
|
|
|
|
for (let col = 0; col < size; col++) {
|
|
let module = data.get(row, col);
|
|
if (module === lastCol) {
|
|
sameCountCol++;
|
|
} else {
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5);
|
|
lastCol = module;
|
|
sameCountCol = 1;
|
|
}
|
|
|
|
module = data.get(col, row);
|
|
if (module === lastRow) {
|
|
sameCountRow++;
|
|
} else {
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5);
|
|
lastRow = module;
|
|
sameCountRow = 1;
|
|
}
|
|
}
|
|
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5);
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5);
|
|
}
|
|
|
|
return points
|
|
};
|
|
|
|
/**
|
|
* Find 2x2 blocks with the same color and assign a penalty value
|
|
*
|
|
* Points: N2 * (m - 1) * (n - 1)
|
|
*/
|
|
exports.getPenaltyN2 = function getPenaltyN2 (data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
|
|
for (let row = 0; row < size - 1; row++) {
|
|
for (let col = 0; col < size - 1; col++) {
|
|
const last = data.get(row, col) +
|
|
data.get(row, col + 1) +
|
|
data.get(row + 1, col) +
|
|
data.get(row + 1, col + 1);
|
|
|
|
if (last === 4 || last === 0) points++;
|
|
}
|
|
}
|
|
|
|
return points * PenaltyScores.N2
|
|
};
|
|
|
|
/**
|
|
* Find 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column,
|
|
* preceded or followed by light area 4 modules wide
|
|
*
|
|
* Points: N3 * number of pattern found
|
|
*/
|
|
exports.getPenaltyN3 = function getPenaltyN3 (data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
let bitsCol = 0;
|
|
let bitsRow = 0;
|
|
|
|
for (let row = 0; row < size; row++) {
|
|
bitsCol = bitsRow = 0;
|
|
for (let col = 0; col < size; col++) {
|
|
bitsCol = ((bitsCol << 1) & 0x7FF) | data.get(row, col);
|
|
if (col >= 10 && (bitsCol === 0x5D0 || bitsCol === 0x05D)) points++;
|
|
|
|
bitsRow = ((bitsRow << 1) & 0x7FF) | data.get(col, row);
|
|
if (col >= 10 && (bitsRow === 0x5D0 || bitsRow === 0x05D)) points++;
|
|
}
|
|
}
|
|
|
|
return points * PenaltyScores.N3
|
|
};
|
|
|
|
/**
|
|
* Calculate proportion of dark modules in entire symbol
|
|
*
|
|
* Points: N4 * k
|
|
*
|
|
* k is the rating of the deviation of the proportion of dark modules
|
|
* in the symbol from 50% in steps of 5%
|
|
*/
|
|
exports.getPenaltyN4 = function getPenaltyN4 (data) {
|
|
let darkCount = 0;
|
|
const modulesCount = data.data.length;
|
|
|
|
for (let i = 0; i < modulesCount; i++) darkCount += data.data[i];
|
|
|
|
const k = Math.abs(Math.ceil((darkCount * 100 / modulesCount) / 5) - 10);
|
|
|
|
return k * PenaltyScores.N4
|
|
};
|
|
|
|
/**
|
|
* Return mask value at given position
|
|
*
|
|
* @param {Number} maskPattern Pattern reference value
|
|
* @param {Number} i Row
|
|
* @param {Number} j Column
|
|
* @return {Boolean} Mask value
|
|
*/
|
|
function getMaskAt (maskPattern, i, j) {
|
|
switch (maskPattern) {
|
|
case exports.Patterns.PATTERN000: return (i + j) % 2 === 0
|
|
case exports.Patterns.PATTERN001: return i % 2 === 0
|
|
case exports.Patterns.PATTERN010: return j % 3 === 0
|
|
case exports.Patterns.PATTERN011: return (i + j) % 3 === 0
|
|
case exports.Patterns.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0
|
|
case exports.Patterns.PATTERN101: return (i * j) % 2 + (i * j) % 3 === 0
|
|
case exports.Patterns.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 === 0
|
|
case exports.Patterns.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 === 0
|
|
|
|
default: throw new Error('bad maskPattern:' + maskPattern)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a mask pattern to a BitMatrix
|
|
*
|
|
* @param {Number} pattern Pattern reference number
|
|
* @param {BitMatrix} data BitMatrix data
|
|
*/
|
|
exports.applyMask = function applyMask (pattern, data) {
|
|
const size = data.size;
|
|
|
|
for (let col = 0; col < size; col++) {
|
|
for (let row = 0; row < size; row++) {
|
|
if (data.isReserved(row, col)) continue
|
|
data.xor(row, col, getMaskAt(pattern, row, col));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the best mask pattern for data
|
|
*
|
|
* @param {BitMatrix} data
|
|
* @return {Number} Mask pattern reference number
|
|
*/
|
|
exports.getBestMask = function getBestMask (data, setupFormatFunc) {
|
|
const numPatterns = Object.keys(exports.Patterns).length;
|
|
let bestPattern = 0;
|
|
let lowerPenalty = Infinity;
|
|
|
|
for (let p = 0; p < numPatterns; p++) {
|
|
setupFormatFunc(p);
|
|
exports.applyMask(p, data);
|
|
|
|
// Calculate penalty
|
|
const penalty =
|
|
exports.getPenaltyN1(data) +
|
|
exports.getPenaltyN2(data) +
|
|
exports.getPenaltyN3(data) +
|
|
exports.getPenaltyN4(data);
|
|
|
|
// Undo previously applied mask
|
|
exports.applyMask(p, data);
|
|
|
|
if (penalty < lowerPenalty) {
|
|
lowerPenalty = penalty;
|
|
bestPattern = p;
|
|
}
|
|
}
|
|
|
|
return bestPattern
|
|
};
|
|
} (maskPattern));
|
|
|
|
var errorCorrectionCode = {};
|
|
|
|
const ECLevel$1 = errorCorrectionLevel;
|
|
|
|
const EC_BLOCKS_TABLE = [
|
|
// L M Q H
|
|
1, 1, 1, 1,
|
|
1, 1, 1, 1,
|
|
1, 1, 2, 2,
|
|
1, 2, 2, 4,
|
|
1, 2, 4, 4,
|
|
2, 4, 4, 4,
|
|
2, 4, 6, 5,
|
|
2, 4, 6, 6,
|
|
2, 5, 8, 8,
|
|
4, 5, 8, 8,
|
|
4, 5, 8, 11,
|
|
4, 8, 10, 11,
|
|
4, 9, 12, 16,
|
|
4, 9, 16, 16,
|
|
6, 10, 12, 18,
|
|
6, 10, 17, 16,
|
|
6, 11, 16, 19,
|
|
6, 13, 18, 21,
|
|
7, 14, 21, 25,
|
|
8, 16, 20, 25,
|
|
8, 17, 23, 25,
|
|
9, 17, 23, 34,
|
|
9, 18, 25, 30,
|
|
10, 20, 27, 32,
|
|
12, 21, 29, 35,
|
|
12, 23, 34, 37,
|
|
12, 25, 34, 40,
|
|
13, 26, 35, 42,
|
|
14, 28, 38, 45,
|
|
15, 29, 40, 48,
|
|
16, 31, 43, 51,
|
|
17, 33, 45, 54,
|
|
18, 35, 48, 57,
|
|
19, 37, 51, 60,
|
|
19, 38, 53, 63,
|
|
20, 40, 56, 66,
|
|
21, 43, 59, 70,
|
|
22, 45, 62, 74,
|
|
24, 47, 65, 77,
|
|
25, 49, 68, 81
|
|
];
|
|
|
|
const EC_CODEWORDS_TABLE = [
|
|
// L M Q H
|
|
7, 10, 13, 17,
|
|
10, 16, 22, 28,
|
|
15, 26, 36, 44,
|
|
20, 36, 52, 64,
|
|
26, 48, 72, 88,
|
|
36, 64, 96, 112,
|
|
40, 72, 108, 130,
|
|
48, 88, 132, 156,
|
|
60, 110, 160, 192,
|
|
72, 130, 192, 224,
|
|
80, 150, 224, 264,
|
|
96, 176, 260, 308,
|
|
104, 198, 288, 352,
|
|
120, 216, 320, 384,
|
|
132, 240, 360, 432,
|
|
144, 280, 408, 480,
|
|
168, 308, 448, 532,
|
|
180, 338, 504, 588,
|
|
196, 364, 546, 650,
|
|
224, 416, 600, 700,
|
|
224, 442, 644, 750,
|
|
252, 476, 690, 816,
|
|
270, 504, 750, 900,
|
|
300, 560, 810, 960,
|
|
312, 588, 870, 1050,
|
|
336, 644, 952, 1110,
|
|
360, 700, 1020, 1200,
|
|
390, 728, 1050, 1260,
|
|
420, 784, 1140, 1350,
|
|
450, 812, 1200, 1440,
|
|
480, 868, 1290, 1530,
|
|
510, 924, 1350, 1620,
|
|
540, 980, 1440, 1710,
|
|
570, 1036, 1530, 1800,
|
|
570, 1064, 1590, 1890,
|
|
600, 1120, 1680, 1980,
|
|
630, 1204, 1770, 2100,
|
|
660, 1260, 1860, 2220,
|
|
720, 1316, 1950, 2310,
|
|
750, 1372, 2040, 2430
|
|
];
|
|
|
|
/**
|
|
* Returns the number of error correction block that the QR Code should contain
|
|
* for the specified version and error correction level.
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @param {Number} errorCorrectionLevel Error correction level
|
|
* @return {Number} Number of error correction blocks
|
|
*/
|
|
errorCorrectionCode.getBlocksCount = function getBlocksCount (version, errorCorrectionLevel) {
|
|
switch (errorCorrectionLevel) {
|
|
case ECLevel$1.L:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 0]
|
|
case ECLevel$1.M:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 1]
|
|
case ECLevel$1.Q:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 2]
|
|
case ECLevel$1.H:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 3]
|
|
default:
|
|
return undefined
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the number of error correction codewords to use for the specified
|
|
* version and error correction level.
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @param {Number} errorCorrectionLevel Error correction level
|
|
* @return {Number} Number of error correction codewords
|
|
*/
|
|
errorCorrectionCode.getTotalCodewordsCount = function getTotalCodewordsCount (version, errorCorrectionLevel) {
|
|
switch (errorCorrectionLevel) {
|
|
case ECLevel$1.L:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0]
|
|
case ECLevel$1.M:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1]
|
|
case ECLevel$1.Q:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2]
|
|
case ECLevel$1.H:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3]
|
|
default:
|
|
return undefined
|
|
}
|
|
};
|
|
|
|
var polynomial = {};
|
|
|
|
var galoisField = {};
|
|
|
|
const EXP_TABLE = new Uint8Array(512);
|
|
const LOG_TABLE = new Uint8Array(256)
|
|
/**
|
|
* Precompute the log and anti-log tables for faster computation later
|
|
*
|
|
* For each possible value in the galois field 2^8, we will pre-compute
|
|
* the logarithm and anti-logarithm (exponential) of this value
|
|
*
|
|
* ref {@link https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Introduction_to_mathematical_fields}
|
|
*/
|
|
;(function initTables () {
|
|
let x = 1;
|
|
for (let i = 0; i < 255; i++) {
|
|
EXP_TABLE[i] = x;
|
|
LOG_TABLE[x] = i;
|
|
|
|
x <<= 1; // multiply by 2
|
|
|
|
// The QR code specification says to use byte-wise modulo 100011101 arithmetic.
|
|
// This means that when a number is 256 or larger, it should be XORed with 0x11D.
|
|
if (x & 0x100) { // similar to x >= 256, but a lot faster (because 0x100 == 256)
|
|
x ^= 0x11D;
|
|
}
|
|
}
|
|
|
|
// Optimization: double the size of the anti-log table so that we don't need to mod 255 to
|
|
// stay inside the bounds (because we will mainly use this table for the multiplication of
|
|
// two GF numbers, no more).
|
|
// @see {@link mul}
|
|
for (let i = 255; i < 512; i++) {
|
|
EXP_TABLE[i] = EXP_TABLE[i - 255];
|
|
}
|
|
}());
|
|
|
|
/**
|
|
* Returns log value of n inside Galois Field
|
|
*
|
|
* @param {Number} n
|
|
* @return {Number}
|
|
*/
|
|
galoisField.log = function log (n) {
|
|
if (n < 1) throw new Error('log(' + n + ')')
|
|
return LOG_TABLE[n]
|
|
};
|
|
|
|
/**
|
|
* Returns anti-log value of n inside Galois Field
|
|
*
|
|
* @param {Number} n
|
|
* @return {Number}
|
|
*/
|
|
galoisField.exp = function exp (n) {
|
|
return EXP_TABLE[n]
|
|
};
|
|
|
|
/**
|
|
* Multiplies two number inside Galois Field
|
|
*
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {Number}
|
|
*/
|
|
galoisField.mul = function mul (x, y) {
|
|
if (x === 0 || y === 0) return 0
|
|
|
|
// should be EXP_TABLE[(LOG_TABLE[x] + LOG_TABLE[y]) % 255] if EXP_TABLE wasn't oversized
|
|
// @see {@link initTables}
|
|
return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]]
|
|
};
|
|
|
|
(function (exports) {
|
|
const GF = galoisField;
|
|
|
|
/**
|
|
* Multiplies two polynomials inside Galois Field
|
|
*
|
|
* @param {Uint8Array} p1 Polynomial
|
|
* @param {Uint8Array} p2 Polynomial
|
|
* @return {Uint8Array} Product of p1 and p2
|
|
*/
|
|
exports.mul = function mul (p1, p2) {
|
|
const coeff = new Uint8Array(p1.length + p2.length - 1);
|
|
|
|
for (let i = 0; i < p1.length; i++) {
|
|
for (let j = 0; j < p2.length; j++) {
|
|
coeff[i + j] ^= GF.mul(p1[i], p2[j]);
|
|
}
|
|
}
|
|
|
|
return coeff
|
|
};
|
|
|
|
/**
|
|
* Calculate the remainder of polynomials division
|
|
*
|
|
* @param {Uint8Array} divident Polynomial
|
|
* @param {Uint8Array} divisor Polynomial
|
|
* @return {Uint8Array} Remainder
|
|
*/
|
|
exports.mod = function mod (divident, divisor) {
|
|
let result = new Uint8Array(divident);
|
|
|
|
while ((result.length - divisor.length) >= 0) {
|
|
const coeff = result[0];
|
|
|
|
for (let i = 0; i < divisor.length; i++) {
|
|
result[i] ^= GF.mul(divisor[i], coeff);
|
|
}
|
|
|
|
// remove all zeros from buffer head
|
|
let offset = 0;
|
|
while (offset < result.length && result[offset] === 0) offset++;
|
|
result = result.slice(offset);
|
|
}
|
|
|
|
return result
|
|
};
|
|
|
|
/**
|
|
* Generate an irreducible generator polynomial of specified degree
|
|
* (used by Reed-Solomon encoder)
|
|
*
|
|
* @param {Number} degree Degree of the generator polynomial
|
|
* @return {Uint8Array} Buffer containing polynomial coefficients
|
|
*/
|
|
exports.generateECPolynomial = function generateECPolynomial (degree) {
|
|
let poly = new Uint8Array([1]);
|
|
for (let i = 0; i < degree; i++) {
|
|
poly = exports.mul(poly, new Uint8Array([1, GF.exp(i)]));
|
|
}
|
|
|
|
return poly
|
|
};
|
|
} (polynomial));
|
|
|
|
const Polynomial = polynomial;
|
|
|
|
function ReedSolomonEncoder$1 (degree) {
|
|
this.genPoly = undefined;
|
|
this.degree = degree;
|
|
|
|
if (this.degree) this.initialize(this.degree);
|
|
}
|
|
|
|
/**
|
|
* Initialize the encoder.
|
|
* The input param should correspond to the number of error correction codewords.
|
|
*
|
|
* @param {Number} degree
|
|
*/
|
|
ReedSolomonEncoder$1.prototype.initialize = function initialize (degree) {
|
|
// create an irreducible generator polynomial
|
|
this.degree = degree;
|
|
this.genPoly = Polynomial.generateECPolynomial(this.degree);
|
|
};
|
|
|
|
/**
|
|
* Encodes a chunk of data
|
|
*
|
|
* @param {Uint8Array} data Buffer containing input data
|
|
* @return {Uint8Array} Buffer containing encoded data
|
|
*/
|
|
ReedSolomonEncoder$1.prototype.encode = function encode (data) {
|
|
if (!this.genPoly) {
|
|
throw new Error('Encoder not initialized')
|
|
}
|
|
|
|
// Calculate EC for this data block
|
|
// extends data size to data+genPoly size
|
|
const paddedData = new Uint8Array(data.length + this.degree);
|
|
paddedData.set(data);
|
|
|
|
// The error correction codewords are the remainder after dividing the data codewords
|
|
// by a generator polynomial
|
|
const remainder = Polynomial.mod(paddedData, this.genPoly);
|
|
|
|
// return EC data blocks (last n byte, where n is the degree of genPoly)
|
|
// If coefficients number in remainder are less than genPoly degree,
|
|
// pad with 0s to the left to reach the needed number of coefficients
|
|
const start = this.degree - remainder.length;
|
|
if (start > 0) {
|
|
const buff = new Uint8Array(this.degree);
|
|
buff.set(remainder, start);
|
|
|
|
return buff
|
|
}
|
|
|
|
return remainder
|
|
};
|
|
|
|
var reedSolomonEncoder = ReedSolomonEncoder$1;
|
|
|
|
var version = {};
|
|
|
|
var mode = {};
|
|
|
|
var versionCheck = {};
|
|
|
|
/**
|
|
* Check if QR Code version is valid
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Boolean} true if valid version, false otherwise
|
|
*/
|
|
|
|
versionCheck.isValid = function isValid (version) {
|
|
return !isNaN(version) && version >= 1 && version <= 40
|
|
};
|
|
|
|
var regex = {};
|
|
|
|
const numeric = '[0-9]+';
|
|
const alphanumeric = '[A-Z $%*+\\-./:]+';
|
|
let kanji = '(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|' +
|
|
'[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|' +
|
|
'[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|' +
|
|
'[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+';
|
|
kanji = kanji.replace(/u/g, '\\u');
|
|
|
|
const byte = '(?:(?![A-Z0-9 $%*+\\-./:]|' + kanji + ')(?:.|[\r\n]))+';
|
|
|
|
regex.KANJI = new RegExp(kanji, 'g');
|
|
regex.BYTE_KANJI = new RegExp('[^A-Z0-9 $%*+\\-./:]+', 'g');
|
|
regex.BYTE = new RegExp(byte, 'g');
|
|
regex.NUMERIC = new RegExp(numeric, 'g');
|
|
regex.ALPHANUMERIC = new RegExp(alphanumeric, 'g');
|
|
|
|
const TEST_KANJI = new RegExp('^' + kanji + '$');
|
|
const TEST_NUMERIC = new RegExp('^' + numeric + '$');
|
|
const TEST_ALPHANUMERIC = new RegExp('^[A-Z0-9 $%*+\\-./:]+$');
|
|
|
|
regex.testKanji = function testKanji (str) {
|
|
return TEST_KANJI.test(str)
|
|
};
|
|
|
|
regex.testNumeric = function testNumeric (str) {
|
|
return TEST_NUMERIC.test(str)
|
|
};
|
|
|
|
regex.testAlphanumeric = function testAlphanumeric (str) {
|
|
return TEST_ALPHANUMERIC.test(str)
|
|
};
|
|
|
|
(function (exports) {
|
|
const VersionCheck = versionCheck;
|
|
const Regex = regex;
|
|
|
|
/**
|
|
* Numeric mode encodes data from the decimal digit set (0 - 9)
|
|
* (byte values 30HEX to 39HEX).
|
|
* Normally, 3 data characters are represented by 10 bits.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
exports.NUMERIC = {
|
|
id: 'Numeric',
|
|
bit: 1 << 0,
|
|
ccBits: [10, 12, 14]
|
|
};
|
|
|
|
/**
|
|
* Alphanumeric mode encodes data from a set of 45 characters,
|
|
* i.e. 10 numeric digits (0 - 9),
|
|
* 26 alphabetic characters (A - Z),
|
|
* and 9 symbols (SP, $, %, *, +, -, ., /, :).
|
|
* Normally, two input characters are represented by 11 bits.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
exports.ALPHANUMERIC = {
|
|
id: 'Alphanumeric',
|
|
bit: 1 << 1,
|
|
ccBits: [9, 11, 13]
|
|
};
|
|
|
|
/**
|
|
* In byte mode, data is encoded at 8 bits per character.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
exports.BYTE = {
|
|
id: 'Byte',
|
|
bit: 1 << 2,
|
|
ccBits: [8, 16, 16]
|
|
};
|
|
|
|
/**
|
|
* The Kanji mode efficiently encodes Kanji characters in accordance with
|
|
* the Shift JIS system based on JIS X 0208.
|
|
* The Shift JIS values are shifted from the JIS X 0208 values.
|
|
* JIS X 0208 gives details of the shift coded representation.
|
|
* Each two-byte character value is compacted to a 13-bit binary codeword.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
exports.KANJI = {
|
|
id: 'Kanji',
|
|
bit: 1 << 3,
|
|
ccBits: [8, 10, 12]
|
|
};
|
|
|
|
/**
|
|
* Mixed mode will contain a sequences of data in a combination of any of
|
|
* the modes described above
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
exports.MIXED = {
|
|
bit: -1
|
|
};
|
|
|
|
/**
|
|
* Returns the number of bits needed to store the data length
|
|
* according to QR Code specifications.
|
|
*
|
|
* @param {Mode} mode Data mode
|
|
* @param {Number} version QR Code version
|
|
* @return {Number} Number of bits
|
|
*/
|
|
exports.getCharCountIndicator = function getCharCountIndicator (mode, version) {
|
|
if (!mode.ccBits) throw new Error('Invalid mode: ' + mode)
|
|
|
|
if (!VersionCheck.isValid(version)) {
|
|
throw new Error('Invalid version: ' + version)
|
|
}
|
|
|
|
if (version >= 1 && version < 10) return mode.ccBits[0]
|
|
else if (version < 27) return mode.ccBits[1]
|
|
return mode.ccBits[2]
|
|
};
|
|
|
|
/**
|
|
* Returns the most efficient mode to store the specified data
|
|
*
|
|
* @param {String} dataStr Input data string
|
|
* @return {Mode} Best mode
|
|
*/
|
|
exports.getBestModeForData = function getBestModeForData (dataStr) {
|
|
if (Regex.testNumeric(dataStr)) return exports.NUMERIC
|
|
else if (Regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC
|
|
else if (Regex.testKanji(dataStr)) return exports.KANJI
|
|
else return exports.BYTE
|
|
};
|
|
|
|
/**
|
|
* Return mode name as string
|
|
*
|
|
* @param {Mode} mode Mode object
|
|
* @returns {String} Mode name
|
|
*/
|
|
exports.toString = function toString (mode) {
|
|
if (mode && mode.id) return mode.id
|
|
throw new Error('Invalid mode')
|
|
};
|
|
|
|
/**
|
|
* Check if input param is a valid mode object
|
|
*
|
|
* @param {Mode} mode Mode object
|
|
* @returns {Boolean} True if valid mode, false otherwise
|
|
*/
|
|
exports.isValid = function isValid (mode) {
|
|
return mode && mode.bit && mode.ccBits
|
|
};
|
|
|
|
/**
|
|
* Get mode object from its name
|
|
*
|
|
* @param {String} string Mode name
|
|
* @returns {Mode} Mode object
|
|
*/
|
|
function fromString (string) {
|
|
if (typeof string !== 'string') {
|
|
throw new Error('Param is not a string')
|
|
}
|
|
|
|
const lcStr = string.toLowerCase();
|
|
|
|
switch (lcStr) {
|
|
case 'numeric':
|
|
return exports.NUMERIC
|
|
case 'alphanumeric':
|
|
return exports.ALPHANUMERIC
|
|
case 'kanji':
|
|
return exports.KANJI
|
|
case 'byte':
|
|
return exports.BYTE
|
|
default:
|
|
throw new Error('Unknown mode: ' + string)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns mode from a value.
|
|
* If value is not a valid mode, returns defaultValue
|
|
*
|
|
* @param {Mode|String} value Encoding mode
|
|
* @param {Mode} defaultValue Fallback value
|
|
* @return {Mode} Encoding mode
|
|
*/
|
|
exports.from = function from (value, defaultValue) {
|
|
if (exports.isValid(value)) {
|
|
return value
|
|
}
|
|
|
|
try {
|
|
return fromString(value)
|
|
} catch (e) {
|
|
return defaultValue
|
|
}
|
|
};
|
|
} (mode));
|
|
|
|
(function (exports) {
|
|
const Utils = utils$3;
|
|
const ECCode = errorCorrectionCode;
|
|
const ECLevel = errorCorrectionLevel;
|
|
const Mode = mode;
|
|
const VersionCheck = versionCheck;
|
|
|
|
// Generator polynomial used to encode version information
|
|
const G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
|
|
const G18_BCH = Utils.getBCHDigit(G18);
|
|
|
|
function getBestVersionForDataLength (mode, length, errorCorrectionLevel) {
|
|
for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
|
|
if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode)) {
|
|
return currentVersion
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function getReservedBitsCount (mode, version) {
|
|
// Character count indicator + mode indicator bits
|
|
return Mode.getCharCountIndicator(mode, version) + 4
|
|
}
|
|
|
|
function getTotalBitsFromDataArray (segments, version) {
|
|
let totalBits = 0;
|
|
|
|
segments.forEach(function (data) {
|
|
const reservedBits = getReservedBitsCount(data.mode, version);
|
|
totalBits += reservedBits + data.getBitsLength();
|
|
});
|
|
|
|
return totalBits
|
|
}
|
|
|
|
function getBestVersionForMixedData (segments, errorCorrectionLevel) {
|
|
for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
|
|
const length = getTotalBitsFromDataArray(segments, currentVersion);
|
|
if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, Mode.MIXED)) {
|
|
return currentVersion
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Returns version number from a value.
|
|
* If value is not a valid version, returns defaultValue
|
|
*
|
|
* @param {Number|String} value QR Code version
|
|
* @param {Number} defaultValue Fallback value
|
|
* @return {Number} QR Code version number
|
|
*/
|
|
exports.from = function from (value, defaultValue) {
|
|
if (VersionCheck.isValid(value)) {
|
|
return parseInt(value, 10)
|
|
}
|
|
|
|
return defaultValue
|
|
};
|
|
|
|
/**
|
|
* Returns how much data can be stored with the specified QR code version
|
|
* and error correction level
|
|
*
|
|
* @param {Number} version QR Code version (1-40)
|
|
* @param {Number} errorCorrectionLevel Error correction level
|
|
* @param {Mode} mode Data mode
|
|
* @return {Number} Quantity of storable data
|
|
*/
|
|
exports.getCapacity = function getCapacity (version, errorCorrectionLevel, mode) {
|
|
if (!VersionCheck.isValid(version)) {
|
|
throw new Error('Invalid QR Code version')
|
|
}
|
|
|
|
// Use Byte mode as default
|
|
if (typeof mode === 'undefined') mode = Mode.BYTE;
|
|
|
|
// Total codewords for this QR code version (Data + Error correction)
|
|
const totalCodewords = Utils.getSymbolTotalCodewords(version);
|
|
|
|
// Total number of error correction codewords
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
|
|
// Total number of data codewords
|
|
const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8;
|
|
|
|
if (mode === Mode.MIXED) return dataTotalCodewordsBits
|
|
|
|
const usableBits = dataTotalCodewordsBits - getReservedBitsCount(mode, version);
|
|
|
|
// Return max number of storable codewords
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return Math.floor((usableBits / 10) * 3)
|
|
|
|
case Mode.ALPHANUMERIC:
|
|
return Math.floor((usableBits / 11) * 2)
|
|
|
|
case Mode.KANJI:
|
|
return Math.floor(usableBits / 13)
|
|
|
|
case Mode.BYTE:
|
|
default:
|
|
return Math.floor(usableBits / 8)
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the minimum version needed to contain the amount of data
|
|
*
|
|
* @param {Segment} data Segment of data
|
|
* @param {Number} [errorCorrectionLevel=H] Error correction level
|
|
* @param {Mode} mode Data mode
|
|
* @return {Number} QR Code version
|
|
*/
|
|
exports.getBestVersionForData = function getBestVersionForData (data, errorCorrectionLevel) {
|
|
let seg;
|
|
|
|
const ecl = ECLevel.from(errorCorrectionLevel, ECLevel.M);
|
|
|
|
if (Array.isArray(data)) {
|
|
if (data.length > 1) {
|
|
return getBestVersionForMixedData(data, ecl)
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return 1
|
|
}
|
|
|
|
seg = data[0];
|
|
} else {
|
|
seg = data;
|
|
}
|
|
|
|
return getBestVersionForDataLength(seg.mode, seg.getLength(), ecl)
|
|
};
|
|
|
|
/**
|
|
* Returns version information with relative error correction bits
|
|
*
|
|
* The version information is included in QR Code symbols of version 7 or larger.
|
|
* It consists of an 18-bit sequence containing 6 data bits,
|
|
* with 12 error correction bits calculated using the (18, 6) Golay code.
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @return {Number} Encoded version info bits
|
|
*/
|
|
exports.getEncodedBits = function getEncodedBits (version) {
|
|
if (!VersionCheck.isValid(version) || version < 7) {
|
|
throw new Error('Invalid QR Code version')
|
|
}
|
|
|
|
let d = version << 12;
|
|
|
|
while (Utils.getBCHDigit(d) - G18_BCH >= 0) {
|
|
d ^= (G18 << (Utils.getBCHDigit(d) - G18_BCH));
|
|
}
|
|
|
|
return (version << 12) | d
|
|
};
|
|
} (version));
|
|
|
|
var formatInfo = {};
|
|
|
|
const Utils$3 = utils$3;
|
|
|
|
const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
|
|
const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
|
|
const G15_BCH = Utils$3.getBCHDigit(G15);
|
|
|
|
/**
|
|
* Returns format information with relative error correction bits
|
|
*
|
|
* The format information is a 15-bit sequence containing 5 data bits,
|
|
* with 10 error correction bits calculated using the (15, 5) BCH code.
|
|
*
|
|
* @param {Number} errorCorrectionLevel Error correction level
|
|
* @param {Number} mask Mask pattern
|
|
* @return {Number} Encoded format information bits
|
|
*/
|
|
formatInfo.getEncodedBits = function getEncodedBits (errorCorrectionLevel, mask) {
|
|
const data = ((errorCorrectionLevel.bit << 3) | mask);
|
|
let d = data << 10;
|
|
|
|
while (Utils$3.getBCHDigit(d) - G15_BCH >= 0) {
|
|
d ^= (G15 << (Utils$3.getBCHDigit(d) - G15_BCH));
|
|
}
|
|
|
|
// xor final data with mask pattern in order to ensure that
|
|
// no combination of Error Correction Level and data mask pattern
|
|
// will result in an all-zero data string
|
|
return ((data << 10) | d) ^ G15_MASK
|
|
};
|
|
|
|
var segments = {};
|
|
|
|
const Mode$4 = mode;
|
|
|
|
function NumericData (data) {
|
|
this.mode = Mode$4.NUMERIC;
|
|
this.data = data.toString();
|
|
}
|
|
|
|
NumericData.getBitsLength = function getBitsLength (length) {
|
|
return 10 * Math.floor(length / 3) + ((length % 3) ? ((length % 3) * 3 + 1) : 0)
|
|
};
|
|
|
|
NumericData.prototype.getLength = function getLength () {
|
|
return this.data.length
|
|
};
|
|
|
|
NumericData.prototype.getBitsLength = function getBitsLength () {
|
|
return NumericData.getBitsLength(this.data.length)
|
|
};
|
|
|
|
NumericData.prototype.write = function write (bitBuffer) {
|
|
let i, group, value;
|
|
|
|
// The input data string is divided into groups of three digits,
|
|
// and each group is converted to its 10-bit binary equivalent.
|
|
for (i = 0; i + 3 <= this.data.length; i += 3) {
|
|
group = this.data.substr(i, 3);
|
|
value = parseInt(group, 10);
|
|
|
|
bitBuffer.put(value, 10);
|
|
}
|
|
|
|
// If the number of input digits is not an exact multiple of three,
|
|
// the final one or two digits are converted to 4 or 7 bits respectively.
|
|
const remainingNum = this.data.length - i;
|
|
if (remainingNum > 0) {
|
|
group = this.data.substr(i);
|
|
value = parseInt(group, 10);
|
|
|
|
bitBuffer.put(value, remainingNum * 3 + 1);
|
|
}
|
|
};
|
|
|
|
var numericData = NumericData;
|
|
|
|
const Mode$3 = mode;
|
|
|
|
/**
|
|
* Array of characters available in alphanumeric mode
|
|
*
|
|
* As per QR Code specification, to each character
|
|
* is assigned a value from 0 to 44 which in this case coincides
|
|
* with the array index
|
|
*
|
|
* @type {Array}
|
|
*/
|
|
const ALPHA_NUM_CHARS = [
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
' ', '$', '%', '*', '+', '-', '.', '/', ':'
|
|
];
|
|
|
|
function AlphanumericData (data) {
|
|
this.mode = Mode$3.ALPHANUMERIC;
|
|
this.data = data;
|
|
}
|
|
|
|
AlphanumericData.getBitsLength = function getBitsLength (length) {
|
|
return 11 * Math.floor(length / 2) + 6 * (length % 2)
|
|
};
|
|
|
|
AlphanumericData.prototype.getLength = function getLength () {
|
|
return this.data.length
|
|
};
|
|
|
|
AlphanumericData.prototype.getBitsLength = function getBitsLength () {
|
|
return AlphanumericData.getBitsLength(this.data.length)
|
|
};
|
|
|
|
AlphanumericData.prototype.write = function write (bitBuffer) {
|
|
let i;
|
|
|
|
// Input data characters are divided into groups of two characters
|
|
// and encoded as 11-bit binary codes.
|
|
for (i = 0; i + 2 <= this.data.length; i += 2) {
|
|
// The character value of the first character is multiplied by 45
|
|
let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45;
|
|
|
|
// The character value of the second digit is added to the product
|
|
value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1]);
|
|
|
|
// The sum is then stored as 11-bit binary number
|
|
bitBuffer.put(value, 11);
|
|
}
|
|
|
|
// If the number of input data characters is not a multiple of two,
|
|
// the character value of the final character is encoded as a 6-bit binary number.
|
|
if (this.data.length % 2) {
|
|
bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6);
|
|
}
|
|
};
|
|
|
|
var alphanumericData = AlphanumericData;
|
|
|
|
const Mode$2 = mode;
|
|
|
|
function ByteData (data) {
|
|
this.mode = Mode$2.BYTE;
|
|
if (typeof (data) === 'string') {
|
|
this.data = new TextEncoder().encode(data);
|
|
} else {
|
|
this.data = new Uint8Array(data);
|
|
}
|
|
}
|
|
|
|
ByteData.getBitsLength = function getBitsLength (length) {
|
|
return length * 8
|
|
};
|
|
|
|
ByteData.prototype.getLength = function getLength () {
|
|
return this.data.length
|
|
};
|
|
|
|
ByteData.prototype.getBitsLength = function getBitsLength () {
|
|
return ByteData.getBitsLength(this.data.length)
|
|
};
|
|
|
|
ByteData.prototype.write = function (bitBuffer) {
|
|
for (let i = 0, l = this.data.length; i < l; i++) {
|
|
bitBuffer.put(this.data[i], 8);
|
|
}
|
|
};
|
|
|
|
var byteData = ByteData;
|
|
|
|
const Mode$1 = mode;
|
|
const Utils$2 = utils$3;
|
|
|
|
function KanjiData (data) {
|
|
this.mode = Mode$1.KANJI;
|
|
this.data = data;
|
|
}
|
|
|
|
KanjiData.getBitsLength = function getBitsLength (length) {
|
|
return length * 13
|
|
};
|
|
|
|
KanjiData.prototype.getLength = function getLength () {
|
|
return this.data.length
|
|
};
|
|
|
|
KanjiData.prototype.getBitsLength = function getBitsLength () {
|
|
return KanjiData.getBitsLength(this.data.length)
|
|
};
|
|
|
|
KanjiData.prototype.write = function (bitBuffer) {
|
|
let i;
|
|
|
|
// In the Shift JIS system, Kanji characters are represented by a two byte combination.
|
|
// These byte values are shifted from the JIS X 0208 values.
|
|
// JIS X 0208 gives details of the shift coded representation.
|
|
for (i = 0; i < this.data.length; i++) {
|
|
let value = Utils$2.toSJIS(this.data[i]);
|
|
|
|
// For characters with Shift JIS values from 0x8140 to 0x9FFC:
|
|
if (value >= 0x8140 && value <= 0x9FFC) {
|
|
// Subtract 0x8140 from Shift JIS value
|
|
value -= 0x8140;
|
|
|
|
// For characters with Shift JIS values from 0xE040 to 0xEBBF
|
|
} else if (value >= 0xE040 && value <= 0xEBBF) {
|
|
// Subtract 0xC140 from Shift JIS value
|
|
value -= 0xC140;
|
|
} else {
|
|
throw new Error(
|
|
'Invalid SJIS character: ' + this.data[i] + '\n' +
|
|
'Make sure your charset is UTF-8')
|
|
}
|
|
|
|
// Multiply most significant byte of result by 0xC0
|
|
// and add least significant byte to product
|
|
value = (((value >>> 8) & 0xff) * 0xC0) + (value & 0xff);
|
|
|
|
// Convert result to a 13-bit binary string
|
|
bitBuffer.put(value, 13);
|
|
}
|
|
};
|
|
|
|
var kanjiData = KanjiData;
|
|
|
|
var dijkstra = {exports: {}};
|
|
|
|
(function (module) {
|
|
|
|
/******************************************************************************
|
|
* Created 2008-08-19.
|
|
*
|
|
* Dijkstra path-finding functions. Adapted from the Dijkstar Python project.
|
|
*
|
|
* Copyright (C) 2008
|
|
* Wyatt Baldwin <self@wyattbaldwin.com>
|
|
* All rights reserved
|
|
*
|
|
* Licensed under the MIT license.
|
|
*
|
|
* http://www.opensource.org/licenses/mit-license.php
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*****************************************************************************/
|
|
var dijkstra = {
|
|
single_source_shortest_paths: function(graph, s, d) {
|
|
// Predecessor map for each node that has been encountered.
|
|
// node ID => predecessor node ID
|
|
var predecessors = {};
|
|
|
|
// Costs of shortest paths from s to all nodes encountered.
|
|
// node ID => cost
|
|
var costs = {};
|
|
costs[s] = 0;
|
|
|
|
// Costs of shortest paths from s to all nodes encountered; differs from
|
|
// `costs` in that it provides easy access to the node that currently has
|
|
// the known shortest path from s.
|
|
// XXX: Do we actually need both `costs` and `open`?
|
|
var open = dijkstra.PriorityQueue.make();
|
|
open.push(s, 0);
|
|
|
|
var closest,
|
|
u, v,
|
|
cost_of_s_to_u,
|
|
adjacent_nodes,
|
|
cost_of_e,
|
|
cost_of_s_to_u_plus_cost_of_e,
|
|
cost_of_s_to_v,
|
|
first_visit;
|
|
while (!open.empty()) {
|
|
// In the nodes remaining in graph that have a known cost from s,
|
|
// find the node, u, that currently has the shortest path from s.
|
|
closest = open.pop();
|
|
u = closest.value;
|
|
cost_of_s_to_u = closest.cost;
|
|
|
|
// Get nodes adjacent to u...
|
|
adjacent_nodes = graph[u] || {};
|
|
|
|
// ...and explore the edges that connect u to those nodes, updating
|
|
// the cost of the shortest paths to any or all of those nodes as
|
|
// necessary. v is the node across the current edge from u.
|
|
for (v in adjacent_nodes) {
|
|
if (adjacent_nodes.hasOwnProperty(v)) {
|
|
// Get the cost of the edge running from u to v.
|
|
cost_of_e = adjacent_nodes[v];
|
|
|
|
// Cost of s to u plus the cost of u to v across e--this is *a*
|
|
// cost from s to v that may or may not be less than the current
|
|
// known cost to v.
|
|
cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
|
|
|
|
// If we haven't visited v yet OR if the current known cost from s to
|
|
// v is greater than the new cost we just found (cost of s to u plus
|
|
// cost of u to v across e), update v's cost in the cost list and
|
|
// update v's predecessor in the predecessor list (it's now u).
|
|
cost_of_s_to_v = costs[v];
|
|
first_visit = (typeof costs[v] === 'undefined');
|
|
if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
|
|
costs[v] = cost_of_s_to_u_plus_cost_of_e;
|
|
open.push(v, cost_of_s_to_u_plus_cost_of_e);
|
|
predecessors[v] = u;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') {
|
|
var msg = ['Could not find a path from ', s, ' to ', d, '.'].join('');
|
|
throw new Error(msg);
|
|
}
|
|
|
|
return predecessors;
|
|
},
|
|
|
|
extract_shortest_path_from_predecessor_list: function(predecessors, d) {
|
|
var nodes = [];
|
|
var u = d;
|
|
while (u) {
|
|
nodes.push(u);
|
|
predecessors[u];
|
|
u = predecessors[u];
|
|
}
|
|
nodes.reverse();
|
|
return nodes;
|
|
},
|
|
|
|
find_path: function(graph, s, d) {
|
|
var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
|
|
return dijkstra.extract_shortest_path_from_predecessor_list(
|
|
predecessors, d);
|
|
},
|
|
|
|
/**
|
|
* A very naive priority queue implementation.
|
|
*/
|
|
PriorityQueue: {
|
|
make: function (opts) {
|
|
var T = dijkstra.PriorityQueue,
|
|
t = {},
|
|
key;
|
|
opts = opts || {};
|
|
for (key in T) {
|
|
if (T.hasOwnProperty(key)) {
|
|
t[key] = T[key];
|
|
}
|
|
}
|
|
t.queue = [];
|
|
t.sorter = opts.sorter || T.default_sorter;
|
|
return t;
|
|
},
|
|
|
|
default_sorter: function (a, b) {
|
|
return a.cost - b.cost;
|
|
},
|
|
|
|
/**
|
|
* Add a new item to the queue and ensure the highest priority element
|
|
* is at the front of the queue.
|
|
*/
|
|
push: function (value, cost) {
|
|
var item = {value: value, cost: cost};
|
|
this.queue.push(item);
|
|
this.queue.sort(this.sorter);
|
|
},
|
|
|
|
/**
|
|
* Return the highest priority element in the queue.
|
|
*/
|
|
pop: function () {
|
|
return this.queue.shift();
|
|
},
|
|
|
|
empty: function () {
|
|
return this.queue.length === 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// node.js module exports
|
|
{
|
|
module.exports = dijkstra;
|
|
}
|
|
} (dijkstra));
|
|
|
|
var dijkstraExports = dijkstra.exports;
|
|
|
|
(function (exports) {
|
|
const Mode = mode;
|
|
const NumericData = numericData;
|
|
const AlphanumericData = alphanumericData;
|
|
const ByteData = byteData;
|
|
const KanjiData = kanjiData;
|
|
const Regex = regex;
|
|
const Utils = utils$3;
|
|
const dijkstra = dijkstraExports;
|
|
|
|
/**
|
|
* Returns UTF8 byte length
|
|
*
|
|
* @param {String} str Input string
|
|
* @return {Number} Number of byte
|
|
*/
|
|
function getStringByteLength (str) {
|
|
return unescape(encodeURIComponent(str)).length
|
|
}
|
|
|
|
/**
|
|
* Get a list of segments of the specified mode
|
|
* from a string
|
|
*
|
|
* @param {Mode} mode Segment mode
|
|
* @param {String} str String to process
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function getSegments (regex, mode, str) {
|
|
const segments = [];
|
|
let result;
|
|
|
|
while ((result = regex.exec(str)) !== null) {
|
|
segments.push({
|
|
data: result[0],
|
|
index: result.index,
|
|
mode: mode,
|
|
length: result[0].length
|
|
});
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
/**
|
|
* Extracts a series of segments with the appropriate
|
|
* modes from a string
|
|
*
|
|
* @param {String} dataStr Input string
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function getSegmentsFromString (dataStr) {
|
|
const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr);
|
|
const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr);
|
|
let byteSegs;
|
|
let kanjiSegs;
|
|
|
|
if (Utils.isKanjiModeEnabled()) {
|
|
byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr);
|
|
kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr);
|
|
} else {
|
|
byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr);
|
|
kanjiSegs = [];
|
|
}
|
|
|
|
const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs);
|
|
|
|
return segs
|
|
.sort(function (s1, s2) {
|
|
return s1.index - s2.index
|
|
})
|
|
.map(function (obj) {
|
|
return {
|
|
data: obj.data,
|
|
mode: obj.mode,
|
|
length: obj.length
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns how many bits are needed to encode a string of
|
|
* specified length with the specified mode
|
|
*
|
|
* @param {Number} length String length
|
|
* @param {Mode} mode Segment mode
|
|
* @return {Number} Bit length
|
|
*/
|
|
function getSegmentBitsLength (length, mode) {
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return NumericData.getBitsLength(length)
|
|
case Mode.ALPHANUMERIC:
|
|
return AlphanumericData.getBitsLength(length)
|
|
case Mode.KANJI:
|
|
return KanjiData.getBitsLength(length)
|
|
case Mode.BYTE:
|
|
return ByteData.getBitsLength(length)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges adjacent segments which have the same mode
|
|
*
|
|
* @param {Array} segs Array of object with segments data
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function mergeSegments (segs) {
|
|
return segs.reduce(function (acc, curr) {
|
|
const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null;
|
|
if (prevSeg && prevSeg.mode === curr.mode) {
|
|
acc[acc.length - 1].data += curr.data;
|
|
return acc
|
|
}
|
|
|
|
acc.push(curr);
|
|
return acc
|
|
}, [])
|
|
}
|
|
|
|
/**
|
|
* Generates a list of all possible nodes combination which
|
|
* will be used to build a segments graph.
|
|
*
|
|
* Nodes are divided by groups. Each group will contain a list of all the modes
|
|
* in which is possible to encode the given text.
|
|
*
|
|
* For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte.
|
|
* The group for '12345' will contain then 3 objects, one for each
|
|
* possible encoding mode.
|
|
*
|
|
* Each node represents a possible segment.
|
|
*
|
|
* @param {Array} segs Array of object with segments data
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function buildNodes (segs) {
|
|
const nodes = [];
|
|
for (let i = 0; i < segs.length; i++) {
|
|
const seg = segs[i];
|
|
|
|
switch (seg.mode) {
|
|
case Mode.NUMERIC:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length },
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
]);
|
|
break
|
|
case Mode.ALPHANUMERIC:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
]);
|
|
break
|
|
case Mode.KANJI:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
]);
|
|
break
|
|
case Mode.BYTE:
|
|
nodes.push([
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
]);
|
|
}
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
/**
|
|
* Builds a graph from a list of nodes.
|
|
* All segments in each node group will be connected with all the segments of
|
|
* the next group and so on.
|
|
*
|
|
* At each connection will be assigned a weight depending on the
|
|
* segment's byte length.
|
|
*
|
|
* @param {Array} nodes Array of object with segments data
|
|
* @param {Number} version QR Code version
|
|
* @return {Object} Graph of all possible segments
|
|
*/
|
|
function buildGraph (nodes, version) {
|
|
const table = {};
|
|
const graph = { start: {} };
|
|
let prevNodeIds = ['start'];
|
|
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const nodeGroup = nodes[i];
|
|
const currentNodeIds = [];
|
|
|
|
for (let j = 0; j < nodeGroup.length; j++) {
|
|
const node = nodeGroup[j];
|
|
const key = '' + i + j;
|
|
|
|
currentNodeIds.push(key);
|
|
table[key] = { node: node, lastCount: 0 };
|
|
graph[key] = {};
|
|
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
const prevNodeId = prevNodeIds[n];
|
|
|
|
if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) {
|
|
graph[prevNodeId][key] =
|
|
getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) -
|
|
getSegmentBitsLength(table[prevNodeId].lastCount, node.mode);
|
|
|
|
table[prevNodeId].lastCount += node.length;
|
|
} else {
|
|
if (table[prevNodeId]) table[prevNodeId].lastCount = node.length;
|
|
|
|
graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) +
|
|
4 + Mode.getCharCountIndicator(node.mode, version); // switch cost
|
|
}
|
|
}
|
|
}
|
|
|
|
prevNodeIds = currentNodeIds;
|
|
}
|
|
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
graph[prevNodeIds[n]].end = 0;
|
|
}
|
|
|
|
return { map: graph, table: table }
|
|
}
|
|
|
|
/**
|
|
* Builds a segment from a specified data and mode.
|
|
* If a mode is not specified, the more suitable will be used.
|
|
*
|
|
* @param {String} data Input data
|
|
* @param {Mode | String} modesHint Data mode
|
|
* @return {Segment} Segment
|
|
*/
|
|
function buildSingleSegment (data, modesHint) {
|
|
let mode;
|
|
const bestMode = Mode.getBestModeForData(data);
|
|
|
|
mode = Mode.from(modesHint, bestMode);
|
|
|
|
// Make sure data can be encoded
|
|
if (mode !== Mode.BYTE && mode.bit < bestMode.bit) {
|
|
throw new Error('"' + data + '"' +
|
|
' cannot be encoded with mode ' + Mode.toString(mode) +
|
|
'.\n Suggested mode is: ' + Mode.toString(bestMode))
|
|
}
|
|
|
|
// Use Mode.BYTE if Kanji support is disabled
|
|
if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) {
|
|
mode = Mode.BYTE;
|
|
}
|
|
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return new NumericData(data)
|
|
|
|
case Mode.ALPHANUMERIC:
|
|
return new AlphanumericData(data)
|
|
|
|
case Mode.KANJI:
|
|
return new KanjiData(data)
|
|
|
|
case Mode.BYTE:
|
|
return new ByteData(data)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a list of segments from an array.
|
|
* Array can contain Strings or Objects with segment's info.
|
|
*
|
|
* For each item which is a string, will be generated a segment with the given
|
|
* string and the more appropriate encoding mode.
|
|
*
|
|
* For each item which is an object, will be generated a segment with the given
|
|
* data and mode.
|
|
* Objects must contain at least the property "data".
|
|
* If property "mode" is not present, the more suitable mode will be used.
|
|
*
|
|
* @param {Array} array Array of objects with segments data
|
|
* @return {Array} Array of Segments
|
|
*/
|
|
exports.fromArray = function fromArray (array) {
|
|
return array.reduce(function (acc, seg) {
|
|
if (typeof seg === 'string') {
|
|
acc.push(buildSingleSegment(seg, null));
|
|
} else if (seg.data) {
|
|
acc.push(buildSingleSegment(seg.data, seg.mode));
|
|
}
|
|
|
|
return acc
|
|
}, [])
|
|
};
|
|
|
|
/**
|
|
* Builds an optimized sequence of segments from a string,
|
|
* which will produce the shortest possible bitstream.
|
|
*
|
|
* @param {String} data Input string
|
|
* @param {Number} version QR Code version
|
|
* @return {Array} Array of segments
|
|
*/
|
|
exports.fromString = function fromString (data, version) {
|
|
const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled());
|
|
|
|
const nodes = buildNodes(segs);
|
|
const graph = buildGraph(nodes, version);
|
|
const path = dijkstra.find_path(graph.map, 'start', 'end');
|
|
|
|
const optimizedSegs = [];
|
|
for (let i = 1; i < path.length - 1; i++) {
|
|
optimizedSegs.push(graph.table[path[i]].node);
|
|
}
|
|
|
|
return exports.fromArray(mergeSegments(optimizedSegs))
|
|
};
|
|
|
|
/**
|
|
* Splits a string in various segments with the modes which
|
|
* best represent their content.
|
|
* The produced segments are far from being optimized.
|
|
* The output of this function is only used to estimate a QR Code version
|
|
* which may contain the data.
|
|
*
|
|
* @param {string} data Input string
|
|
* @return {Array} Array of segments
|
|
*/
|
|
exports.rawSplit = function rawSplit (data) {
|
|
return exports.fromArray(
|
|
getSegmentsFromString(data, Utils.isKanjiModeEnabled())
|
|
)
|
|
};
|
|
} (segments));
|
|
|
|
const Utils$1 = utils$3;
|
|
const ECLevel = errorCorrectionLevel;
|
|
const BitBuffer = bitBuffer;
|
|
const BitMatrix = bitMatrix;
|
|
const AlignmentPattern = alignmentPattern;
|
|
const FinderPattern = finderPattern;
|
|
const MaskPattern = maskPattern;
|
|
const ECCode = errorCorrectionCode;
|
|
const ReedSolomonEncoder = reedSolomonEncoder;
|
|
const Version = version;
|
|
const FormatInfo = formatInfo;
|
|
const Mode = mode;
|
|
const Segments = segments;
|
|
|
|
/**
|
|
* QRCode for JavaScript
|
|
*
|
|
* modified by Ryan Day for nodejs support
|
|
* Copyright (c) 2011 Ryan Day
|
|
*
|
|
* Licensed under the MIT license:
|
|
* http://www.opensource.org/licenses/mit-license.php
|
|
*
|
|
//---------------------------------------------------------------------
|
|
// QRCode for JavaScript
|
|
//
|
|
// Copyright (c) 2009 Kazuhiko Arase
|
|
//
|
|
// URL: http://www.d-project.com/
|
|
//
|
|
// Licensed under the MIT license:
|
|
// http://www.opensource.org/licenses/mit-license.php
|
|
//
|
|
// The word "QR Code" is registered trademark of
|
|
// DENSO WAVE INCORPORATED
|
|
// http://www.denso-wave.com/qrcode/faqpatent-e.html
|
|
//
|
|
//---------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Add finder patterns bits to matrix
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
* @param {Number} version QR Code version
|
|
*/
|
|
function setupFinderPattern (matrix, version) {
|
|
const size = matrix.size;
|
|
const pos = FinderPattern.getPositions(version);
|
|
|
|
for (let i = 0; i < pos.length; i++) {
|
|
const row = pos[i][0];
|
|
const col = pos[i][1];
|
|
|
|
for (let r = -1; r <= 7; r++) {
|
|
if (row + r <= -1 || size <= row + r) continue
|
|
|
|
for (let c = -1; c <= 7; c++) {
|
|
if (col + c <= -1 || size <= col + c) continue
|
|
|
|
if ((r >= 0 && r <= 6 && (c === 0 || c === 6)) ||
|
|
(c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
|
|
(r >= 2 && r <= 4 && c >= 2 && c <= 4)) {
|
|
matrix.set(row + r, col + c, true, true);
|
|
} else {
|
|
matrix.set(row + r, col + c, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add timing pattern bits to matrix
|
|
*
|
|
* Note: this function must be called before {@link setupAlignmentPattern}
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
*/
|
|
function setupTimingPattern (matrix) {
|
|
const size = matrix.size;
|
|
|
|
for (let r = 8; r < size - 8; r++) {
|
|
const value = r % 2 === 0;
|
|
matrix.set(r, 6, value, true);
|
|
matrix.set(6, r, value, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add alignment patterns bits to matrix
|
|
*
|
|
* Note: this function must be called after {@link setupTimingPattern}
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
* @param {Number} version QR Code version
|
|
*/
|
|
function setupAlignmentPattern (matrix, version) {
|
|
const pos = AlignmentPattern.getPositions(version);
|
|
|
|
for (let i = 0; i < pos.length; i++) {
|
|
const row = pos[i][0];
|
|
const col = pos[i][1];
|
|
|
|
for (let r = -2; r <= 2; r++) {
|
|
for (let c = -2; c <= 2; c++) {
|
|
if (r === -2 || r === 2 || c === -2 || c === 2 ||
|
|
(r === 0 && c === 0)) {
|
|
matrix.set(row + r, col + c, true, true);
|
|
} else {
|
|
matrix.set(row + r, col + c, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add version info bits to matrix
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
* @param {Number} version QR Code version
|
|
*/
|
|
function setupVersionInfo (matrix, version) {
|
|
const size = matrix.size;
|
|
const bits = Version.getEncodedBits(version);
|
|
let row, col, mod;
|
|
|
|
for (let i = 0; i < 18; i++) {
|
|
row = Math.floor(i / 3);
|
|
col = i % 3 + size - 8 - 3;
|
|
mod = ((bits >> i) & 1) === 1;
|
|
|
|
matrix.set(row, col, mod, true);
|
|
matrix.set(col, row, mod, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add format info bits to matrix
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
* @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
|
|
* @param {Number} maskPattern Mask pattern reference value
|
|
*/
|
|
function setupFormatInfo (matrix, errorCorrectionLevel, maskPattern) {
|
|
const size = matrix.size;
|
|
const bits = FormatInfo.getEncodedBits(errorCorrectionLevel, maskPattern);
|
|
let i, mod;
|
|
|
|
for (i = 0; i < 15; i++) {
|
|
mod = ((bits >> i) & 1) === 1;
|
|
|
|
// vertical
|
|
if (i < 6) {
|
|
matrix.set(i, 8, mod, true);
|
|
} else if (i < 8) {
|
|
matrix.set(i + 1, 8, mod, true);
|
|
} else {
|
|
matrix.set(size - 15 + i, 8, mod, true);
|
|
}
|
|
|
|
// horizontal
|
|
if (i < 8) {
|
|
matrix.set(8, size - i - 1, mod, true);
|
|
} else if (i < 9) {
|
|
matrix.set(8, 15 - i - 1 + 1, mod, true);
|
|
} else {
|
|
matrix.set(8, 15 - i - 1, mod, true);
|
|
}
|
|
}
|
|
|
|
// fixed module
|
|
matrix.set(size - 8, 8, 1, true);
|
|
}
|
|
|
|
/**
|
|
* Add encoded data bits to matrix
|
|
*
|
|
* @param {BitMatrix} matrix Modules matrix
|
|
* @param {Uint8Array} data Data codewords
|
|
*/
|
|
function setupData (matrix, data) {
|
|
const size = matrix.size;
|
|
let inc = -1;
|
|
let row = size - 1;
|
|
let bitIndex = 7;
|
|
let byteIndex = 0;
|
|
|
|
for (let col = size - 1; col > 0; col -= 2) {
|
|
if (col === 6) col--;
|
|
|
|
while (true) {
|
|
for (let c = 0; c < 2; c++) {
|
|
if (!matrix.isReserved(row, col - c)) {
|
|
let dark = false;
|
|
|
|
if (byteIndex < data.length) {
|
|
dark = (((data[byteIndex] >>> bitIndex) & 1) === 1);
|
|
}
|
|
|
|
matrix.set(row, col - c, dark);
|
|
bitIndex--;
|
|
|
|
if (bitIndex === -1) {
|
|
byteIndex++;
|
|
bitIndex = 7;
|
|
}
|
|
}
|
|
}
|
|
|
|
row += inc;
|
|
|
|
if (row < 0 || size <= row) {
|
|
row -= inc;
|
|
inc = -inc;
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create encoded codewords from data input
|
|
*
|
|
* @param {Number} version QR Code version
|
|
* @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
|
|
* @param {ByteData} data Data input
|
|
* @return {Uint8Array} Buffer containing encoded codewords
|
|
*/
|
|
function createData (version, errorCorrectionLevel, segments) {
|
|
// Prepare data buffer
|
|
const buffer = new BitBuffer();
|
|
|
|
segments.forEach(function (data) {
|
|
// prefix data with mode indicator (4 bits)
|
|
buffer.put(data.mode.bit, 4);
|
|
|
|
// Prefix data with character count indicator.
|
|
// The character count indicator is a string of bits that represents the
|
|
// number of characters that are being encoded.
|
|
// The character count indicator must be placed after the mode indicator
|
|
// and must be a certain number of bits long, depending on the QR version
|
|
// and data mode
|
|
// @see {@link Mode.getCharCountIndicator}.
|
|
buffer.put(data.getLength(), Mode.getCharCountIndicator(data.mode, version));
|
|
|
|
// add binary data sequence to buffer
|
|
data.write(buffer);
|
|
});
|
|
|
|
// Calculate required number of bits
|
|
const totalCodewords = Utils$1.getSymbolTotalCodewords(version);
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8;
|
|
|
|
// Add a terminator.
|
|
// If the bit string is shorter than the total number of required bits,
|
|
// a terminator of up to four 0s must be added to the right side of the string.
|
|
// If the bit string is more than four bits shorter than the required number of bits,
|
|
// add four 0s to the end.
|
|
if (buffer.getLengthInBits() + 4 <= dataTotalCodewordsBits) {
|
|
buffer.put(0, 4);
|
|
}
|
|
|
|
// If the bit string is fewer than four bits shorter, add only the number of 0s that
|
|
// are needed to reach the required number of bits.
|
|
|
|
// After adding the terminator, if the number of bits in the string is not a multiple of 8,
|
|
// pad the string on the right with 0s to make the string's length a multiple of 8.
|
|
while (buffer.getLengthInBits() % 8 !== 0) {
|
|
buffer.putBit(0);
|
|
}
|
|
|
|
// Add pad bytes if the string is still shorter than the total number of required bits.
|
|
// Extend the buffer to fill the data capacity of the symbol corresponding to
|
|
// the Version and Error Correction Level by adding the Pad Codewords 11101100 (0xEC)
|
|
// and 00010001 (0x11) alternately.
|
|
const remainingByte = (dataTotalCodewordsBits - buffer.getLengthInBits()) / 8;
|
|
for (let i = 0; i < remainingByte; i++) {
|
|
buffer.put(i % 2 ? 0x11 : 0xEC, 8);
|
|
}
|
|
|
|
return createCodewords(buffer, version, errorCorrectionLevel)
|
|
}
|
|
|
|
/**
|
|
* Encode input data with Reed-Solomon and return codewords with
|
|
* relative error correction bits
|
|
*
|
|
* @param {BitBuffer} bitBuffer Data to encode
|
|
* @param {Number} version QR Code version
|
|
* @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level
|
|
* @return {Uint8Array} Buffer containing encoded codewords
|
|
*/
|
|
function createCodewords (bitBuffer, version, errorCorrectionLevel) {
|
|
// Total codewords for this QR code version (Data + Error correction)
|
|
const totalCodewords = Utils$1.getSymbolTotalCodewords(version);
|
|
|
|
// Total number of error correction codewords
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
|
|
// Total number of data codewords
|
|
const dataTotalCodewords = totalCodewords - ecTotalCodewords;
|
|
|
|
// Total number of blocks
|
|
const ecTotalBlocks = ECCode.getBlocksCount(version, errorCorrectionLevel);
|
|
|
|
// Calculate how many blocks each group should contain
|
|
const blocksInGroup2 = totalCodewords % ecTotalBlocks;
|
|
const blocksInGroup1 = ecTotalBlocks - blocksInGroup2;
|
|
|
|
const totalCodewordsInGroup1 = Math.floor(totalCodewords / ecTotalBlocks);
|
|
|
|
const dataCodewordsInGroup1 = Math.floor(dataTotalCodewords / ecTotalBlocks);
|
|
const dataCodewordsInGroup2 = dataCodewordsInGroup1 + 1;
|
|
|
|
// Number of EC codewords is the same for both groups
|
|
const ecCount = totalCodewordsInGroup1 - dataCodewordsInGroup1;
|
|
|
|
// Initialize a Reed-Solomon encoder with a generator polynomial of degree ecCount
|
|
const rs = new ReedSolomonEncoder(ecCount);
|
|
|
|
let offset = 0;
|
|
const dcData = new Array(ecTotalBlocks);
|
|
const ecData = new Array(ecTotalBlocks);
|
|
let maxDataSize = 0;
|
|
const buffer = new Uint8Array(bitBuffer.buffer);
|
|
|
|
// Divide the buffer into the required number of blocks
|
|
for (let b = 0; b < ecTotalBlocks; b++) {
|
|
const dataSize = b < blocksInGroup1 ? dataCodewordsInGroup1 : dataCodewordsInGroup2;
|
|
|
|
// extract a block of data from buffer
|
|
dcData[b] = buffer.slice(offset, offset + dataSize);
|
|
|
|
// Calculate EC codewords for this data block
|
|
ecData[b] = rs.encode(dcData[b]);
|
|
|
|
offset += dataSize;
|
|
maxDataSize = Math.max(maxDataSize, dataSize);
|
|
}
|
|
|
|
// Create final data
|
|
// Interleave the data and error correction codewords from each block
|
|
const data = new Uint8Array(totalCodewords);
|
|
let index = 0;
|
|
let i, r;
|
|
|
|
// Add data codewords
|
|
for (i = 0; i < maxDataSize; i++) {
|
|
for (r = 0; r < ecTotalBlocks; r++) {
|
|
if (i < dcData[r].length) {
|
|
data[index++] = dcData[r][i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apped EC codewords
|
|
for (i = 0; i < ecCount; i++) {
|
|
for (r = 0; r < ecTotalBlocks; r++) {
|
|
data[index++] = ecData[r][i];
|
|
}
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Build QR Code symbol
|
|
*
|
|
* @param {String} data Input string
|
|
* @param {Number} version QR Code version
|
|
* @param {ErrorCorretionLevel} errorCorrectionLevel Error level
|
|
* @param {MaskPattern} maskPattern Mask pattern
|
|
* @return {Object} Object containing symbol data
|
|
*/
|
|
function createSymbol (data, version, errorCorrectionLevel, maskPattern) {
|
|
let segments;
|
|
|
|
if (Array.isArray(data)) {
|
|
segments = Segments.fromArray(data);
|
|
} else if (typeof data === 'string') {
|
|
let estimatedVersion = version;
|
|
|
|
if (!estimatedVersion) {
|
|
const rawSegments = Segments.rawSplit(data);
|
|
|
|
// Estimate best version that can contain raw splitted segments
|
|
estimatedVersion = Version.getBestVersionForData(rawSegments, errorCorrectionLevel);
|
|
}
|
|
|
|
// Build optimized segments
|
|
// If estimated version is undefined, try with the highest version
|
|
segments = Segments.fromString(data, estimatedVersion || 40);
|
|
} else {
|
|
throw new Error('Invalid data')
|
|
}
|
|
|
|
// Get the min version that can contain data
|
|
const bestVersion = Version.getBestVersionForData(segments, errorCorrectionLevel);
|
|
|
|
// If no version is found, data cannot be stored
|
|
if (!bestVersion) {
|
|
throw new Error('The amount of data is too big to be stored in a QR Code')
|
|
}
|
|
|
|
// If not specified, use min version as default
|
|
if (!version) {
|
|
version = bestVersion;
|
|
|
|
// Check if the specified version can contain the data
|
|
} else if (version < bestVersion) {
|
|
throw new Error('\n' +
|
|
'The chosen QR Code version cannot contain this amount of data.\n' +
|
|
'Minimum version required to store current data is: ' + bestVersion + '.\n'
|
|
)
|
|
}
|
|
|
|
const dataBits = createData(version, errorCorrectionLevel, segments);
|
|
|
|
// Allocate matrix buffer
|
|
const moduleCount = Utils$1.getSymbolSize(version);
|
|
const modules = new BitMatrix(moduleCount);
|
|
|
|
// Add function modules
|
|
setupFinderPattern(modules, version);
|
|
setupTimingPattern(modules);
|
|
setupAlignmentPattern(modules, version);
|
|
|
|
// Add temporary dummy bits for format info just to set them as reserved.
|
|
// This is needed to prevent these bits from being masked by {@link MaskPattern.applyMask}
|
|
// since the masking operation must be performed only on the encoding region.
|
|
// These blocks will be replaced with correct values later in code.
|
|
setupFormatInfo(modules, errorCorrectionLevel, 0);
|
|
|
|
if (version >= 7) {
|
|
setupVersionInfo(modules, version);
|
|
}
|
|
|
|
// Add data codewords
|
|
setupData(modules, dataBits);
|
|
|
|
if (isNaN(maskPattern)) {
|
|
// Find best mask pattern
|
|
maskPattern = MaskPattern.getBestMask(modules,
|
|
setupFormatInfo.bind(null, modules, errorCorrectionLevel));
|
|
}
|
|
|
|
// Apply mask pattern
|
|
MaskPattern.applyMask(maskPattern, modules);
|
|
|
|
// Replace format info bits with correct values
|
|
setupFormatInfo(modules, errorCorrectionLevel, maskPattern);
|
|
|
|
return {
|
|
modules: modules,
|
|
version: version,
|
|
errorCorrectionLevel: errorCorrectionLevel,
|
|
maskPattern: maskPattern,
|
|
segments: segments
|
|
}
|
|
}
|
|
|
|
/**
|
|
* QR Code
|
|
*
|
|
* @param {String | Array} data Input data
|
|
* @param {Object} options Optional configurations
|
|
* @param {Number} options.version QR Code version
|
|
* @param {String} options.errorCorrectionLevel Error correction level
|
|
* @param {Function} options.toSJISFunc Helper func to convert utf8 to sjis
|
|
*/
|
|
qrcode.create = function create (data, options) {
|
|
if (typeof data === 'undefined' || data === '') {
|
|
throw new Error('No input text')
|
|
}
|
|
|
|
let errorCorrectionLevel = ECLevel.M;
|
|
let version;
|
|
let mask;
|
|
|
|
if (typeof options !== 'undefined') {
|
|
// Use higher error correction level as default
|
|
errorCorrectionLevel = ECLevel.from(options.errorCorrectionLevel, ECLevel.M);
|
|
version = Version.from(options.version);
|
|
mask = MaskPattern.from(options.maskPattern);
|
|
|
|
if (options.toSJISFunc) {
|
|
Utils$1.setToSJISFunction(options.toSJISFunc);
|
|
}
|
|
}
|
|
|
|
return createSymbol(data, version, errorCorrectionLevel, mask)
|
|
};
|
|
|
|
var canvas = {};
|
|
|
|
var utils$2 = {};
|
|
|
|
(function (exports) {
|
|
function hex2rgba (hex) {
|
|
if (typeof hex === 'number') {
|
|
hex = hex.toString();
|
|
}
|
|
|
|
if (typeof hex !== 'string') {
|
|
throw new Error('Color should be defined as hex string')
|
|
}
|
|
|
|
let hexCode = hex.slice().replace('#', '').split('');
|
|
if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) {
|
|
throw new Error('Invalid hex color: ' + hex)
|
|
}
|
|
|
|
// Convert from short to long form (fff -> ffffff)
|
|
if (hexCode.length === 3 || hexCode.length === 4) {
|
|
hexCode = Array.prototype.concat.apply([], hexCode.map(function (c) {
|
|
return [c, c]
|
|
}));
|
|
}
|
|
|
|
// Add default alpha value
|
|
if (hexCode.length === 6) hexCode.push('F', 'F');
|
|
|
|
const hexValue = parseInt(hexCode.join(''), 16);
|
|
|
|
return {
|
|
r: (hexValue >> 24) & 255,
|
|
g: (hexValue >> 16) & 255,
|
|
b: (hexValue >> 8) & 255,
|
|
a: hexValue & 255,
|
|
hex: '#' + hexCode.slice(0, 6).join('')
|
|
}
|
|
}
|
|
|
|
exports.getOptions = function getOptions (options) {
|
|
if (!options) options = {};
|
|
if (!options.color) options.color = {};
|
|
|
|
const margin = typeof options.margin === 'undefined' ||
|
|
options.margin === null ||
|
|
options.margin < 0
|
|
? 4
|
|
: options.margin;
|
|
|
|
const width = options.width && options.width >= 21 ? options.width : undefined;
|
|
const scale = options.scale || 4;
|
|
|
|
return {
|
|
width: width,
|
|
scale: width ? 4 : scale,
|
|
margin: margin,
|
|
color: {
|
|
dark: hex2rgba(options.color.dark || '#000000ff'),
|
|
light: hex2rgba(options.color.light || '#ffffffff')
|
|
},
|
|
type: options.type,
|
|
rendererOpts: options.rendererOpts || {}
|
|
}
|
|
};
|
|
|
|
exports.getScale = function getScale (qrSize, opts) {
|
|
return opts.width && opts.width >= qrSize + opts.margin * 2
|
|
? opts.width / (qrSize + opts.margin * 2)
|
|
: opts.scale
|
|
};
|
|
|
|
exports.getImageWidth = function getImageWidth (qrSize, opts) {
|
|
const scale = exports.getScale(qrSize, opts);
|
|
return Math.floor((qrSize + opts.margin * 2) * scale)
|
|
};
|
|
|
|
exports.qrToImageData = function qrToImageData (imgData, qr, opts) {
|
|
const size = qr.modules.size;
|
|
const data = qr.modules.data;
|
|
const scale = exports.getScale(size, opts);
|
|
const symbolSize = Math.floor((size + opts.margin * 2) * scale);
|
|
const scaledMargin = opts.margin * scale;
|
|
const palette = [opts.color.light, opts.color.dark];
|
|
|
|
for (let i = 0; i < symbolSize; i++) {
|
|
for (let j = 0; j < symbolSize; j++) {
|
|
let posDst = (i * symbolSize + j) * 4;
|
|
let pxColor = opts.color.light;
|
|
|
|
if (i >= scaledMargin && j >= scaledMargin &&
|
|
i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) {
|
|
const iSrc = Math.floor((i - scaledMargin) / scale);
|
|
const jSrc = Math.floor((j - scaledMargin) / scale);
|
|
pxColor = palette[data[iSrc * size + jSrc] ? 1 : 0];
|
|
}
|
|
|
|
imgData[posDst++] = pxColor.r;
|
|
imgData[posDst++] = pxColor.g;
|
|
imgData[posDst++] = pxColor.b;
|
|
imgData[posDst] = pxColor.a;
|
|
}
|
|
}
|
|
};
|
|
} (utils$2));
|
|
|
|
(function (exports) {
|
|
const Utils = utils$2;
|
|
|
|
function clearCanvas (ctx, canvas, size) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
if (!canvas.style) canvas.style = {};
|
|
canvas.height = size;
|
|
canvas.width = size;
|
|
canvas.style.height = size + 'px';
|
|
canvas.style.width = size + 'px';
|
|
}
|
|
|
|
function getCanvasElement () {
|
|
try {
|
|
return document.createElement('canvas')
|
|
} catch (e) {
|
|
throw new Error('You need to specify a canvas element')
|
|
}
|
|
}
|
|
|
|
exports.render = function render (qrData, canvas, options) {
|
|
let opts = options;
|
|
let canvasEl = canvas;
|
|
|
|
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
|
|
opts = canvas;
|
|
canvas = undefined;
|
|
}
|
|
|
|
if (!canvas) {
|
|
canvasEl = getCanvasElement();
|
|
}
|
|
|
|
opts = Utils.getOptions(opts);
|
|
const size = Utils.getImageWidth(qrData.modules.size, opts);
|
|
|
|
const ctx = canvasEl.getContext('2d');
|
|
const image = ctx.createImageData(size, size);
|
|
Utils.qrToImageData(image.data, qrData, opts);
|
|
|
|
clearCanvas(ctx, canvasEl, size);
|
|
ctx.putImageData(image, 0, 0);
|
|
|
|
return canvasEl
|
|
};
|
|
|
|
exports.renderToDataURL = function renderToDataURL (qrData, canvas, options) {
|
|
let opts = options;
|
|
|
|
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
|
|
opts = canvas;
|
|
canvas = undefined;
|
|
}
|
|
|
|
if (!opts) opts = {};
|
|
|
|
const canvasEl = exports.render(qrData, canvas, opts);
|
|
|
|
const type = opts.type || 'image/png';
|
|
const rendererOpts = opts.rendererOpts || {};
|
|
|
|
return canvasEl.toDataURL(type, rendererOpts.quality)
|
|
};
|
|
} (canvas));
|
|
|
|
var svgTag = {};
|
|
|
|
const Utils = utils$2;
|
|
|
|
function getColorAttrib (color, attrib) {
|
|
const alpha = color.a / 255;
|
|
const str = attrib + '="' + color.hex + '"';
|
|
|
|
return alpha < 1
|
|
? str + ' ' + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"'
|
|
: str
|
|
}
|
|
|
|
function svgCmd (cmd, x, y) {
|
|
let str = cmd + x;
|
|
if (typeof y !== 'undefined') str += ' ' + y;
|
|
|
|
return str
|
|
}
|
|
|
|
function qrToPath (data, size, margin) {
|
|
let path = '';
|
|
let moveBy = 0;
|
|
let newRow = false;
|
|
let lineLength = 0;
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const col = Math.floor(i % size);
|
|
const row = Math.floor(i / size);
|
|
|
|
if (!col && !newRow) newRow = true;
|
|
|
|
if (data[i]) {
|
|
lineLength++;
|
|
|
|
if (!(i > 0 && col > 0 && data[i - 1])) {
|
|
path += newRow
|
|
? svgCmd('M', col + margin, 0.5 + row + margin)
|
|
: svgCmd('m', moveBy, 0);
|
|
|
|
moveBy = 0;
|
|
newRow = false;
|
|
}
|
|
|
|
if (!(col + 1 < size && data[i + 1])) {
|
|
path += svgCmd('h', lineLength);
|
|
lineLength = 0;
|
|
}
|
|
} else {
|
|
moveBy++;
|
|
}
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
svgTag.render = function render (qrData, options, cb) {
|
|
const opts = Utils.getOptions(options);
|
|
const size = qrData.modules.size;
|
|
const data = qrData.modules.data;
|
|
const qrcodesize = size + opts.margin * 2;
|
|
|
|
const bg = !opts.color.light.a
|
|
? ''
|
|
: '<path ' + getColorAttrib(opts.color.light, 'fill') +
|
|
' d="M0 0h' + qrcodesize + 'v' + qrcodesize + 'H0z"/>';
|
|
|
|
const path =
|
|
'<path ' + getColorAttrib(opts.color.dark, 'stroke') +
|
|
' d="' + qrToPath(data, size, opts.margin) + '"/>';
|
|
|
|
const viewBox = 'viewBox="' + '0 0 ' + qrcodesize + ' ' + qrcodesize + '"';
|
|
|
|
const width = !opts.width ? '' : 'width="' + opts.width + '" height="' + opts.width + '" ';
|
|
|
|
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path + '</svg>\n';
|
|
|
|
if (typeof cb === 'function') {
|
|
cb(null, svgTag);
|
|
}
|
|
|
|
return svgTag
|
|
};
|
|
|
|
const canPromise = canPromise$1;
|
|
|
|
const QRCode = qrcode;
|
|
const CanvasRenderer = canvas;
|
|
const SvgRenderer = svgTag;
|
|
|
|
function renderCanvas (renderFunc, canvas, text, opts, cb) {
|
|
const args = [].slice.call(arguments, 1);
|
|
const argsNum = args.length;
|
|
const isLastArgCb = typeof args[argsNum - 1] === 'function';
|
|
|
|
if (!isLastArgCb && !canPromise()) {
|
|
throw new Error('Callback required as last argument')
|
|
}
|
|
|
|
if (isLastArgCb) {
|
|
if (argsNum < 2) {
|
|
throw new Error('Too few arguments provided')
|
|
}
|
|
|
|
if (argsNum === 2) {
|
|
cb = text;
|
|
text = canvas;
|
|
canvas = opts = undefined;
|
|
} else if (argsNum === 3) {
|
|
if (canvas.getContext && typeof cb === 'undefined') {
|
|
cb = opts;
|
|
opts = undefined;
|
|
} else {
|
|
cb = opts;
|
|
opts = text;
|
|
text = canvas;
|
|
canvas = undefined;
|
|
}
|
|
}
|
|
} else {
|
|
if (argsNum < 1) {
|
|
throw new Error('Too few arguments provided')
|
|
}
|
|
|
|
if (argsNum === 1) {
|
|
text = canvas;
|
|
canvas = opts = undefined;
|
|
} else if (argsNum === 2 && !canvas.getContext) {
|
|
opts = text;
|
|
text = canvas;
|
|
canvas = undefined;
|
|
}
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
try {
|
|
const data = QRCode.create(text, opts);
|
|
resolve(renderFunc(data, canvas, opts));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
})
|
|
}
|
|
|
|
try {
|
|
const data = QRCode.create(text, opts);
|
|
cb(null, renderFunc(data, canvas, opts));
|
|
} catch (e) {
|
|
cb(e);
|
|
}
|
|
}
|
|
|
|
QRCode.create;
|
|
renderCanvas.bind(null, CanvasRenderer.render);
|
|
renderCanvas.bind(null, CanvasRenderer.renderToDataURL);
|
|
|
|
// only svg for now.
|
|
renderCanvas.bind(null, function (data, _, opts) {
|
|
return SvgRenderer.render(data, opts)
|
|
});
|
|
|
|
function generateEmojiList() {
|
|
const emojiRanges = [
|
|
[128512, 128591],
|
|
[127744, 128511],
|
|
[128640, 128767],
|
|
[128768, 128895]
|
|
];
|
|
const emojiList = [];
|
|
for (const range of emojiRanges) {
|
|
const [start, end] = range;
|
|
for (let i = start; i <= end && emojiList.length < 256; i++) {
|
|
emojiList.push(String.fromCodePoint(i));
|
|
}
|
|
if (emojiList.length >= 256) {
|
|
break;
|
|
}
|
|
}
|
|
return emojiList.slice(0, 256);
|
|
}
|
|
async function addressToEmoji(text) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(text);
|
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
const hash = new Uint8Array(hashBuffer);
|
|
const bytes = hash.slice(-4);
|
|
const emojiList = generateEmojiList();
|
|
const emojis = Array.from(bytes).map((byte) => emojiList[byte]).join("");
|
|
return emojis;
|
|
}
|
|
async function emojisPairingRequest() {
|
|
try {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const sp_adress = urlParams.get("sp_address");
|
|
if (!sp_adress) {
|
|
return;
|
|
}
|
|
const emojis = await addressToEmoji(sp_adress);
|
|
const emojiDisplay = container?.querySelector(".pairing-request");
|
|
if (emojiDisplay) {
|
|
emojiDisplay.textContent = "(Request from: " + emojis + ")";
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
async function displayEmojis(text) {
|
|
console.log("🚀 ~ Services ~ adressToEmoji");
|
|
try {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const emojis = await addressToEmoji(text);
|
|
const emojiDisplay = container?.querySelector(".emoji-display");
|
|
if (emojiDisplay) {
|
|
emojiDisplay.textContent = emojis;
|
|
}
|
|
emojisPairingRequest();
|
|
initAddressInput();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
function initAddressInput() {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const addressInput = container.querySelector("#addressInput");
|
|
const emojiDisplay = container.querySelector("#emoji-display-2");
|
|
const okButton = container.querySelector("#okButton");
|
|
const createButton = container.querySelector("#createButton");
|
|
container.querySelector("#actionButton");
|
|
addSubscription(addressInput, "input", async () => {
|
|
let address = addressInput.value;
|
|
try {
|
|
const url = new URL(address);
|
|
const urlParams = new URLSearchParams(url.search);
|
|
const extractedAddress = urlParams.get("sp_address") || "";
|
|
if (extractedAddress) {
|
|
address = extractedAddress;
|
|
addressInput.value = address;
|
|
}
|
|
} catch (e) {
|
|
console.log("Ce n'est pas une URL valide, on garde l'adresse originale.");
|
|
}
|
|
if (address) {
|
|
const emojis = await addressToEmoji(address);
|
|
if (emojiDisplay) {
|
|
emojiDisplay.innerHTML = emojis;
|
|
}
|
|
if (okButton) {
|
|
okButton.style.display = "inline-block";
|
|
}
|
|
} else {
|
|
if (emojiDisplay) {
|
|
emojiDisplay.innerHTML = "";
|
|
}
|
|
if (okButton) {
|
|
okButton.style.display = "none";
|
|
}
|
|
}
|
|
});
|
|
if (createButton) {
|
|
addSubscription(createButton, "click", () => {
|
|
onCreateButtonClick();
|
|
});
|
|
}
|
|
}
|
|
async function onCreateButtonClick() {
|
|
try {
|
|
await prepareAndSendPairingTx$1();
|
|
const service = await Services.getInstance();
|
|
await service.confirmPairing();
|
|
} catch (e) {
|
|
console.error(`onCreateButtonClick error: ${e}`);
|
|
}
|
|
}
|
|
async function prepareAndSendPairingTx$1() {
|
|
const service = await Services.getInstance();
|
|
try {
|
|
await service.checkConnections([]);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
try {
|
|
const relayAddress = service.getAllRelays();
|
|
const createPairingProcessReturn = await service.createPairingProcess("", []);
|
|
if (!createPairingProcessReturn.updated_process) {
|
|
throw new Error("createPairingProcess returned an empty new process");
|
|
}
|
|
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
|
|
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
|
|
await service.handleApiReturn(createPairingProcessReturn);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
async function generateCreateBtn() {
|
|
try {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const createBtn = container?.querySelector(".create-btn");
|
|
if (createBtn) {
|
|
createBtn.textContent = "CREATE";
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
async function validate() {
|
|
console.log("==> VALIDATE");
|
|
const modalservice = await ModalService.getInstance();
|
|
modalservice.closeValidationModal();
|
|
}
|
|
async function initValidationModal(processDiffs) {
|
|
console.log("🚀 ~ initValidationModal ~ processDiffs:", processDiffs);
|
|
for (const diff of processDiffs.diffs) {
|
|
let diffs = "";
|
|
for (const value of diff) {
|
|
diffs += `
|
|
<div class="radio-buttons">
|
|
<label>
|
|
<input type="radio" name="validation1" value="old" />
|
|
Keep Old
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="validation1" value="new" />
|
|
Keep New
|
|
</label>
|
|
</div>
|
|
<div class="diff">
|
|
<div class="diff-side diff-old">
|
|
<pre>-${value.previous_value}</pre>
|
|
</div>
|
|
<div class="diff-side diff-new">
|
|
<pre>+${value.new_value}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
const state = `
|
|
<div class="expansion-panel">
|
|
<div class="expansion-panel-header">State ${diff[0].new_state_merkle_root}</div>
|
|
<div class="expansion-panel-body">
|
|
${diffs}
|
|
</div>
|
|
</div>
|
|
`;
|
|
const box = document.querySelector(".validation-box");
|
|
if (box)
|
|
box.innerHTML += state;
|
|
}
|
|
document.querySelectorAll(".expansion-panel-header").forEach((header) => {
|
|
header.addEventListener("click", function(event) {
|
|
const target = event.target;
|
|
const body = target.nextElementSibling;
|
|
if (body?.style)
|
|
body.style.display = body.style.display === "block" ? "none" : "block";
|
|
});
|
|
});
|
|
}
|
|
window.validate = validate;
|
|
|
|
function interpolate(template, data) {
|
|
return template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()]);
|
|
}
|
|
|
|
let ModalService$1 = class ModalService {
|
|
static instance;
|
|
stateId = null;
|
|
processId = null;
|
|
constructor() {
|
|
}
|
|
paired_addresses = [];
|
|
modal = null;
|
|
// Method to access the singleton instance of Services
|
|
static async getInstance() {
|
|
if (!ModalService.instance) {
|
|
ModalService.instance = new ModalService();
|
|
}
|
|
return ModalService.instance;
|
|
}
|
|
openLoginModal(myAddress, receiverAddress) {
|
|
const container = document.querySelector(".page-container");
|
|
let html = modalHtml;
|
|
html = html.replace("{{device1}}", myAddress);
|
|
html = html.replace("{{device2}}", receiverAddress);
|
|
if (container)
|
|
container.innerHTML += html;
|
|
const modal = document.getElementById("login-modal");
|
|
if (modal)
|
|
modal.style.display = "flex";
|
|
const newScript = document.createElement("script");
|
|
newScript.setAttribute("type", "module");
|
|
newScript.textContent = modalScript;
|
|
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
|
|
}
|
|
async injectModal(members) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/confirmation-modal.html").then((res) => res.text());
|
|
html = html.replace("{{device1}}", await addressToEmoji(members[0]["sp_addresses"][0]));
|
|
html = html.replace("{{device2}}", await addressToEmoji(members[0]["sp_addresses"][1]));
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.src = "/src/components/modal/confirmation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
async injectCreationModal(members) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/creation-modal.html").then((res) => res.text());
|
|
html = html.replace("{{device1}}", await addressToEmoji(members[0]["sp_addresses"][0]));
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.src = "/src/components/modal/confirmation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
// Device 1 wait Device 2
|
|
async injectWaitingModal() {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/waiting-modal.html").then((res) => res.text());
|
|
container.innerHTML += html;
|
|
}
|
|
}
|
|
async injectValidationModal(processDiff) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/validation-modal/validation-modal.html").then((res) => res.text());
|
|
html = interpolate(html, { processId: processDiff.processId });
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.id = "validation-modal-script";
|
|
script.src = "/src/components/validation-modal/validation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
const css = document.createElement("style");
|
|
css.id = "validation-modal-css";
|
|
css.innerText = validationModalStyle;
|
|
document.head.appendChild(css);
|
|
initValidationModal(processDiff);
|
|
}
|
|
}
|
|
async closeValidationModal() {
|
|
const script = document.querySelector("#validation-modal-script");
|
|
const css = document.querySelector("#validation-modal-css");
|
|
const component = document.querySelector("#validation-modal");
|
|
script?.remove();
|
|
css?.remove();
|
|
component?.remove();
|
|
}
|
|
async openPairingConfirmationModal(roleDefinition, processId, stateId) {
|
|
let memberOutPoints;
|
|
if (roleDefinition["pairing"]) {
|
|
const owner = roleDefinition["pairing"];
|
|
memberOutPoints = owner.members;
|
|
} else {
|
|
throw new Error('No "pairing" role');
|
|
}
|
|
if (memberOutPoints.length != 1) {
|
|
throw new Error("Must have exactly 1 member");
|
|
}
|
|
console.log("MEMBER OUTPOINTS:", memberOutPoints);
|
|
const service = await Services.getInstance();
|
|
const localAddress = service.getDeviceAddress();
|
|
const members = [];
|
|
for (const outPoint of memberOutPoints) {
|
|
const member = { sp_addresses: [] };
|
|
members.push(member);
|
|
}
|
|
for (const member of members) {
|
|
if (member.sp_addresses) {
|
|
for (const address of member.sp_addresses) {
|
|
if (address !== localAddress) {
|
|
this.paired_addresses.push(address);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.processId = processId;
|
|
this.stateId = stateId;
|
|
if (members[0].sp_addresses.length === 1) {
|
|
await this.injectCreationModal(members);
|
|
this.modal = document.getElementById("creation-modal");
|
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
|
} else {
|
|
await this.injectModal(members);
|
|
this.modal = document.getElementById("modal");
|
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
|
}
|
|
if (this.modal)
|
|
this.modal.style.display = "flex";
|
|
window.onclick = (event) => {
|
|
if (event.target === this.modal) {
|
|
this.closeConfirmationModal();
|
|
}
|
|
};
|
|
}
|
|
confirmLogin() {
|
|
console.log("=============> Confirm Login");
|
|
}
|
|
async closeLoginModal() {
|
|
if (this.modal)
|
|
this.modal.style.display = "none";
|
|
}
|
|
async showConfirmationModal(options, fullscreen = false) {
|
|
const modalElement = document.createElement("div");
|
|
modalElement.id = "confirmation-modal";
|
|
modalElement.innerHTML = `
|
|
<div class="modal-overlay">
|
|
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ""}>
|
|
<h2>${options.title}</h2>
|
|
<div class="modal-body">
|
|
${options.content}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || "Annuler"}</button>
|
|
<button id="confirm-button" class="btn btn-primary">${options.confirmText || "Confirmer"}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modalElement);
|
|
return new Promise((resolve) => {
|
|
const confirmButton = modalElement.querySelector("#confirm-button");
|
|
const cancelButton = modalElement.querySelector("#cancel-button");
|
|
const modalOverlay = modalElement.querySelector(".modal-overlay");
|
|
const cleanup = () => {
|
|
modalElement.remove();
|
|
};
|
|
confirmButton?.addEventListener("click", () => {
|
|
cleanup();
|
|
resolve(true);
|
|
});
|
|
cancelButton?.addEventListener("click", () => {
|
|
cleanup();
|
|
resolve(false);
|
|
});
|
|
modalOverlay?.addEventListener("click", (e) => {
|
|
if (e.target === modalOverlay) {
|
|
cleanup();
|
|
resolve(false);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async closeConfirmationModal() {
|
|
const service = await Services.getInstance();
|
|
await service.unpairDevice();
|
|
if (this.modal)
|
|
this.modal.style.display = "none";
|
|
}
|
|
};
|
|
|
|
class Database {
|
|
static instance;
|
|
db = null;
|
|
dbName = "4nk";
|
|
dbVersion = 1;
|
|
serviceWorkerRegistration = null;
|
|
messageChannel = null;
|
|
messageChannelForGet = null;
|
|
serviceWorkerCheckIntervalId = null;
|
|
storeDefinitions = {
|
|
AnkLabels: {
|
|
name: "labels",
|
|
options: { keyPath: "emoji" },
|
|
indices: []
|
|
},
|
|
AnkWallet: {
|
|
name: "wallet",
|
|
options: { keyPath: "pre_id" },
|
|
indices: []
|
|
},
|
|
AnkProcess: {
|
|
name: "processes",
|
|
options: {},
|
|
indices: []
|
|
},
|
|
AnkSharedSecrets: {
|
|
name: "shared_secrets",
|
|
options: {},
|
|
indices: []
|
|
},
|
|
AnkUnconfirmedSecrets: {
|
|
name: "unconfirmed_secrets",
|
|
options: { autoIncrement: true },
|
|
indices: []
|
|
},
|
|
AnkPendingDiffs: {
|
|
name: "diffs",
|
|
options: { keyPath: "value_commitment" },
|
|
indices: [
|
|
{ name: "byStateId", keyPath: "state_id", options: { unique: false } },
|
|
{ name: "byNeedValidation", keyPath: "need_validation", options: { unique: false } },
|
|
{ name: "byStatus", keyPath: "validation_status", options: { unique: false } }
|
|
]
|
|
},
|
|
AnkData: {
|
|
name: "data",
|
|
options: {},
|
|
indices: []
|
|
}
|
|
};
|
|
// Private constructor to prevent direct instantiation from outside
|
|
constructor() {
|
|
}
|
|
// Method to access the singleton instance of Database
|
|
static async getInstance() {
|
|
if (!Database.instance) {
|
|
Database.instance = new Database();
|
|
await Database.instance.init();
|
|
}
|
|
return Database.instance;
|
|
}
|
|
// Initialize the database
|
|
async init() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
|
|
if (!db.objectStoreNames.contains(name)) {
|
|
let store = db.createObjectStore(name, options);
|
|
indices.forEach(({ name: name2, keyPath, options: options2 }) => {
|
|
store.createIndex(name2, keyPath, options2);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
request.onsuccess = async () => {
|
|
this.db = request.result;
|
|
resolve();
|
|
};
|
|
request.onerror = () => {
|
|
console.error("Database error:", request.error);
|
|
reject(request.error);
|
|
};
|
|
});
|
|
}
|
|
async getDb() {
|
|
if (!this.db) {
|
|
await this.init();
|
|
}
|
|
return this.db;
|
|
}
|
|
getStoreList() {
|
|
const objectList = {};
|
|
Object.keys(this.storeDefinitions).forEach((key) => {
|
|
objectList[key] = this.storeDefinitions[key].name;
|
|
});
|
|
return objectList;
|
|
}
|
|
async registerServiceWorker(path) {
|
|
if (!("serviceWorker" in navigator))
|
|
return;
|
|
console.log("registering worker at", path);
|
|
try {
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
if (registrations.length === 0) {
|
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: "module" });
|
|
console.log("Service Worker registered with scope:", this.serviceWorkerRegistration.scope);
|
|
} else if (registrations.length === 1) {
|
|
this.serviceWorkerRegistration = registrations[0];
|
|
await this.serviceWorkerRegistration.update();
|
|
console.log("Service Worker updated");
|
|
} else {
|
|
console.log("Multiple Service Worker(s) detected. Unregistering all...");
|
|
await Promise.all(registrations.map((reg) => reg.unregister()));
|
|
console.log("All previous Service Workers unregistered.");
|
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: "module" });
|
|
console.log("Service Worker registered with scope:", this.serviceWorkerRegistration.scope);
|
|
}
|
|
await this.checkForUpdates();
|
|
navigator.serviceWorker.addEventListener("message", async (event) => {
|
|
console.log("Received message from service worker:", event.data);
|
|
await this.handleServiceWorkerMessage(event.data);
|
|
});
|
|
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
|
const activeWorker = this.serviceWorkerRegistration?.active || await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
|
const service = await Services.getInstance();
|
|
const payload = await service.getMyProcesses();
|
|
if (payload && payload.length != 0) {
|
|
activeWorker?.postMessage({ type: "SCAN", payload });
|
|
}
|
|
}, 5e3);
|
|
} catch (error) {
|
|
console.error("Service Worker registration failed:", error);
|
|
}
|
|
}
|
|
// Helper function to wait for service worker activation
|
|
async waitForServiceWorkerActivation(registration) {
|
|
return new Promise((resolve) => {
|
|
if (registration.active) {
|
|
resolve(registration.active);
|
|
} else {
|
|
const listener = () => {
|
|
if (registration.active) {
|
|
navigator.serviceWorker.removeEventListener("controllerchange", listener);
|
|
resolve(registration.active);
|
|
}
|
|
};
|
|
navigator.serviceWorker.addEventListener("controllerchange", listener);
|
|
}
|
|
});
|
|
}
|
|
async checkForUpdates() {
|
|
if (this.serviceWorkerRegistration) {
|
|
try {
|
|
await this.serviceWorkerRegistration.update();
|
|
if (this.serviceWorkerRegistration.waiting) {
|
|
this.serviceWorkerRegistration.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking for service worker updates:", error);
|
|
}
|
|
}
|
|
}
|
|
async handleServiceWorkerMessage(message) {
|
|
switch (message.type) {
|
|
case "TO_DOWNLOAD":
|
|
await this.handleDownloadList(message.data);
|
|
break;
|
|
default:
|
|
console.warn("Unknown message type received from service worker:", message);
|
|
}
|
|
}
|
|
async handleDownloadList(downloadList) {
|
|
let requestedStateId = [];
|
|
const service = await Services.getInstance();
|
|
for (const hash of downloadList) {
|
|
const diff = await service.getDiffByValue(hash);
|
|
if (!diff) {
|
|
console.warn(`Missing a diff for hash ${hash}`);
|
|
continue;
|
|
}
|
|
const processId = diff.process_id;
|
|
const stateId = diff.state_id;
|
|
const roles = diff.roles;
|
|
try {
|
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
|
if (valueBytes) {
|
|
const blob = new Blob([valueBytes], { type: "application/octet-stream" });
|
|
await service.saveBlobToDb(hash, blob);
|
|
document.dispatchEvent(new CustomEvent("newDataReceived", {
|
|
detail: {
|
|
processId,
|
|
stateId,
|
|
hash
|
|
}
|
|
}));
|
|
} else {
|
|
console.log("Request data from managers of the process");
|
|
if (!requestedStateId.includes(stateId)) {
|
|
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
|
requestedStateId.push(stateId);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
handleAddObjectResponse = async (event) => {
|
|
const data = event.data;
|
|
console.log("Received response from service worker (ADD_OBJECT):", data);
|
|
const service = await Services.getInstance();
|
|
if (data.type === "NOTIFICATIONS") {
|
|
service.setNotifications(data.data);
|
|
} else if (data.type === "TO_DOWNLOAD") {
|
|
console.log(`Received missing data ${data}`);
|
|
let requestedStateId = [];
|
|
for (const hash of data.data) {
|
|
try {
|
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
|
if (valueBytes) {
|
|
const blob = new Blob([valueBytes], { type: "application/octet-stream" });
|
|
await service.saveBlobToDb(hash, blob);
|
|
} else {
|
|
console.log("Request data from managers of the process");
|
|
const diff = await service.getDiffByValue(hash);
|
|
if (diff === null) {
|
|
continue;
|
|
}
|
|
const processId = diff.process_id;
|
|
const stateId = diff.state_id;
|
|
const roles = diff.roles;
|
|
if (!requestedStateId.includes(stateId)) {
|
|
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
|
requestedStateId.push(stateId);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
handleGetObjectResponse = (event) => {
|
|
console.log("Received response from service worker (GET_OBJECT):", event.data);
|
|
};
|
|
addObject(payload) {
|
|
return new Promise(async (resolve, reject) => {
|
|
if (!this.serviceWorkerRegistration) {
|
|
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
|
}
|
|
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
|
const messageChannel = new MessageChannel();
|
|
messageChannel.port1.onmessage = (event) => {
|
|
if (event.data.status === "success") {
|
|
resolve();
|
|
} else {
|
|
const error = event.data.message;
|
|
reject(new Error(error || "Unknown error occurred while adding object"));
|
|
}
|
|
};
|
|
try {
|
|
activeWorker?.postMessage({
|
|
type: "ADD_OBJECT",
|
|
payload
|
|
}, [messageChannel.port2]);
|
|
} catch (error) {
|
|
reject(new Error(`Failed to send message to service worker: ${error}`));
|
|
}
|
|
});
|
|
}
|
|
batchWriting(payload) {
|
|
return new Promise(async (resolve, reject) => {
|
|
if (!this.serviceWorkerRegistration) {
|
|
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
|
}
|
|
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
|
const messageChannel = new MessageChannel();
|
|
messageChannel.port1.onmessage = (event) => {
|
|
if (event.data.status === "success") {
|
|
resolve();
|
|
} else {
|
|
const error = event.data.message;
|
|
reject(new Error(error || "Unknown error occurred while adding objects"));
|
|
}
|
|
};
|
|
try {
|
|
activeWorker?.postMessage({
|
|
type: "BATCH_WRITING",
|
|
payload
|
|
}, [messageChannel.port2]);
|
|
} catch (error) {
|
|
reject(new Error(`Failed to send message to service worker: ${error}`));
|
|
}
|
|
});
|
|
}
|
|
async getObject(storeName, key) {
|
|
const db = await this.getDb();
|
|
const tx = db.transaction(storeName, "readonly");
|
|
const store = tx.objectStore(storeName);
|
|
const result = await new Promise((resolve, reject) => {
|
|
const getRequest = store.get(key);
|
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
});
|
|
return result;
|
|
}
|
|
async dumpStore(storeName) {
|
|
const db = await this.getDb();
|
|
const tx = db.transaction(storeName, "readonly");
|
|
const store = tx.objectStore(storeName);
|
|
try {
|
|
return new Promise((resolve, reject) => {
|
|
const result = {};
|
|
const cursor = store.openCursor();
|
|
cursor.onsuccess = (event) => {
|
|
const request = event.target;
|
|
const cursor2 = request.result;
|
|
if (cursor2) {
|
|
result[cursor2.key] = cursor2.value;
|
|
cursor2.continue();
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
};
|
|
cursor.onerror = () => {
|
|
reject(cursor.error);
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching data from IndexedDB:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
async deleteObject(storeName, key) {
|
|
const db = await this.getDb();
|
|
const tx = db.transaction(storeName, "readwrite");
|
|
const store = tx.objectStore(storeName);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const getRequest = store.delete(key);
|
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
async clearStore(storeName) {
|
|
const db = await this.getDb();
|
|
const tx = db.transaction(storeName, "readwrite");
|
|
const store = tx.objectStore(storeName);
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
const clearRequest = store.clear();
|
|
clearRequest.onsuccess = () => resolve(clearRequest.result);
|
|
clearRequest.onerror = () => reject(clearRequest.error);
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
// Request a store by index
|
|
async requestStoreByIndex(storeName, indexName, request) {
|
|
const db = await this.getDb();
|
|
const tx = db.transaction(storeName, "readonly");
|
|
const store = tx.objectStore(storeName);
|
|
const index = store.index(indexName);
|
|
try {
|
|
return new Promise((resolve, reject) => {
|
|
const getAllRequest = index.getAll(request);
|
|
getAllRequest.onsuccess = () => {
|
|
const allItems = getAllRequest.result;
|
|
const filtered = allItems.filter((item) => item.state_id === request);
|
|
resolve(filtered);
|
|
};
|
|
getAllRequest.onerror = () => reject(getAllRequest.error);
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
function bind(fn, thisArg) {
|
|
return function wrap() {
|
|
return fn.apply(thisArg, arguments);
|
|
};
|
|
}
|
|
|
|
// utils is a library of generic helper functions non-specific to axios
|
|
|
|
const {toString} = Object.prototype;
|
|
const {getPrototypeOf} = Object;
|
|
const {iterator, toStringTag} = Symbol;
|
|
|
|
const kindOf = (cache => thing => {
|
|
const str = toString.call(thing);
|
|
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
|
|
})(Object.create(null));
|
|
|
|
const kindOfTest = (type) => {
|
|
type = type.toLowerCase();
|
|
return (thing) => kindOf(thing) === type
|
|
};
|
|
|
|
const typeOfTest = type => thing => typeof thing === type;
|
|
|
|
/**
|
|
* Determine if a value is an Array
|
|
*
|
|
* @param {Object} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is an Array, otherwise false
|
|
*/
|
|
const {isArray} = Array;
|
|
|
|
/**
|
|
* Determine if a value is undefined
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if the value is undefined, otherwise false
|
|
*/
|
|
const isUndefined = typeOfTest('undefined');
|
|
|
|
/**
|
|
* Determine if a value is a Buffer
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a Buffer, otherwise false
|
|
*/
|
|
function isBuffer(val) {
|
|
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
|
|
&& isFunction(val.constructor.isBuffer) && val.constructor.isBuffer(val);
|
|
}
|
|
|
|
/**
|
|
* Determine if a value is an ArrayBuffer
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is an ArrayBuffer, otherwise false
|
|
*/
|
|
const isArrayBuffer = kindOfTest('ArrayBuffer');
|
|
|
|
|
|
/**
|
|
* Determine if a value is a view on an ArrayBuffer
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false
|
|
*/
|
|
function isArrayBufferView(val) {
|
|
let result;
|
|
if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
|
|
result = ArrayBuffer.isView(val);
|
|
} else {
|
|
result = (val) && (val.buffer) && (isArrayBuffer(val.buffer));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Determine if a value is a String
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a String, otherwise false
|
|
*/
|
|
const isString = typeOfTest('string');
|
|
|
|
/**
|
|
* Determine if a value is a Function
|
|
*
|
|
* @param {*} val The value to test
|
|
* @returns {boolean} True if value is a Function, otherwise false
|
|
*/
|
|
const isFunction = typeOfTest('function');
|
|
|
|
/**
|
|
* Determine if a value is a Number
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a Number, otherwise false
|
|
*/
|
|
const isNumber = typeOfTest('number');
|
|
|
|
/**
|
|
* Determine if a value is an Object
|
|
*
|
|
* @param {*} thing The value to test
|
|
*
|
|
* @returns {boolean} True if value is an Object, otherwise false
|
|
*/
|
|
const isObject$1 = (thing) => thing !== null && typeof thing === 'object';
|
|
|
|
/**
|
|
* Determine if a value is a Boolean
|
|
*
|
|
* @param {*} thing The value to test
|
|
* @returns {boolean} True if value is a Boolean, otherwise false
|
|
*/
|
|
const isBoolean = thing => thing === true || thing === false;
|
|
|
|
/**
|
|
* Determine if a value is a plain Object
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a plain Object, otherwise false
|
|
*/
|
|
const isPlainObject = (val) => {
|
|
if (kindOf(val) !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const prototype = getPrototypeOf(val);
|
|
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(toStringTag in val) && !(iterator in val);
|
|
};
|
|
|
|
/**
|
|
* Determine if a value is an empty object (safely handles Buffers)
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is an empty object, otherwise false
|
|
*/
|
|
const isEmptyObject = (val) => {
|
|
// Early return for non-objects or Buffers to prevent RangeError
|
|
if (!isObject$1(val) || isBuffer(val)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return Object.keys(val).length === 0 && Object.getPrototypeOf(val) === Object.prototype;
|
|
} catch (e) {
|
|
// Fallback for any other objects that might cause RangeError with Object.keys()
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Determine if a value is a Date
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a Date, otherwise false
|
|
*/
|
|
const isDate = kindOfTest('Date');
|
|
|
|
/**
|
|
* Determine if a value is a File
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a File, otherwise false
|
|
*/
|
|
const isFile = kindOfTest('File');
|
|
|
|
/**
|
|
* Determine if a value is a Blob
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a Blob, otherwise false
|
|
*/
|
|
const isBlob = kindOfTest('Blob');
|
|
|
|
/**
|
|
* Determine if a value is a FileList
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a File, otherwise false
|
|
*/
|
|
const isFileList = kindOfTest('FileList');
|
|
|
|
/**
|
|
* Determine if a value is a Stream
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a Stream, otherwise false
|
|
*/
|
|
const isStream = (val) => isObject$1(val) && isFunction(val.pipe);
|
|
|
|
/**
|
|
* Determine if a value is a FormData
|
|
*
|
|
* @param {*} thing The value to test
|
|
*
|
|
* @returns {boolean} True if value is an FormData, otherwise false
|
|
*/
|
|
const isFormData = (thing) => {
|
|
let kind;
|
|
return thing && (
|
|
(typeof FormData === 'function' && thing instanceof FormData) || (
|
|
isFunction(thing.append) && (
|
|
(kind = kindOf(thing)) === 'formdata' ||
|
|
// detect form-data instance
|
|
(kind === 'object' && isFunction(thing.toString) && thing.toString() === '[object FormData]')
|
|
)
|
|
)
|
|
)
|
|
};
|
|
|
|
/**
|
|
* Determine if a value is a URLSearchParams object
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
|
|
*/
|
|
const isURLSearchParams = kindOfTest('URLSearchParams');
|
|
|
|
const [isReadableStream, isRequest, isResponse, isHeaders] = ['ReadableStream', 'Request', 'Response', 'Headers'].map(kindOfTest);
|
|
|
|
/**
|
|
* Trim excess whitespace off the beginning and end of a string
|
|
*
|
|
* @param {String} str The String to trim
|
|
*
|
|
* @returns {String} The String freed of excess whitespace
|
|
*/
|
|
const trim = (str) => str.trim ?
|
|
str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
|
|
|
/**
|
|
* Iterate over an Array or an Object invoking a function for each item.
|
|
*
|
|
* If `obj` is an Array callback will be called passing
|
|
* the value, index, and complete array for each item.
|
|
*
|
|
* If 'obj' is an Object callback will be called passing
|
|
* the value, key, and complete object for each property.
|
|
*
|
|
* @param {Object|Array} obj The object to iterate
|
|
* @param {Function} fn The callback to invoke for each item
|
|
*
|
|
* @param {Boolean} [allOwnKeys = false]
|
|
* @returns {any}
|
|
*/
|
|
function forEach(obj, fn, {allOwnKeys = false} = {}) {
|
|
// Don't bother if no value provided
|
|
if (obj === null || typeof obj === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
let i;
|
|
let l;
|
|
|
|
// Force an array if not already something iterable
|
|
if (typeof obj !== 'object') {
|
|
/*eslint no-param-reassign:0*/
|
|
obj = [obj];
|
|
}
|
|
|
|
if (isArray(obj)) {
|
|
// Iterate over array values
|
|
for (i = 0, l = obj.length; i < l; i++) {
|
|
fn.call(null, obj[i], i, obj);
|
|
}
|
|
} else {
|
|
// Buffer check
|
|
if (isBuffer(obj)) {
|
|
return;
|
|
}
|
|
|
|
// Iterate over object keys
|
|
const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
|
|
const len = keys.length;
|
|
let key;
|
|
|
|
for (i = 0; i < len; i++) {
|
|
key = keys[i];
|
|
fn.call(null, obj[key], key, obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
function findKey(obj, key) {
|
|
if (isBuffer(obj)){
|
|
return null;
|
|
}
|
|
|
|
key = key.toLowerCase();
|
|
const keys = Object.keys(obj);
|
|
let i = keys.length;
|
|
let _key;
|
|
while (i-- > 0) {
|
|
_key = keys[i];
|
|
if (key === _key.toLowerCase()) {
|
|
return _key;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const _global = (() => {
|
|
/*eslint no-undef:0*/
|
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global)
|
|
})();
|
|
|
|
const isContextDefined = (context) => !isUndefined(context) && context !== _global;
|
|
|
|
/**
|
|
* Accepts varargs expecting each argument to be an object, then
|
|
* immutably merges the properties of each object and returns result.
|
|
*
|
|
* When multiple objects contain the same key the later object in
|
|
* the arguments list will take precedence.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```js
|
|
* var result = merge({foo: 123}, {foo: 456});
|
|
* console.log(result.foo); // outputs 456
|
|
* ```
|
|
*
|
|
* @param {Object} obj1 Object to merge
|
|
*
|
|
* @returns {Object} Result of all merge properties
|
|
*/
|
|
function merge(/* obj1, obj2, obj3, ... */) {
|
|
const {caseless} = isContextDefined(this) && this || {};
|
|
const result = {};
|
|
const assignValue = (val, key) => {
|
|
const targetKey = caseless && findKey(result, key) || key;
|
|
if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
|
|
result[targetKey] = merge(result[targetKey], val);
|
|
} else if (isPlainObject(val)) {
|
|
result[targetKey] = merge({}, val);
|
|
} else if (isArray(val)) {
|
|
result[targetKey] = val.slice();
|
|
} else {
|
|
result[targetKey] = val;
|
|
}
|
|
};
|
|
|
|
for (let i = 0, l = arguments.length; i < l; i++) {
|
|
arguments[i] && forEach(arguments[i], assignValue);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Extends object a by mutably adding to it the properties of object b.
|
|
*
|
|
* @param {Object} a The object to be extended
|
|
* @param {Object} b The object to copy properties from
|
|
* @param {Object} thisArg The object to bind function to
|
|
*
|
|
* @param {Boolean} [allOwnKeys]
|
|
* @returns {Object} The resulting value of object a
|
|
*/
|
|
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
|
|
forEach(b, (val, key) => {
|
|
if (thisArg && isFunction(val)) {
|
|
a[key] = bind(val, thisArg);
|
|
} else {
|
|
a[key] = val;
|
|
}
|
|
}, {allOwnKeys});
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
|
|
*
|
|
* @param {string} content with BOM
|
|
*
|
|
* @returns {string} content value without BOM
|
|
*/
|
|
const stripBOM = (content) => {
|
|
if (content.charCodeAt(0) === 0xFEFF) {
|
|
content = content.slice(1);
|
|
}
|
|
return content;
|
|
};
|
|
|
|
/**
|
|
* Inherit the prototype methods from one constructor into another
|
|
* @param {function} constructor
|
|
* @param {function} superConstructor
|
|
* @param {object} [props]
|
|
* @param {object} [descriptors]
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
const inherits = (constructor, superConstructor, props, descriptors) => {
|
|
constructor.prototype = Object.create(superConstructor.prototype, descriptors);
|
|
constructor.prototype.constructor = constructor;
|
|
Object.defineProperty(constructor, 'super', {
|
|
value: superConstructor.prototype
|
|
});
|
|
props && Object.assign(constructor.prototype, props);
|
|
};
|
|
|
|
/**
|
|
* Resolve object with deep prototype chain to a flat object
|
|
* @param {Object} sourceObj source object
|
|
* @param {Object} [destObj]
|
|
* @param {Function|Boolean} [filter]
|
|
* @param {Function} [propFilter]
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
const toFlatObject = (sourceObj, destObj, filter, propFilter) => {
|
|
let props;
|
|
let i;
|
|
let prop;
|
|
const merged = {};
|
|
|
|
destObj = destObj || {};
|
|
// eslint-disable-next-line no-eq-null,eqeqeq
|
|
if (sourceObj == null) return destObj;
|
|
|
|
do {
|
|
props = Object.getOwnPropertyNames(sourceObj);
|
|
i = props.length;
|
|
while (i-- > 0) {
|
|
prop = props[i];
|
|
if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) {
|
|
destObj[prop] = sourceObj[prop];
|
|
merged[prop] = true;
|
|
}
|
|
}
|
|
sourceObj = filter !== false && getPrototypeOf(sourceObj);
|
|
} while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype);
|
|
|
|
return destObj;
|
|
};
|
|
|
|
/**
|
|
* Determines whether a string ends with the characters of a specified string
|
|
*
|
|
* @param {String} str
|
|
* @param {String} searchString
|
|
* @param {Number} [position= 0]
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
const endsWith = (str, searchString, position) => {
|
|
str = String(str);
|
|
if (position === undefined || position > str.length) {
|
|
position = str.length;
|
|
}
|
|
position -= searchString.length;
|
|
const lastIndex = str.indexOf(searchString, position);
|
|
return lastIndex !== -1 && lastIndex === position;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns new array from array like object or null if failed
|
|
*
|
|
* @param {*} [thing]
|
|
*
|
|
* @returns {?Array}
|
|
*/
|
|
const toArray = (thing) => {
|
|
if (!thing) return null;
|
|
if (isArray(thing)) return thing;
|
|
let i = thing.length;
|
|
if (!isNumber(i)) return null;
|
|
const arr = new Array(i);
|
|
while (i-- > 0) {
|
|
arr[i] = thing[i];
|
|
}
|
|
return arr;
|
|
};
|
|
|
|
/**
|
|
* Checking if the Uint8Array exists and if it does, it returns a function that checks if the
|
|
* thing passed in is an instance of Uint8Array
|
|
*
|
|
* @param {TypedArray}
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
// eslint-disable-next-line func-names
|
|
const isTypedArray = (TypedArray => {
|
|
// eslint-disable-next-line func-names
|
|
return thing => {
|
|
return TypedArray && thing instanceof TypedArray;
|
|
};
|
|
})(typeof Uint8Array !== 'undefined' && getPrototypeOf(Uint8Array));
|
|
|
|
/**
|
|
* For each entry in the object, call the function with the key and value.
|
|
*
|
|
* @param {Object<any, any>} obj - The object to iterate over.
|
|
* @param {Function} fn - The function to call for each entry.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
const forEachEntry = (obj, fn) => {
|
|
const generator = obj && obj[iterator];
|
|
|
|
const _iterator = generator.call(obj);
|
|
|
|
let result;
|
|
|
|
while ((result = _iterator.next()) && !result.done) {
|
|
const pair = result.value;
|
|
fn.call(obj, pair[0], pair[1]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* It takes a regular expression and a string, and returns an array of all the matches
|
|
*
|
|
* @param {string} regExp - The regular expression to match against.
|
|
* @param {string} str - The string to search.
|
|
*
|
|
* @returns {Array<boolean>}
|
|
*/
|
|
const matchAll = (regExp, str) => {
|
|
let matches;
|
|
const arr = [];
|
|
|
|
while ((matches = regExp.exec(str)) !== null) {
|
|
arr.push(matches);
|
|
}
|
|
|
|
return arr;
|
|
};
|
|
|
|
/* Checking if the kindOfTest function returns true when passed an HTMLFormElement. */
|
|
const isHTMLForm = kindOfTest('HTMLFormElement');
|
|
|
|
const toCamelCase = str => {
|
|
return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,
|
|
function replacer(m, p1, p2) {
|
|
return p1.toUpperCase() + p2;
|
|
}
|
|
);
|
|
};
|
|
|
|
/* Creating a function that will check if an object has a property. */
|
|
const hasOwnProperty = (({hasOwnProperty}) => (obj, prop) => hasOwnProperty.call(obj, prop))(Object.prototype);
|
|
|
|
/**
|
|
* Determine if a value is a RegExp object
|
|
*
|
|
* @param {*} val The value to test
|
|
*
|
|
* @returns {boolean} True if value is a RegExp object, otherwise false
|
|
*/
|
|
const isRegExp = kindOfTest('RegExp');
|
|
|
|
const reduceDescriptors = (obj, reducer) => {
|
|
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
const reducedDescriptors = {};
|
|
|
|
forEach(descriptors, (descriptor, name) => {
|
|
let ret;
|
|
if ((ret = reducer(descriptor, name, obj)) !== false) {
|
|
reducedDescriptors[name] = ret || descriptor;
|
|
}
|
|
});
|
|
|
|
Object.defineProperties(obj, reducedDescriptors);
|
|
};
|
|
|
|
/**
|
|
* Makes all methods read-only
|
|
* @param {Object} obj
|
|
*/
|
|
|
|
const freezeMethods = (obj) => {
|
|
reduceDescriptors(obj, (descriptor, name) => {
|
|
// skip restricted props in strict mode
|
|
if (isFunction(obj) && ['arguments', 'caller', 'callee'].indexOf(name) !== -1) {
|
|
return false;
|
|
}
|
|
|
|
const value = obj[name];
|
|
|
|
if (!isFunction(value)) return;
|
|
|
|
descriptor.enumerable = false;
|
|
|
|
if ('writable' in descriptor) {
|
|
descriptor.writable = false;
|
|
return;
|
|
}
|
|
|
|
if (!descriptor.set) {
|
|
descriptor.set = () => {
|
|
throw Error('Can not rewrite read-only method \'' + name + '\'');
|
|
};
|
|
}
|
|
});
|
|
};
|
|
|
|
const toObjectSet = (arrayOrString, delimiter) => {
|
|
const obj = {};
|
|
|
|
const define = (arr) => {
|
|
arr.forEach(value => {
|
|
obj[value] = true;
|
|
});
|
|
};
|
|
|
|
isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter));
|
|
|
|
return obj;
|
|
};
|
|
|
|
const noop = () => {};
|
|
|
|
const toFiniteNumber = (value, defaultValue) => {
|
|
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
|
};
|
|
|
|
/**
|
|
* If the thing is a FormData object, return true, otherwise return false.
|
|
*
|
|
* @param {unknown} thing - The thing to check.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function isSpecCompliantForm(thing) {
|
|
return !!(thing && isFunction(thing.append) && thing[toStringTag] === 'FormData' && thing[iterator]);
|
|
}
|
|
|
|
const toJSONObject = (obj) => {
|
|
const stack = new Array(10);
|
|
|
|
const visit = (source, i) => {
|
|
|
|
if (isObject$1(source)) {
|
|
if (stack.indexOf(source) >= 0) {
|
|
return;
|
|
}
|
|
|
|
//Buffer check
|
|
if (isBuffer(source)) {
|
|
return source;
|
|
}
|
|
|
|
if(!('toJSON' in source)) {
|
|
stack[i] = source;
|
|
const target = isArray(source) ? [] : {};
|
|
|
|
forEach(source, (value, key) => {
|
|
const reducedValue = visit(value, i + 1);
|
|
!isUndefined(reducedValue) && (target[key] = reducedValue);
|
|
});
|
|
|
|
stack[i] = undefined;
|
|
|
|
return target;
|
|
}
|
|
}
|
|
|
|
return source;
|
|
};
|
|
|
|
return visit(obj, 0);
|
|
};
|
|
|
|
const isAsyncFn = kindOfTest('AsyncFunction');
|
|
|
|
const isThenable = (thing) =>
|
|
thing && (isObject$1(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing.catch);
|
|
|
|
// original code
|
|
// https://github.com/DigitalBrainJS/AxiosPromise/blob/16deab13710ec09779922131f3fa5954320f83ab/lib/utils.js#L11-L34
|
|
|
|
const _setImmediate = ((setImmediateSupported, postMessageSupported) => {
|
|
if (setImmediateSupported) {
|
|
return setImmediate;
|
|
}
|
|
|
|
return postMessageSupported ? ((token, callbacks) => {
|
|
_global.addEventListener("message", ({source, data}) => {
|
|
if (source === _global && data === token) {
|
|
callbacks.length && callbacks.shift()();
|
|
}
|
|
}, false);
|
|
|
|
return (cb) => {
|
|
callbacks.push(cb);
|
|
_global.postMessage(token, "*");
|
|
}
|
|
})(`axios@${Math.random()}`, []) : (cb) => setTimeout(cb);
|
|
})(
|
|
typeof setImmediate === 'function',
|
|
isFunction(_global.postMessage)
|
|
);
|
|
|
|
const asap = typeof queueMicrotask !== 'undefined' ?
|
|
queueMicrotask.bind(_global) : ( typeof process !== 'undefined' && process.nextTick || _setImmediate);
|
|
|
|
// *********************
|
|
|
|
|
|
const isIterable = (thing) => thing != null && isFunction(thing[iterator]);
|
|
|
|
|
|
const utils$1 = {
|
|
isArray,
|
|
isArrayBuffer,
|
|
isBuffer,
|
|
isFormData,
|
|
isArrayBufferView,
|
|
isString,
|
|
isNumber,
|
|
isBoolean,
|
|
isObject: isObject$1,
|
|
isPlainObject,
|
|
isEmptyObject,
|
|
isReadableStream,
|
|
isRequest,
|
|
isResponse,
|
|
isHeaders,
|
|
isUndefined,
|
|
isDate,
|
|
isFile,
|
|
isBlob,
|
|
isRegExp,
|
|
isFunction,
|
|
isStream,
|
|
isURLSearchParams,
|
|
isTypedArray,
|
|
isFileList,
|
|
forEach,
|
|
merge,
|
|
extend,
|
|
trim,
|
|
stripBOM,
|
|
inherits,
|
|
toFlatObject,
|
|
kindOf,
|
|
kindOfTest,
|
|
endsWith,
|
|
toArray,
|
|
forEachEntry,
|
|
matchAll,
|
|
isHTMLForm,
|
|
hasOwnProperty,
|
|
hasOwnProp: hasOwnProperty, // an alias to avoid ESLint no-prototype-builtins detection
|
|
reduceDescriptors,
|
|
freezeMethods,
|
|
toObjectSet,
|
|
toCamelCase,
|
|
noop,
|
|
toFiniteNumber,
|
|
findKey,
|
|
global: _global,
|
|
isContextDefined,
|
|
isSpecCompliantForm,
|
|
toJSONObject,
|
|
isAsyncFn,
|
|
isThenable,
|
|
setImmediate: _setImmediate,
|
|
asap,
|
|
isIterable
|
|
};
|
|
|
|
/**
|
|
* Create an Error with the specified message, config, error code, request and response.
|
|
*
|
|
* @param {string} message The error message.
|
|
* @param {string} [code] The error code (for example, 'ECONNABORTED').
|
|
* @param {Object} [config] The config.
|
|
* @param {Object} [request] The request.
|
|
* @param {Object} [response] The response.
|
|
*
|
|
* @returns {Error} The created error.
|
|
*/
|
|
function AxiosError(message, code, config, request, response) {
|
|
Error.call(this);
|
|
|
|
if (Error.captureStackTrace) {
|
|
Error.captureStackTrace(this, this.constructor);
|
|
} else {
|
|
this.stack = (new Error()).stack;
|
|
}
|
|
|
|
this.message = message;
|
|
this.name = 'AxiosError';
|
|
code && (this.code = code);
|
|
config && (this.config = config);
|
|
request && (this.request = request);
|
|
if (response) {
|
|
this.response = response;
|
|
this.status = response.status ? response.status : null;
|
|
}
|
|
}
|
|
|
|
utils$1.inherits(AxiosError, Error, {
|
|
toJSON: function toJSON() {
|
|
return {
|
|
// Standard
|
|
message: this.message,
|
|
name: this.name,
|
|
// Microsoft
|
|
description: this.description,
|
|
number: this.number,
|
|
// Mozilla
|
|
fileName: this.fileName,
|
|
lineNumber: this.lineNumber,
|
|
columnNumber: this.columnNumber,
|
|
stack: this.stack,
|
|
// Axios
|
|
config: utils$1.toJSONObject(this.config),
|
|
code: this.code,
|
|
status: this.status
|
|
};
|
|
}
|
|
});
|
|
|
|
const prototype$1 = AxiosError.prototype;
|
|
const descriptors = {};
|
|
|
|
[
|
|
'ERR_BAD_OPTION_VALUE',
|
|
'ERR_BAD_OPTION',
|
|
'ECONNABORTED',
|
|
'ETIMEDOUT',
|
|
'ERR_NETWORK',
|
|
'ERR_FR_TOO_MANY_REDIRECTS',
|
|
'ERR_DEPRECATED',
|
|
'ERR_BAD_RESPONSE',
|
|
'ERR_BAD_REQUEST',
|
|
'ERR_CANCELED',
|
|
'ERR_NOT_SUPPORT',
|
|
'ERR_INVALID_URL'
|
|
// eslint-disable-next-line func-names
|
|
].forEach(code => {
|
|
descriptors[code] = {value: code};
|
|
});
|
|
|
|
Object.defineProperties(AxiosError, descriptors);
|
|
Object.defineProperty(prototype$1, 'isAxiosError', {value: true});
|
|
|
|
// eslint-disable-next-line func-names
|
|
AxiosError.from = (error, code, config, request, response, customProps) => {
|
|
const axiosError = Object.create(prototype$1);
|
|
|
|
utils$1.toFlatObject(error, axiosError, function filter(obj) {
|
|
return obj !== Error.prototype;
|
|
}, prop => {
|
|
return prop !== 'isAxiosError';
|
|
});
|
|
|
|
AxiosError.call(axiosError, error.message, code, config, request, response);
|
|
|
|
axiosError.cause = error;
|
|
|
|
axiosError.name = error.name;
|
|
|
|
customProps && Object.assign(axiosError, customProps);
|
|
|
|
return axiosError;
|
|
};
|
|
|
|
// eslint-disable-next-line strict
|
|
const httpAdapter = null;
|
|
|
|
/**
|
|
* Determines if the given thing is a array or js object.
|
|
*
|
|
* @param {string} thing - The object or array to be visited.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function isVisitable(thing) {
|
|
return utils$1.isPlainObject(thing) || utils$1.isArray(thing);
|
|
}
|
|
|
|
/**
|
|
* It removes the brackets from the end of a string
|
|
*
|
|
* @param {string} key - The key of the parameter.
|
|
*
|
|
* @returns {string} the key without the brackets.
|
|
*/
|
|
function removeBrackets(key) {
|
|
return utils$1.endsWith(key, '[]') ? key.slice(0, -2) : key;
|
|
}
|
|
|
|
/**
|
|
* It takes a path, a key, and a boolean, and returns a string
|
|
*
|
|
* @param {string} path - The path to the current key.
|
|
* @param {string} key - The key of the current object being iterated over.
|
|
* @param {string} dots - If true, the key will be rendered with dots instead of brackets.
|
|
*
|
|
* @returns {string} The path to the current key.
|
|
*/
|
|
function renderKey(path, key, dots) {
|
|
if (!path) return key;
|
|
return path.concat(key).map(function each(token, i) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
token = removeBrackets(token);
|
|
return !dots && i ? '[' + token + ']' : token;
|
|
}).join(dots ? '.' : '');
|
|
}
|
|
|
|
/**
|
|
* If the array is an array and none of its elements are visitable, then it's a flat array.
|
|
*
|
|
* @param {Array<any>} arr - The array to check
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function isFlatArray(arr) {
|
|
return utils$1.isArray(arr) && !arr.some(isVisitable);
|
|
}
|
|
|
|
const predicates = utils$1.toFlatObject(utils$1, {}, null, function filter(prop) {
|
|
return /^is[A-Z]/.test(prop);
|
|
});
|
|
|
|
/**
|
|
* Convert a data object to FormData
|
|
*
|
|
* @param {Object} obj
|
|
* @param {?Object} [formData]
|
|
* @param {?Object} [options]
|
|
* @param {Function} [options.visitor]
|
|
* @param {Boolean} [options.metaTokens = true]
|
|
* @param {Boolean} [options.dots = false]
|
|
* @param {?Boolean} [options.indexes = false]
|
|
*
|
|
* @returns {Object}
|
|
**/
|
|
|
|
/**
|
|
* It converts an object into a FormData object
|
|
*
|
|
* @param {Object<any, any>} obj - The object to convert to form data.
|
|
* @param {string} formData - The FormData object to append to.
|
|
* @param {Object<string, any>} options
|
|
*
|
|
* @returns
|
|
*/
|
|
function toFormData(obj, formData, options) {
|
|
if (!utils$1.isObject(obj)) {
|
|
throw new TypeError('target must be an object');
|
|
}
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
formData = formData || new (FormData)();
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
options = utils$1.toFlatObject(options, {
|
|
metaTokens: true,
|
|
dots: false,
|
|
indexes: false
|
|
}, false, function defined(option, source) {
|
|
// eslint-disable-next-line no-eq-null,eqeqeq
|
|
return !utils$1.isUndefined(source[option]);
|
|
});
|
|
|
|
const metaTokens = options.metaTokens;
|
|
// eslint-disable-next-line no-use-before-define
|
|
const visitor = options.visitor || defaultVisitor;
|
|
const dots = options.dots;
|
|
const indexes = options.indexes;
|
|
const _Blob = options.Blob || typeof Blob !== 'undefined' && Blob;
|
|
const useBlob = _Blob && utils$1.isSpecCompliantForm(formData);
|
|
|
|
if (!utils$1.isFunction(visitor)) {
|
|
throw new TypeError('visitor must be a function');
|
|
}
|
|
|
|
function convertValue(value) {
|
|
if (value === null) return '';
|
|
|
|
if (utils$1.isDate(value)) {
|
|
return value.toISOString();
|
|
}
|
|
|
|
if (utils$1.isBoolean(value)) {
|
|
return value.toString();
|
|
}
|
|
|
|
if (!useBlob && utils$1.isBlob(value)) {
|
|
throw new AxiosError('Blob is not supported. Use a Buffer instead.');
|
|
}
|
|
|
|
if (utils$1.isArrayBuffer(value) || utils$1.isTypedArray(value)) {
|
|
return useBlob && typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Default visitor.
|
|
*
|
|
* @param {*} value
|
|
* @param {String|Number} key
|
|
* @param {Array<String|Number>} path
|
|
* @this {FormData}
|
|
*
|
|
* @returns {boolean} return true to visit the each prop of the value recursively
|
|
*/
|
|
function defaultVisitor(value, key, path) {
|
|
let arr = value;
|
|
|
|
if (value && !path && typeof value === 'object') {
|
|
if (utils$1.endsWith(key, '{}')) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
key = metaTokens ? key : key.slice(0, -2);
|
|
// eslint-disable-next-line no-param-reassign
|
|
value = JSON.stringify(value);
|
|
} else if (
|
|
(utils$1.isArray(value) && isFlatArray(value)) ||
|
|
((utils$1.isFileList(value) || utils$1.endsWith(key, '[]')) && (arr = utils$1.toArray(value))
|
|
)) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
key = removeBrackets(key);
|
|
|
|
arr.forEach(function each(el, index) {
|
|
!(utils$1.isUndefined(el) || el === null) && formData.append(
|
|
// eslint-disable-next-line no-nested-ternary
|
|
indexes === true ? renderKey([key], index, dots) : (indexes === null ? key : key + '[]'),
|
|
convertValue(el)
|
|
);
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isVisitable(value)) {
|
|
return true;
|
|
}
|
|
|
|
formData.append(renderKey(path, key, dots), convertValue(value));
|
|
|
|
return false;
|
|
}
|
|
|
|
const stack = [];
|
|
|
|
const exposedHelpers = Object.assign(predicates, {
|
|
defaultVisitor,
|
|
convertValue,
|
|
isVisitable
|
|
});
|
|
|
|
function build(value, path) {
|
|
if (utils$1.isUndefined(value)) return;
|
|
|
|
if (stack.indexOf(value) !== -1) {
|
|
throw Error('Circular reference detected in ' + path.join('.'));
|
|
}
|
|
|
|
stack.push(value);
|
|
|
|
utils$1.forEach(value, function each(el, key) {
|
|
const result = !(utils$1.isUndefined(el) || el === null) && visitor.call(
|
|
formData, el, utils$1.isString(key) ? key.trim() : key, path, exposedHelpers
|
|
);
|
|
|
|
if (result === true) {
|
|
build(el, path ? path.concat(key) : [key]);
|
|
}
|
|
});
|
|
|
|
stack.pop();
|
|
}
|
|
|
|
if (!utils$1.isObject(obj)) {
|
|
throw new TypeError('data must be an object');
|
|
}
|
|
|
|
build(obj);
|
|
|
|
return formData;
|
|
}
|
|
|
|
/**
|
|
* It encodes a string by replacing all characters that are not in the unreserved set with
|
|
* their percent-encoded equivalents
|
|
*
|
|
* @param {string} str - The string to encode.
|
|
*
|
|
* @returns {string} The encoded string.
|
|
*/
|
|
function encode$2(str) {
|
|
const charMap = {
|
|
'!': '%21',
|
|
"'": '%27',
|
|
'(': '%28',
|
|
')': '%29',
|
|
'~': '%7E',
|
|
'%20': '+',
|
|
'%00': '\x00'
|
|
};
|
|
return encodeURIComponent(str).replace(/[!'()~]|%20|%00/g, function replacer(match) {
|
|
return charMap[match];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* It takes a params object and converts it to a FormData object
|
|
*
|
|
* @param {Object<string, any>} params - The parameters to be converted to a FormData object.
|
|
* @param {Object<string, any>} options - The options object passed to the Axios constructor.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function AxiosURLSearchParams(params, options) {
|
|
this._pairs = [];
|
|
|
|
params && toFormData(params, this, options);
|
|
}
|
|
|
|
const prototype = AxiosURLSearchParams.prototype;
|
|
|
|
prototype.append = function append(name, value) {
|
|
this._pairs.push([name, value]);
|
|
};
|
|
|
|
prototype.toString = function toString(encoder) {
|
|
const _encode = encoder ? function(value) {
|
|
return encoder.call(this, value, encode$2);
|
|
} : encode$2;
|
|
|
|
return this._pairs.map(function each(pair) {
|
|
return _encode(pair[0]) + '=' + _encode(pair[1]);
|
|
}, '').join('&');
|
|
};
|
|
|
|
/**
|
|
* It replaces all instances of the characters `:`, `$`, `,`, `+`, `[`, and `]` with their
|
|
* URI encoded counterparts
|
|
*
|
|
* @param {string} val The value to be encoded.
|
|
*
|
|
* @returns {string} The encoded value.
|
|
*/
|
|
function encode$1(val) {
|
|
return encodeURIComponent(val).
|
|
replace(/%3A/gi, ':').
|
|
replace(/%24/g, '$').
|
|
replace(/%2C/gi, ',').
|
|
replace(/%20/g, '+').
|
|
replace(/%5B/gi, '[').
|
|
replace(/%5D/gi, ']');
|
|
}
|
|
|
|
/**
|
|
* Build a URL by appending params to the end
|
|
*
|
|
* @param {string} url The base of the url (e.g., http://www.google.com)
|
|
* @param {object} [params] The params to be appended
|
|
* @param {?(object|Function)} options
|
|
*
|
|
* @returns {string} The formatted url
|
|
*/
|
|
function buildURL(url, params, options) {
|
|
/*eslint no-param-reassign:0*/
|
|
if (!params) {
|
|
return url;
|
|
}
|
|
|
|
const _encode = options && options.encode || encode$1;
|
|
|
|
if (utils$1.isFunction(options)) {
|
|
options = {
|
|
serialize: options
|
|
};
|
|
}
|
|
|
|
const serializeFn = options && options.serialize;
|
|
|
|
let serializedParams;
|
|
|
|
if (serializeFn) {
|
|
serializedParams = serializeFn(params, options);
|
|
} else {
|
|
serializedParams = utils$1.isURLSearchParams(params) ?
|
|
params.toString() :
|
|
new AxiosURLSearchParams(params, options).toString(_encode);
|
|
}
|
|
|
|
if (serializedParams) {
|
|
const hashmarkIndex = url.indexOf("#");
|
|
|
|
if (hashmarkIndex !== -1) {
|
|
url = url.slice(0, hashmarkIndex);
|
|
}
|
|
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
class InterceptorManager {
|
|
constructor() {
|
|
this.handlers = [];
|
|
}
|
|
|
|
/**
|
|
* Add a new interceptor to the stack
|
|
*
|
|
* @param {Function} fulfilled The function to handle `then` for a `Promise`
|
|
* @param {Function} rejected The function to handle `reject` for a `Promise`
|
|
*
|
|
* @return {Number} An ID used to remove interceptor later
|
|
*/
|
|
use(fulfilled, rejected, options) {
|
|
this.handlers.push({
|
|
fulfilled,
|
|
rejected,
|
|
synchronous: options ? options.synchronous : false,
|
|
runWhen: options ? options.runWhen : null
|
|
});
|
|
return this.handlers.length - 1;
|
|
}
|
|
|
|
/**
|
|
* Remove an interceptor from the stack
|
|
*
|
|
* @param {Number} id The ID that was returned by `use`
|
|
*
|
|
* @returns {Boolean} `true` if the interceptor was removed, `false` otherwise
|
|
*/
|
|
eject(id) {
|
|
if (this.handlers[id]) {
|
|
this.handlers[id] = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all interceptors from the stack
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
clear() {
|
|
if (this.handlers) {
|
|
this.handlers = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterate over all the registered interceptors
|
|
*
|
|
* This method is particularly useful for skipping over any
|
|
* interceptors that may have become `null` calling `eject`.
|
|
*
|
|
* @param {Function} fn The function to call for each interceptor
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
forEach(fn) {
|
|
utils$1.forEach(this.handlers, function forEachHandler(h) {
|
|
if (h !== null) {
|
|
fn(h);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const transitionalDefaults = {
|
|
silentJSONParsing: true,
|
|
forcedJSONParsing: true,
|
|
clarifyTimeoutError: false
|
|
};
|
|
|
|
const URLSearchParams$1 = typeof URLSearchParams !== 'undefined' ? URLSearchParams : AxiosURLSearchParams;
|
|
|
|
const FormData$1 = typeof FormData !== 'undefined' ? FormData : null;
|
|
|
|
const Blob$1 = typeof Blob !== 'undefined' ? Blob : null;
|
|
|
|
const platform$1 = {
|
|
isBrowser: true,
|
|
classes: {
|
|
URLSearchParams: URLSearchParams$1,
|
|
FormData: FormData$1,
|
|
Blob: Blob$1
|
|
},
|
|
protocols: ['http', 'https', 'file', 'blob', 'url', 'data']
|
|
};
|
|
|
|
const hasBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
|
|
const _navigator = typeof navigator === 'object' && navigator || undefined;
|
|
|
|
/**
|
|
* Determine if we're running in a standard browser environment
|
|
*
|
|
* This allows axios to run in a web worker, and react-native.
|
|
* Both environments support XMLHttpRequest, but not fully standard globals.
|
|
*
|
|
* web workers:
|
|
* typeof window -> undefined
|
|
* typeof document -> undefined
|
|
*
|
|
* react-native:
|
|
* navigator.product -> 'ReactNative'
|
|
* nativescript
|
|
* navigator.product -> 'NativeScript' or 'NS'
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
const hasStandardBrowserEnv = hasBrowserEnv &&
|
|
(!_navigator || ['ReactNative', 'NativeScript', 'NS'].indexOf(_navigator.product) < 0);
|
|
|
|
/**
|
|
* Determine if we're running in a standard browser webWorker environment
|
|
*
|
|
* Although the `isStandardBrowserEnv` method indicates that
|
|
* `allows axios to run in a web worker`, the WebWorker will still be
|
|
* filtered out due to its judgment standard
|
|
* `typeof window !== 'undefined' && typeof document !== 'undefined'`.
|
|
* This leads to a problem when axios post `FormData` in webWorker
|
|
*/
|
|
const hasStandardBrowserWebWorkerEnv = (() => {
|
|
return (
|
|
typeof WorkerGlobalScope !== 'undefined' &&
|
|
// eslint-disable-next-line no-undef
|
|
self instanceof WorkerGlobalScope &&
|
|
typeof self.importScripts === 'function'
|
|
);
|
|
})();
|
|
|
|
const origin = hasBrowserEnv && window.location.href || 'http://localhost';
|
|
|
|
const utils = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
|
__proto__: null,
|
|
hasBrowserEnv,
|
|
hasStandardBrowserEnv,
|
|
hasStandardBrowserWebWorkerEnv,
|
|
navigator: _navigator,
|
|
origin
|
|
}, Symbol.toStringTag, { value: 'Module' }));
|
|
|
|
const platform = {
|
|
...utils,
|
|
...platform$1
|
|
};
|
|
|
|
function toURLEncodedForm(data, options) {
|
|
return toFormData(data, new platform.classes.URLSearchParams(), {
|
|
visitor: function(value, key, path, helpers) {
|
|
if (platform.isNode && utils$1.isBuffer(value)) {
|
|
this.append(key, value.toString('base64'));
|
|
return false;
|
|
}
|
|
|
|
return helpers.defaultVisitor.apply(this, arguments);
|
|
},
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* It takes a string like `foo[x][y][z]` and returns an array like `['foo', 'x', 'y', 'z']
|
|
*
|
|
* @param {string} name - The name of the property to get.
|
|
*
|
|
* @returns An array of strings.
|
|
*/
|
|
function parsePropPath(name) {
|
|
// foo[x][y][z]
|
|
// foo.x.y.z
|
|
// foo-x-y-z
|
|
// foo x y z
|
|
return utils$1.matchAll(/\w+|\[(\w*)]/g, name).map(match => {
|
|
return match[0] === '[]' ? '' : match[1] || match[0];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert an array to an object.
|
|
*
|
|
* @param {Array<any>} arr - The array to convert to an object.
|
|
*
|
|
* @returns An object with the same keys and values as the array.
|
|
*/
|
|
function arrayToObject(arr) {
|
|
const obj = {};
|
|
const keys = Object.keys(arr);
|
|
let i;
|
|
const len = keys.length;
|
|
let key;
|
|
for (i = 0; i < len; i++) {
|
|
key = keys[i];
|
|
obj[key] = arr[key];
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* It takes a FormData object and returns a JavaScript object
|
|
*
|
|
* @param {string} formData The FormData object to convert to JSON.
|
|
*
|
|
* @returns {Object<string, any> | null} The converted object.
|
|
*/
|
|
function formDataToJSON(formData) {
|
|
function buildPath(path, value, target, index) {
|
|
let name = path[index++];
|
|
|
|
if (name === '__proto__') return true;
|
|
|
|
const isNumericKey = Number.isFinite(+name);
|
|
const isLast = index >= path.length;
|
|
name = !name && utils$1.isArray(target) ? target.length : name;
|
|
|
|
if (isLast) {
|
|
if (utils$1.hasOwnProp(target, name)) {
|
|
target[name] = [target[name], value];
|
|
} else {
|
|
target[name] = value;
|
|
}
|
|
|
|
return !isNumericKey;
|
|
}
|
|
|
|
if (!target[name] || !utils$1.isObject(target[name])) {
|
|
target[name] = [];
|
|
}
|
|
|
|
const result = buildPath(path, value, target[name], index);
|
|
|
|
if (result && utils$1.isArray(target[name])) {
|
|
target[name] = arrayToObject(target[name]);
|
|
}
|
|
|
|
return !isNumericKey;
|
|
}
|
|
|
|
if (utils$1.isFormData(formData) && utils$1.isFunction(formData.entries)) {
|
|
const obj = {};
|
|
|
|
utils$1.forEachEntry(formData, (name, value) => {
|
|
buildPath(parsePropPath(name), value, obj, 0);
|
|
});
|
|
|
|
return obj;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* It takes a string, tries to parse it, and if it fails, it returns the stringified version
|
|
* of the input
|
|
*
|
|
* @param {any} rawValue - The value to be stringified.
|
|
* @param {Function} parser - A function that parses a string into a JavaScript object.
|
|
* @param {Function} encoder - A function that takes a value and returns a string.
|
|
*
|
|
* @returns {string} A stringified version of the rawValue.
|
|
*/
|
|
function stringifySafely(rawValue, parser, encoder) {
|
|
if (utils$1.isString(rawValue)) {
|
|
try {
|
|
(parser || JSON.parse)(rawValue);
|
|
return utils$1.trim(rawValue);
|
|
} catch (e) {
|
|
if (e.name !== 'SyntaxError') {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (0, JSON.stringify)(rawValue);
|
|
}
|
|
|
|
const defaults = {
|
|
|
|
transitional: transitionalDefaults,
|
|
|
|
adapter: ['xhr', 'http', 'fetch'],
|
|
|
|
transformRequest: [function transformRequest(data, headers) {
|
|
const contentType = headers.getContentType() || '';
|
|
const hasJSONContentType = contentType.indexOf('application/json') > -1;
|
|
const isObjectPayload = utils$1.isObject(data);
|
|
|
|
if (isObjectPayload && utils$1.isHTMLForm(data)) {
|
|
data = new FormData(data);
|
|
}
|
|
|
|
const isFormData = utils$1.isFormData(data);
|
|
|
|
if (isFormData) {
|
|
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
|
|
}
|
|
|
|
if (utils$1.isArrayBuffer(data) ||
|
|
utils$1.isBuffer(data) ||
|
|
utils$1.isStream(data) ||
|
|
utils$1.isFile(data) ||
|
|
utils$1.isBlob(data) ||
|
|
utils$1.isReadableStream(data)
|
|
) {
|
|
return data;
|
|
}
|
|
if (utils$1.isArrayBufferView(data)) {
|
|
return data.buffer;
|
|
}
|
|
if (utils$1.isURLSearchParams(data)) {
|
|
headers.setContentType('application/x-www-form-urlencoded;charset=utf-8', false);
|
|
return data.toString();
|
|
}
|
|
|
|
let isFileList;
|
|
|
|
if (isObjectPayload) {
|
|
if (contentType.indexOf('application/x-www-form-urlencoded') > -1) {
|
|
return toURLEncodedForm(data, this.formSerializer).toString();
|
|
}
|
|
|
|
if ((isFileList = utils$1.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
|
|
const _FormData = this.env && this.env.FormData;
|
|
|
|
return toFormData(
|
|
isFileList ? {'files[]': data} : data,
|
|
_FormData && new _FormData(),
|
|
this.formSerializer
|
|
);
|
|
}
|
|
}
|
|
|
|
if (isObjectPayload || hasJSONContentType ) {
|
|
headers.setContentType('application/json', false);
|
|
return stringifySafely(data);
|
|
}
|
|
|
|
return data;
|
|
}],
|
|
|
|
transformResponse: [function transformResponse(data) {
|
|
const transitional = this.transitional || defaults.transitional;
|
|
const forcedJSONParsing = transitional && transitional.forcedJSONParsing;
|
|
const JSONRequested = this.responseType === 'json';
|
|
|
|
if (utils$1.isResponse(data) || utils$1.isReadableStream(data)) {
|
|
return data;
|
|
}
|
|
|
|
if (data && utils$1.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
|
|
const silentJSONParsing = transitional && transitional.silentJSONParsing;
|
|
const strictJSONParsing = !silentJSONParsing && JSONRequested;
|
|
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (e) {
|
|
if (strictJSONParsing) {
|
|
if (e.name === 'SyntaxError') {
|
|
throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}],
|
|
|
|
/**
|
|
* A timeout in milliseconds to abort a request. If set to 0 (default) a
|
|
* timeout is not created.
|
|
*/
|
|
timeout: 0,
|
|
|
|
xsrfCookieName: 'XSRF-TOKEN',
|
|
xsrfHeaderName: 'X-XSRF-TOKEN',
|
|
|
|
maxContentLength: -1,
|
|
maxBodyLength: -1,
|
|
|
|
env: {
|
|
FormData: platform.classes.FormData,
|
|
Blob: platform.classes.Blob
|
|
},
|
|
|
|
validateStatus: function validateStatus(status) {
|
|
return status >= 200 && status < 300;
|
|
},
|
|
|
|
headers: {
|
|
common: {
|
|
'Accept': 'application/json, text/plain, */*',
|
|
'Content-Type': undefined
|
|
}
|
|
}
|
|
};
|
|
|
|
utils$1.forEach(['delete', 'get', 'head', 'post', 'put', 'patch'], (method) => {
|
|
defaults.headers[method] = {};
|
|
});
|
|
|
|
// RawAxiosHeaders whose duplicates are ignored by node
|
|
// c.f. https://nodejs.org/api/http.html#http_message_headers
|
|
const ignoreDuplicateOf = utils$1.toObjectSet([
|
|
'age', 'authorization', 'content-length', 'content-type', 'etag',
|
|
'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since',
|
|
'last-modified', 'location', 'max-forwards', 'proxy-authorization',
|
|
'referer', 'retry-after', 'user-agent'
|
|
]);
|
|
|
|
/**
|
|
* Parse headers into an object
|
|
*
|
|
* ```
|
|
* Date: Wed, 27 Aug 2014 08:58:49 GMT
|
|
* Content-Type: application/json
|
|
* Connection: keep-alive
|
|
* Transfer-Encoding: chunked
|
|
* ```
|
|
*
|
|
* @param {String} rawHeaders Headers needing to be parsed
|
|
*
|
|
* @returns {Object} Headers parsed into an object
|
|
*/
|
|
const parseHeaders = rawHeaders => {
|
|
const parsed = {};
|
|
let key;
|
|
let val;
|
|
let i;
|
|
|
|
rawHeaders && rawHeaders.split('\n').forEach(function parser(line) {
|
|
i = line.indexOf(':');
|
|
key = line.substring(0, i).trim().toLowerCase();
|
|
val = line.substring(i + 1).trim();
|
|
|
|
if (!key || (parsed[key] && ignoreDuplicateOf[key])) {
|
|
return;
|
|
}
|
|
|
|
if (key === 'set-cookie') {
|
|
if (parsed[key]) {
|
|
parsed[key].push(val);
|
|
} else {
|
|
parsed[key] = [val];
|
|
}
|
|
} else {
|
|
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
|
}
|
|
});
|
|
|
|
return parsed;
|
|
};
|
|
|
|
const $internals = Symbol('internals');
|
|
|
|
function normalizeHeader(header) {
|
|
return header && String(header).trim().toLowerCase();
|
|
}
|
|
|
|
function normalizeValue(value) {
|
|
if (value === false || value == null) {
|
|
return value;
|
|
}
|
|
|
|
return utils$1.isArray(value) ? value.map(normalizeValue) : String(value);
|
|
}
|
|
|
|
function parseTokens(str) {
|
|
const tokens = Object.create(null);
|
|
const tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
|
|
let match;
|
|
|
|
while ((match = tokensRE.exec(str))) {
|
|
tokens[match[1]] = match[2];
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
const isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim());
|
|
|
|
function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) {
|
|
if (utils$1.isFunction(filter)) {
|
|
return filter.call(this, value, header);
|
|
}
|
|
|
|
if (isHeaderNameFilter) {
|
|
value = header;
|
|
}
|
|
|
|
if (!utils$1.isString(value)) return;
|
|
|
|
if (utils$1.isString(filter)) {
|
|
return value.indexOf(filter) !== -1;
|
|
}
|
|
|
|
if (utils$1.isRegExp(filter)) {
|
|
return filter.test(value);
|
|
}
|
|
}
|
|
|
|
function formatHeader(header) {
|
|
return header.trim()
|
|
.toLowerCase().replace(/([a-z\d])(\w*)/g, (w, char, str) => {
|
|
return char.toUpperCase() + str;
|
|
});
|
|
}
|
|
|
|
function buildAccessors(obj, header) {
|
|
const accessorName = utils$1.toCamelCase(' ' + header);
|
|
|
|
['get', 'set', 'has'].forEach(methodName => {
|
|
Object.defineProperty(obj, methodName + accessorName, {
|
|
value: function(arg1, arg2, arg3) {
|
|
return this[methodName].call(this, header, arg1, arg2, arg3);
|
|
},
|
|
configurable: true
|
|
});
|
|
});
|
|
}
|
|
|
|
class AxiosHeaders {
|
|
constructor(headers) {
|
|
headers && this.set(headers);
|
|
}
|
|
|
|
set(header, valueOrRewrite, rewrite) {
|
|
const self = this;
|
|
|
|
function setHeader(_value, _header, _rewrite) {
|
|
const lHeader = normalizeHeader(_header);
|
|
|
|
if (!lHeader) {
|
|
throw new Error('header name must be a non-empty string');
|
|
}
|
|
|
|
const key = utils$1.findKey(self, lHeader);
|
|
|
|
if(!key || self[key] === undefined || _rewrite === true || (_rewrite === undefined && self[key] !== false)) {
|
|
self[key || _header] = normalizeValue(_value);
|
|
}
|
|
}
|
|
|
|
const setHeaders = (headers, _rewrite) =>
|
|
utils$1.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));
|
|
|
|
if (utils$1.isPlainObject(header) || header instanceof this.constructor) {
|
|
setHeaders(header, valueOrRewrite);
|
|
} else if(utils$1.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
|
|
setHeaders(parseHeaders(header), valueOrRewrite);
|
|
} else if (utils$1.isObject(header) && utils$1.isIterable(header)) {
|
|
let obj = {}, dest, key;
|
|
for (const entry of header) {
|
|
if (!utils$1.isArray(entry)) {
|
|
throw TypeError('Object iterator must return a key-value pair');
|
|
}
|
|
|
|
obj[key = entry[0]] = (dest = obj[key]) ?
|
|
(utils$1.isArray(dest) ? [...dest, entry[1]] : [dest, entry[1]]) : entry[1];
|
|
}
|
|
|
|
setHeaders(obj, valueOrRewrite);
|
|
} else {
|
|
header != null && setHeader(valueOrRewrite, header, rewrite);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
get(header, parser) {
|
|
header = normalizeHeader(header);
|
|
|
|
if (header) {
|
|
const key = utils$1.findKey(this, header);
|
|
|
|
if (key) {
|
|
const value = this[key];
|
|
|
|
if (!parser) {
|
|
return value;
|
|
}
|
|
|
|
if (parser === true) {
|
|
return parseTokens(value);
|
|
}
|
|
|
|
if (utils$1.isFunction(parser)) {
|
|
return parser.call(this, value, key);
|
|
}
|
|
|
|
if (utils$1.isRegExp(parser)) {
|
|
return parser.exec(value);
|
|
}
|
|
|
|
throw new TypeError('parser must be boolean|regexp|function');
|
|
}
|
|
}
|
|
}
|
|
|
|
has(header, matcher) {
|
|
header = normalizeHeader(header);
|
|
|
|
if (header) {
|
|
const key = utils$1.findKey(this, header);
|
|
|
|
return !!(key && this[key] !== undefined && (!matcher || matchHeaderValue(this, this[key], key, matcher)));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
delete(header, matcher) {
|
|
const self = this;
|
|
let deleted = false;
|
|
|
|
function deleteHeader(_header) {
|
|
_header = normalizeHeader(_header);
|
|
|
|
if (_header) {
|
|
const key = utils$1.findKey(self, _header);
|
|
|
|
if (key && (!matcher || matchHeaderValue(self, self[key], key, matcher))) {
|
|
delete self[key];
|
|
|
|
deleted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (utils$1.isArray(header)) {
|
|
header.forEach(deleteHeader);
|
|
} else {
|
|
deleteHeader(header);
|
|
}
|
|
|
|
return deleted;
|
|
}
|
|
|
|
clear(matcher) {
|
|
const keys = Object.keys(this);
|
|
let i = keys.length;
|
|
let deleted = false;
|
|
|
|
while (i--) {
|
|
const key = keys[i];
|
|
if(!matcher || matchHeaderValue(this, this[key], key, matcher, true)) {
|
|
delete this[key];
|
|
deleted = true;
|
|
}
|
|
}
|
|
|
|
return deleted;
|
|
}
|
|
|
|
normalize(format) {
|
|
const self = this;
|
|
const headers = {};
|
|
|
|
utils$1.forEach(this, (value, header) => {
|
|
const key = utils$1.findKey(headers, header);
|
|
|
|
if (key) {
|
|
self[key] = normalizeValue(value);
|
|
delete self[header];
|
|
return;
|
|
}
|
|
|
|
const normalized = format ? formatHeader(header) : String(header).trim();
|
|
|
|
if (normalized !== header) {
|
|
delete self[header];
|
|
}
|
|
|
|
self[normalized] = normalizeValue(value);
|
|
|
|
headers[normalized] = true;
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
concat(...targets) {
|
|
return this.constructor.concat(this, ...targets);
|
|
}
|
|
|
|
toJSON(asStrings) {
|
|
const obj = Object.create(null);
|
|
|
|
utils$1.forEach(this, (value, header) => {
|
|
value != null && value !== false && (obj[header] = asStrings && utils$1.isArray(value) ? value.join(', ') : value);
|
|
});
|
|
|
|
return obj;
|
|
}
|
|
|
|
[Symbol.iterator]() {
|
|
return Object.entries(this.toJSON())[Symbol.iterator]();
|
|
}
|
|
|
|
toString() {
|
|
return Object.entries(this.toJSON()).map(([header, value]) => header + ': ' + value).join('\n');
|
|
}
|
|
|
|
getSetCookie() {
|
|
return this.get("set-cookie") || [];
|
|
}
|
|
|
|
get [Symbol.toStringTag]() {
|
|
return 'AxiosHeaders';
|
|
}
|
|
|
|
static from(thing) {
|
|
return thing instanceof this ? thing : new this(thing);
|
|
}
|
|
|
|
static concat(first, ...targets) {
|
|
const computed = new this(first);
|
|
|
|
targets.forEach((target) => computed.set(target));
|
|
|
|
return computed;
|
|
}
|
|
|
|
static accessor(header) {
|
|
const internals = this[$internals] = (this[$internals] = {
|
|
accessors: {}
|
|
});
|
|
|
|
const accessors = internals.accessors;
|
|
const prototype = this.prototype;
|
|
|
|
function defineAccessor(_header) {
|
|
const lHeader = normalizeHeader(_header);
|
|
|
|
if (!accessors[lHeader]) {
|
|
buildAccessors(prototype, _header);
|
|
accessors[lHeader] = true;
|
|
}
|
|
}
|
|
|
|
utils$1.isArray(header) ? header.forEach(defineAccessor) : defineAccessor(header);
|
|
|
|
return this;
|
|
}
|
|
}
|
|
|
|
AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent', 'Authorization']);
|
|
|
|
// reserved names hotfix
|
|
utils$1.reduceDescriptors(AxiosHeaders.prototype, ({value}, key) => {
|
|
let mapped = key[0].toUpperCase() + key.slice(1); // map `set` => `Set`
|
|
return {
|
|
get: () => value,
|
|
set(headerValue) {
|
|
this[mapped] = headerValue;
|
|
}
|
|
}
|
|
});
|
|
|
|
utils$1.freezeMethods(AxiosHeaders);
|
|
|
|
/**
|
|
* Transform the data for a request or a response
|
|
*
|
|
* @param {Array|Function} fns A single function or Array of functions
|
|
* @param {?Object} response The response object
|
|
*
|
|
* @returns {*} The resulting transformed data
|
|
*/
|
|
function transformData(fns, response) {
|
|
const config = this || defaults;
|
|
const context = response || config;
|
|
const headers = AxiosHeaders.from(context.headers);
|
|
let data = context.data;
|
|
|
|
utils$1.forEach(fns, function transform(fn) {
|
|
data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
|
|
});
|
|
|
|
headers.normalize();
|
|
|
|
return data;
|
|
}
|
|
|
|
function isCancel(value) {
|
|
return !!(value && value.__CANCEL__);
|
|
}
|
|
|
|
/**
|
|
* A `CanceledError` is an object that is thrown when an operation is canceled.
|
|
*
|
|
* @param {string=} message The message.
|
|
* @param {Object=} config The config.
|
|
* @param {Object=} request The request.
|
|
*
|
|
* @returns {CanceledError} The created error.
|
|
*/
|
|
function CanceledError(message, config, request) {
|
|
// eslint-disable-next-line no-eq-null,eqeqeq
|
|
AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request);
|
|
this.name = 'CanceledError';
|
|
}
|
|
|
|
utils$1.inherits(CanceledError, AxiosError, {
|
|
__CANCEL__: true
|
|
});
|
|
|
|
/**
|
|
* Resolve or reject a Promise based on response status.
|
|
*
|
|
* @param {Function} resolve A function that resolves the promise.
|
|
* @param {Function} reject A function that rejects the promise.
|
|
* @param {object} response The response.
|
|
*
|
|
* @returns {object} The response.
|
|
*/
|
|
function settle(resolve, reject, response) {
|
|
const validateStatus = response.config.validateStatus;
|
|
if (!response.status || !validateStatus || validateStatus(response.status)) {
|
|
resolve(response);
|
|
} else {
|
|
reject(new AxiosError(
|
|
'Request failed with status code ' + response.status,
|
|
[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
|
|
response.config,
|
|
response.request,
|
|
response
|
|
));
|
|
}
|
|
}
|
|
|
|
function parseProtocol(url) {
|
|
const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url);
|
|
return match && match[1] || '';
|
|
}
|
|
|
|
/**
|
|
* Calculate data maxRate
|
|
* @param {Number} [samplesCount= 10]
|
|
* @param {Number} [min= 1000]
|
|
* @returns {Function}
|
|
*/
|
|
function speedometer(samplesCount, min) {
|
|
samplesCount = samplesCount || 10;
|
|
const bytes = new Array(samplesCount);
|
|
const timestamps = new Array(samplesCount);
|
|
let head = 0;
|
|
let tail = 0;
|
|
let firstSampleTS;
|
|
|
|
min = min !== undefined ? min : 1000;
|
|
|
|
return function push(chunkLength) {
|
|
const now = Date.now();
|
|
|
|
const startedAt = timestamps[tail];
|
|
|
|
if (!firstSampleTS) {
|
|
firstSampleTS = now;
|
|
}
|
|
|
|
bytes[head] = chunkLength;
|
|
timestamps[head] = now;
|
|
|
|
let i = tail;
|
|
let bytesCount = 0;
|
|
|
|
while (i !== head) {
|
|
bytesCount += bytes[i++];
|
|
i = i % samplesCount;
|
|
}
|
|
|
|
head = (head + 1) % samplesCount;
|
|
|
|
if (head === tail) {
|
|
tail = (tail + 1) % samplesCount;
|
|
}
|
|
|
|
if (now - firstSampleTS < min) {
|
|
return;
|
|
}
|
|
|
|
const passed = startedAt && now - startedAt;
|
|
|
|
return passed ? Math.round(bytesCount * 1000 / passed) : undefined;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Throttle decorator
|
|
* @param {Function} fn
|
|
* @param {Number} freq
|
|
* @return {Function}
|
|
*/
|
|
function throttle(fn, freq) {
|
|
let timestamp = 0;
|
|
let threshold = 1000 / freq;
|
|
let lastArgs;
|
|
let timer;
|
|
|
|
const invoke = (args, now = Date.now()) => {
|
|
timestamp = now;
|
|
lastArgs = null;
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
fn(...args);
|
|
};
|
|
|
|
const throttled = (...args) => {
|
|
const now = Date.now();
|
|
const passed = now - timestamp;
|
|
if ( passed >= threshold) {
|
|
invoke(args, now);
|
|
} else {
|
|
lastArgs = args;
|
|
if (!timer) {
|
|
timer = setTimeout(() => {
|
|
timer = null;
|
|
invoke(lastArgs);
|
|
}, threshold - passed);
|
|
}
|
|
}
|
|
};
|
|
|
|
const flush = () => lastArgs && invoke(lastArgs);
|
|
|
|
return [throttled, flush];
|
|
}
|
|
|
|
const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
|
|
let bytesNotified = 0;
|
|
const _speedometer = speedometer(50, 250);
|
|
|
|
return throttle(e => {
|
|
const loaded = e.loaded;
|
|
const total = e.lengthComputable ? e.total : undefined;
|
|
const progressBytes = loaded - bytesNotified;
|
|
const rate = _speedometer(progressBytes);
|
|
const inRange = loaded <= total;
|
|
|
|
bytesNotified = loaded;
|
|
|
|
const data = {
|
|
loaded,
|
|
total,
|
|
progress: total ? (loaded / total) : undefined,
|
|
bytes: progressBytes,
|
|
rate: rate ? rate : undefined,
|
|
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
|
|
event: e,
|
|
lengthComputable: total != null,
|
|
[isDownloadStream ? 'download' : 'upload']: true
|
|
};
|
|
|
|
listener(data);
|
|
}, freq);
|
|
};
|
|
|
|
const progressEventDecorator = (total, throttled) => {
|
|
const lengthComputable = total != null;
|
|
|
|
return [(loaded) => throttled[0]({
|
|
lengthComputable,
|
|
total,
|
|
loaded
|
|
}), throttled[1]];
|
|
};
|
|
|
|
const asyncDecorator = (fn) => (...args) => utils$1.asap(() => fn(...args));
|
|
|
|
const isURLSameOrigin = platform.hasStandardBrowserEnv ? ((origin, isMSIE) => (url) => {
|
|
url = new URL(url, platform.origin);
|
|
|
|
return (
|
|
origin.protocol === url.protocol &&
|
|
origin.host === url.host &&
|
|
(isMSIE || origin.port === url.port)
|
|
);
|
|
})(
|
|
new URL(platform.origin),
|
|
platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)
|
|
) : () => true;
|
|
|
|
const cookies = platform.hasStandardBrowserEnv ?
|
|
|
|
// Standard browser envs support document.cookie
|
|
{
|
|
write(name, value, expires, path, domain, secure) {
|
|
const cookie = [name + '=' + encodeURIComponent(value)];
|
|
|
|
utils$1.isNumber(expires) && cookie.push('expires=' + new Date(expires).toGMTString());
|
|
|
|
utils$1.isString(path) && cookie.push('path=' + path);
|
|
|
|
utils$1.isString(domain) && cookie.push('domain=' + domain);
|
|
|
|
secure === true && cookie.push('secure');
|
|
|
|
document.cookie = cookie.join('; ');
|
|
},
|
|
|
|
read(name) {
|
|
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
|
|
return (match ? decodeURIComponent(match[3]) : null);
|
|
},
|
|
|
|
remove(name) {
|
|
this.write(name, '', Date.now() - 86400000);
|
|
}
|
|
}
|
|
|
|
:
|
|
|
|
// Non-standard browser env (web workers, react-native) lack needed support.
|
|
{
|
|
write() {},
|
|
read() {
|
|
return null;
|
|
},
|
|
remove() {}
|
|
};
|
|
|
|
/**
|
|
* Determines whether the specified URL is absolute
|
|
*
|
|
* @param {string} url The URL to test
|
|
*
|
|
* @returns {boolean} True if the specified URL is absolute, otherwise false
|
|
*/
|
|
function isAbsoluteURL(url) {
|
|
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
|
|
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
|
|
// by any combination of letters, digits, plus, period, or hyphen.
|
|
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
|
}
|
|
|
|
/**
|
|
* Creates a new URL by combining the specified URLs
|
|
*
|
|
* @param {string} baseURL The base URL
|
|
* @param {string} relativeURL The relative URL
|
|
*
|
|
* @returns {string} The combined URL
|
|
*/
|
|
function combineURLs(baseURL, relativeURL) {
|
|
return relativeURL
|
|
? baseURL.replace(/\/?\/$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
|
: baseURL;
|
|
}
|
|
|
|
/**
|
|
* Creates a new URL by combining the baseURL with the requestedURL,
|
|
* only when the requestedURL is not already an absolute URL.
|
|
* If the requestURL is absolute, this function returns the requestedURL untouched.
|
|
*
|
|
* @param {string} baseURL The base URL
|
|
* @param {string} requestedURL Absolute or relative URL to combine
|
|
*
|
|
* @returns {string} The combined full path
|
|
*/
|
|
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
|
|
let isRelativeUrl = !isAbsoluteURL(requestedURL);
|
|
if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) {
|
|
return combineURLs(baseURL, requestedURL);
|
|
}
|
|
return requestedURL;
|
|
}
|
|
|
|
const headersToObject = (thing) => thing instanceof AxiosHeaders ? { ...thing } : thing;
|
|
|
|
/**
|
|
* Config-specific merge-function which creates a new config-object
|
|
* by merging two configuration objects together.
|
|
*
|
|
* @param {Object} config1
|
|
* @param {Object} config2
|
|
*
|
|
* @returns {Object} New object resulting from merging config2 to config1
|
|
*/
|
|
function mergeConfig(config1, config2) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
config2 = config2 || {};
|
|
const config = {};
|
|
|
|
function getMergedValue(target, source, prop, caseless) {
|
|
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
|
return utils$1.merge.call({caseless}, target, source);
|
|
} else if (utils$1.isPlainObject(source)) {
|
|
return utils$1.merge({}, source);
|
|
} else if (utils$1.isArray(source)) {
|
|
return source.slice();
|
|
}
|
|
return source;
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function mergeDeepProperties(a, b, prop , caseless) {
|
|
if (!utils$1.isUndefined(b)) {
|
|
return getMergedValue(a, b, prop , caseless);
|
|
} else if (!utils$1.isUndefined(a)) {
|
|
return getMergedValue(undefined, a, prop , caseless);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function valueFromConfig2(a, b) {
|
|
if (!utils$1.isUndefined(b)) {
|
|
return getMergedValue(undefined, b);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function defaultToConfig2(a, b) {
|
|
if (!utils$1.isUndefined(b)) {
|
|
return getMergedValue(undefined, b);
|
|
} else if (!utils$1.isUndefined(a)) {
|
|
return getMergedValue(undefined, a);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function mergeDirectKeys(a, b, prop) {
|
|
if (prop in config2) {
|
|
return getMergedValue(a, b);
|
|
} else if (prop in config1) {
|
|
return getMergedValue(undefined, a);
|
|
}
|
|
}
|
|
|
|
const mergeMap = {
|
|
url: valueFromConfig2,
|
|
method: valueFromConfig2,
|
|
data: valueFromConfig2,
|
|
baseURL: defaultToConfig2,
|
|
transformRequest: defaultToConfig2,
|
|
transformResponse: defaultToConfig2,
|
|
paramsSerializer: defaultToConfig2,
|
|
timeout: defaultToConfig2,
|
|
timeoutMessage: defaultToConfig2,
|
|
withCredentials: defaultToConfig2,
|
|
withXSRFToken: defaultToConfig2,
|
|
adapter: defaultToConfig2,
|
|
responseType: defaultToConfig2,
|
|
xsrfCookieName: defaultToConfig2,
|
|
xsrfHeaderName: defaultToConfig2,
|
|
onUploadProgress: defaultToConfig2,
|
|
onDownloadProgress: defaultToConfig2,
|
|
decompress: defaultToConfig2,
|
|
maxContentLength: defaultToConfig2,
|
|
maxBodyLength: defaultToConfig2,
|
|
beforeRedirect: defaultToConfig2,
|
|
transport: defaultToConfig2,
|
|
httpAgent: defaultToConfig2,
|
|
httpsAgent: defaultToConfig2,
|
|
cancelToken: defaultToConfig2,
|
|
socketPath: defaultToConfig2,
|
|
responseEncoding: defaultToConfig2,
|
|
validateStatus: mergeDirectKeys,
|
|
headers: (a, b , prop) => mergeDeepProperties(headersToObject(a), headersToObject(b),prop, true)
|
|
};
|
|
|
|
utils$1.forEach(Object.keys({...config1, ...config2}), function computeConfigValue(prop) {
|
|
const merge = mergeMap[prop] || mergeDeepProperties;
|
|
const configValue = merge(config1[prop], config2[prop], prop);
|
|
(utils$1.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
const resolveConfig = (config) => {
|
|
const newConfig = mergeConfig({}, config);
|
|
|
|
let {data, withXSRFToken, xsrfHeaderName, xsrfCookieName, headers, auth} = newConfig;
|
|
|
|
newConfig.headers = headers = AxiosHeaders.from(headers);
|
|
|
|
newConfig.url = buildURL(buildFullPath(newConfig.baseURL, newConfig.url, newConfig.allowAbsoluteUrls), config.params, config.paramsSerializer);
|
|
|
|
// HTTP basic authentication
|
|
if (auth) {
|
|
headers.set('Authorization', 'Basic ' +
|
|
btoa((auth.username || '') + ':' + (auth.password ? unescape(encodeURIComponent(auth.password)) : ''))
|
|
);
|
|
}
|
|
|
|
let contentType;
|
|
|
|
if (utils$1.isFormData(data)) {
|
|
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
|
|
headers.setContentType(undefined); // Let the browser set it
|
|
} else if ((contentType = headers.getContentType()) !== false) {
|
|
// fix semicolon duplication issue for ReactNative FormData implementation
|
|
const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
|
|
headers.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
|
|
}
|
|
}
|
|
|
|
// Add xsrf header
|
|
// This is only done if running in a standard browser environment.
|
|
// Specifically not if we're in a web worker, or react-native.
|
|
|
|
if (platform.hasStandardBrowserEnv) {
|
|
withXSRFToken && utils$1.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig));
|
|
|
|
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) {
|
|
// Add xsrf header
|
|
const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
|
|
|
|
if (xsrfValue) {
|
|
headers.set(xsrfHeaderName, xsrfValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
return newConfig;
|
|
};
|
|
|
|
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
|
|
|
|
const xhrAdapter = isXHRAdapterSupported && function (config) {
|
|
return new Promise(function dispatchXhrRequest(resolve, reject) {
|
|
const _config = resolveConfig(config);
|
|
let requestData = _config.data;
|
|
const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
|
|
let {responseType, onUploadProgress, onDownloadProgress} = _config;
|
|
let onCanceled;
|
|
let uploadThrottled, downloadThrottled;
|
|
let flushUpload, flushDownload;
|
|
|
|
function done() {
|
|
flushUpload && flushUpload(); // flush events
|
|
flushDownload && flushDownload(); // flush events
|
|
|
|
_config.cancelToken && _config.cancelToken.unsubscribe(onCanceled);
|
|
|
|
_config.signal && _config.signal.removeEventListener('abort', onCanceled);
|
|
}
|
|
|
|
let request = new XMLHttpRequest();
|
|
|
|
request.open(_config.method.toUpperCase(), _config.url, true);
|
|
|
|
// Set the request timeout in MS
|
|
request.timeout = _config.timeout;
|
|
|
|
function onloadend() {
|
|
if (!request) {
|
|
return;
|
|
}
|
|
// Prepare the response
|
|
const responseHeaders = AxiosHeaders.from(
|
|
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
|
|
);
|
|
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
|
|
request.responseText : request.response;
|
|
const response = {
|
|
data: responseData,
|
|
status: request.status,
|
|
statusText: request.statusText,
|
|
headers: responseHeaders,
|
|
config,
|
|
request
|
|
};
|
|
|
|
settle(function _resolve(value) {
|
|
resolve(value);
|
|
done();
|
|
}, function _reject(err) {
|
|
reject(err);
|
|
done();
|
|
}, response);
|
|
|
|
// Clean up request
|
|
request = null;
|
|
}
|
|
|
|
if ('onloadend' in request) {
|
|
// Use onloadend if available
|
|
request.onloadend = onloadend;
|
|
} else {
|
|
// Listen for ready state to emulate onloadend
|
|
request.onreadystatechange = function handleLoad() {
|
|
if (!request || request.readyState !== 4) {
|
|
return;
|
|
}
|
|
|
|
// The request errored out and we didn't get a response, this will be
|
|
// handled by onerror instead
|
|
// With one exception: request that using file: protocol, most browsers
|
|
// will return status as 0 even though it's a successful request
|
|
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
|
|
return;
|
|
}
|
|
// readystate handler is calling before onerror or ontimeout handlers,
|
|
// so we should call onloadend on the next 'tick'
|
|
setTimeout(onloadend);
|
|
};
|
|
}
|
|
|
|
// Handle browser request cancellation (as opposed to a manual cancellation)
|
|
request.onabort = function handleAbort() {
|
|
if (!request) {
|
|
return;
|
|
}
|
|
|
|
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
|
|
|
|
// Clean up request
|
|
request = null;
|
|
};
|
|
|
|
// Handle low level network errors
|
|
request.onerror = function handleError() {
|
|
// Real errors are hidden from us by the browser
|
|
// onerror should only fire if it's a network error
|
|
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
|
|
|
|
// Clean up request
|
|
request = null;
|
|
};
|
|
|
|
// Handle timeout
|
|
request.ontimeout = function handleTimeout() {
|
|
let timeoutErrorMessage = _config.timeout ? 'timeout of ' + _config.timeout + 'ms exceeded' : 'timeout exceeded';
|
|
const transitional = _config.transitional || transitionalDefaults;
|
|
if (_config.timeoutErrorMessage) {
|
|
timeoutErrorMessage = _config.timeoutErrorMessage;
|
|
}
|
|
reject(new AxiosError(
|
|
timeoutErrorMessage,
|
|
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
|
|
config,
|
|
request));
|
|
|
|
// Clean up request
|
|
request = null;
|
|
};
|
|
|
|
// Remove Content-Type if data is undefined
|
|
requestData === undefined && requestHeaders.setContentType(null);
|
|
|
|
// Add headers to the request
|
|
if ('setRequestHeader' in request) {
|
|
utils$1.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
|
|
request.setRequestHeader(key, val);
|
|
});
|
|
}
|
|
|
|
// Add withCredentials to request if needed
|
|
if (!utils$1.isUndefined(_config.withCredentials)) {
|
|
request.withCredentials = !!_config.withCredentials;
|
|
}
|
|
|
|
// Add responseType to request if needed
|
|
if (responseType && responseType !== 'json') {
|
|
request.responseType = _config.responseType;
|
|
}
|
|
|
|
// Handle progress if needed
|
|
if (onDownloadProgress) {
|
|
([downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true));
|
|
request.addEventListener('progress', downloadThrottled);
|
|
}
|
|
|
|
// Not all browsers support upload events
|
|
if (onUploadProgress && request.upload) {
|
|
([uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress));
|
|
|
|
request.upload.addEventListener('progress', uploadThrottled);
|
|
|
|
request.upload.addEventListener('loadend', flushUpload);
|
|
}
|
|
|
|
if (_config.cancelToken || _config.signal) {
|
|
// Handle cancellation
|
|
// eslint-disable-next-line func-names
|
|
onCanceled = cancel => {
|
|
if (!request) {
|
|
return;
|
|
}
|
|
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
|
|
request.abort();
|
|
request = null;
|
|
};
|
|
|
|
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
|
|
if (_config.signal) {
|
|
_config.signal.aborted ? onCanceled() : _config.signal.addEventListener('abort', onCanceled);
|
|
}
|
|
}
|
|
|
|
const protocol = parseProtocol(_config.url);
|
|
|
|
if (protocol && platform.protocols.indexOf(protocol) === -1) {
|
|
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
|
|
return;
|
|
}
|
|
|
|
|
|
// Send the request
|
|
request.send(requestData || null);
|
|
});
|
|
};
|
|
|
|
const composeSignals = (signals, timeout) => {
|
|
const {length} = (signals = signals ? signals.filter(Boolean) : []);
|
|
|
|
if (timeout || length) {
|
|
let controller = new AbortController();
|
|
|
|
let aborted;
|
|
|
|
const onabort = function (reason) {
|
|
if (!aborted) {
|
|
aborted = true;
|
|
unsubscribe();
|
|
const err = reason instanceof Error ? reason : this.reason;
|
|
controller.abort(err instanceof AxiosError ? err : new CanceledError(err instanceof Error ? err.message : err));
|
|
}
|
|
};
|
|
|
|
let timer = timeout && setTimeout(() => {
|
|
timer = null;
|
|
onabort(new AxiosError(`timeout ${timeout} of ms exceeded`, AxiosError.ETIMEDOUT));
|
|
}, timeout);
|
|
|
|
const unsubscribe = () => {
|
|
if (signals) {
|
|
timer && clearTimeout(timer);
|
|
timer = null;
|
|
signals.forEach(signal => {
|
|
signal.unsubscribe ? signal.unsubscribe(onabort) : signal.removeEventListener('abort', onabort);
|
|
});
|
|
signals = null;
|
|
}
|
|
};
|
|
|
|
signals.forEach((signal) => signal.addEventListener('abort', onabort));
|
|
|
|
const {signal} = controller;
|
|
|
|
signal.unsubscribe = () => utils$1.asap(unsubscribe);
|
|
|
|
return signal;
|
|
}
|
|
};
|
|
|
|
const streamChunk = function* (chunk, chunkSize) {
|
|
let len = chunk.byteLength;
|
|
|
|
if (len < chunkSize) {
|
|
yield chunk;
|
|
return;
|
|
}
|
|
|
|
let pos = 0;
|
|
let end;
|
|
|
|
while (pos < len) {
|
|
end = pos + chunkSize;
|
|
yield chunk.slice(pos, end);
|
|
pos = end;
|
|
}
|
|
};
|
|
|
|
const readBytes = async function* (iterable, chunkSize) {
|
|
for await (const chunk of readStream(iterable)) {
|
|
yield* streamChunk(chunk, chunkSize);
|
|
}
|
|
};
|
|
|
|
const readStream = async function* (stream) {
|
|
if (stream[Symbol.asyncIterator]) {
|
|
yield* stream;
|
|
return;
|
|
}
|
|
|
|
const reader = stream.getReader();
|
|
try {
|
|
for (;;) {
|
|
const {done, value} = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
yield value;
|
|
}
|
|
} finally {
|
|
await reader.cancel();
|
|
}
|
|
};
|
|
|
|
const trackStream = (stream, chunkSize, onProgress, onFinish) => {
|
|
const iterator = readBytes(stream, chunkSize);
|
|
|
|
let bytes = 0;
|
|
let done;
|
|
let _onFinish = (e) => {
|
|
if (!done) {
|
|
done = true;
|
|
onFinish && onFinish(e);
|
|
}
|
|
};
|
|
|
|
return new ReadableStream({
|
|
async pull(controller) {
|
|
try {
|
|
const {done, value} = await iterator.next();
|
|
|
|
if (done) {
|
|
_onFinish();
|
|
controller.close();
|
|
return;
|
|
}
|
|
|
|
let len = value.byteLength;
|
|
if (onProgress) {
|
|
let loadedBytes = bytes += len;
|
|
onProgress(loadedBytes);
|
|
}
|
|
controller.enqueue(new Uint8Array(value));
|
|
} catch (err) {
|
|
_onFinish(err);
|
|
throw err;
|
|
}
|
|
},
|
|
cancel(reason) {
|
|
_onFinish(reason);
|
|
return iterator.return();
|
|
}
|
|
}, {
|
|
highWaterMark: 2
|
|
})
|
|
};
|
|
|
|
const isFetchSupported = typeof fetch === 'function' && typeof Request === 'function' && typeof Response === 'function';
|
|
const isReadableStreamSupported = isFetchSupported && typeof ReadableStream === 'function';
|
|
|
|
// used only inside the fetch adapter
|
|
const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ?
|
|
((encoder) => (str) => encoder.encode(str))(new TextEncoder()) :
|
|
async (str) => new Uint8Array(await new Response(str).arrayBuffer())
|
|
);
|
|
|
|
const test = (fn, ...args) => {
|
|
try {
|
|
return !!fn(...args);
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
};
|
|
|
|
const supportsRequestStream = isReadableStreamSupported && test(() => {
|
|
let duplexAccessed = false;
|
|
|
|
const hasContentType = new Request(platform.origin, {
|
|
body: new ReadableStream(),
|
|
method: 'POST',
|
|
get duplex() {
|
|
duplexAccessed = true;
|
|
return 'half';
|
|
},
|
|
}).headers.has('Content-Type');
|
|
|
|
return duplexAccessed && !hasContentType;
|
|
});
|
|
|
|
const DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
|
|
const supportsResponseStream = isReadableStreamSupported &&
|
|
test(() => utils$1.isReadableStream(new Response('').body));
|
|
|
|
|
|
const resolvers = {
|
|
stream: supportsResponseStream && ((res) => res.body)
|
|
};
|
|
|
|
isFetchSupported && (((res) => {
|
|
['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach(type => {
|
|
!resolvers[type] && (resolvers[type] = utils$1.isFunction(res[type]) ? (res) => res[type]() :
|
|
(_, config) => {
|
|
throw new AxiosError(`Response type '${type}' is not supported`, AxiosError.ERR_NOT_SUPPORT, config);
|
|
});
|
|
});
|
|
})(new Response));
|
|
|
|
const getBodyLength = async (body) => {
|
|
if (body == null) {
|
|
return 0;
|
|
}
|
|
|
|
if(utils$1.isBlob(body)) {
|
|
return body.size;
|
|
}
|
|
|
|
if(utils$1.isSpecCompliantForm(body)) {
|
|
const _request = new Request(platform.origin, {
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
return (await _request.arrayBuffer()).byteLength;
|
|
}
|
|
|
|
if(utils$1.isArrayBufferView(body) || utils$1.isArrayBuffer(body)) {
|
|
return body.byteLength;
|
|
}
|
|
|
|
if(utils$1.isURLSearchParams(body)) {
|
|
body = body + '';
|
|
}
|
|
|
|
if(utils$1.isString(body)) {
|
|
return (await encodeText(body)).byteLength;
|
|
}
|
|
};
|
|
|
|
const resolveBodyLength = async (headers, body) => {
|
|
const length = utils$1.toFiniteNumber(headers.getContentLength());
|
|
|
|
return length == null ? getBodyLength(body) : length;
|
|
};
|
|
|
|
const fetchAdapter = isFetchSupported && (async (config) => {
|
|
let {
|
|
url,
|
|
method,
|
|
data,
|
|
signal,
|
|
cancelToken,
|
|
timeout,
|
|
onDownloadProgress,
|
|
onUploadProgress,
|
|
responseType,
|
|
headers,
|
|
withCredentials = 'same-origin',
|
|
fetchOptions
|
|
} = resolveConfig(config);
|
|
|
|
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
|
|
|
|
let composedSignal = composeSignals([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
|
|
|
|
let request;
|
|
|
|
const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => {
|
|
composedSignal.unsubscribe();
|
|
});
|
|
|
|
let requestContentLength;
|
|
|
|
try {
|
|
if (
|
|
onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head' &&
|
|
(requestContentLength = await resolveBodyLength(headers, data)) !== 0
|
|
) {
|
|
let _request = new Request(url, {
|
|
method: 'POST',
|
|
body: data,
|
|
duplex: "half"
|
|
});
|
|
|
|
let contentTypeHeader;
|
|
|
|
if (utils$1.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
|
|
headers.setContentType(contentTypeHeader);
|
|
}
|
|
|
|
if (_request.body) {
|
|
const [onProgress, flush] = progressEventDecorator(
|
|
requestContentLength,
|
|
progressEventReducer(asyncDecorator(onUploadProgress))
|
|
);
|
|
|
|
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
|
|
}
|
|
}
|
|
|
|
if (!utils$1.isString(withCredentials)) {
|
|
withCredentials = withCredentials ? 'include' : 'omit';
|
|
}
|
|
|
|
// Cloudflare Workers throws when credentials are defined
|
|
// see https://github.com/cloudflare/workerd/issues/902
|
|
const isCredentialsSupported = "credentials" in Request.prototype;
|
|
request = new Request(url, {
|
|
...fetchOptions,
|
|
signal: composedSignal,
|
|
method: method.toUpperCase(),
|
|
headers: headers.normalize().toJSON(),
|
|
body: data,
|
|
duplex: "half",
|
|
credentials: isCredentialsSupported ? withCredentials : undefined
|
|
});
|
|
|
|
let response = await fetch(request, fetchOptions);
|
|
|
|
const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
|
|
|
|
if (supportsResponseStream && (onDownloadProgress || (isStreamResponse && unsubscribe))) {
|
|
const options = {};
|
|
|
|
['status', 'statusText', 'headers'].forEach(prop => {
|
|
options[prop] = response[prop];
|
|
});
|
|
|
|
const responseContentLength = utils$1.toFiniteNumber(response.headers.get('content-length'));
|
|
|
|
const [onProgress, flush] = onDownloadProgress && progressEventDecorator(
|
|
responseContentLength,
|
|
progressEventReducer(asyncDecorator(onDownloadProgress), true)
|
|
) || [];
|
|
|
|
response = new Response(
|
|
trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
|
|
flush && flush();
|
|
unsubscribe && unsubscribe();
|
|
}),
|
|
options
|
|
);
|
|
}
|
|
|
|
responseType = responseType || 'text';
|
|
|
|
let responseData = await resolvers[utils$1.findKey(resolvers, responseType) || 'text'](response, config);
|
|
|
|
!isStreamResponse && unsubscribe && unsubscribe();
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
settle(resolve, reject, {
|
|
data: responseData,
|
|
headers: AxiosHeaders.from(response.headers),
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
config,
|
|
request
|
|
});
|
|
})
|
|
} catch (err) {
|
|
unsubscribe && unsubscribe();
|
|
|
|
if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
|
|
throw Object.assign(
|
|
new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request),
|
|
{
|
|
cause: err.cause || err
|
|
}
|
|
)
|
|
}
|
|
|
|
throw AxiosError.from(err, err && err.code, config, request);
|
|
}
|
|
});
|
|
|
|
const knownAdapters = {
|
|
http: httpAdapter,
|
|
xhr: xhrAdapter,
|
|
fetch: fetchAdapter
|
|
};
|
|
|
|
utils$1.forEach(knownAdapters, (fn, value) => {
|
|
if (fn) {
|
|
try {
|
|
Object.defineProperty(fn, 'name', {value});
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-empty
|
|
}
|
|
Object.defineProperty(fn, 'adapterName', {value});
|
|
}
|
|
});
|
|
|
|
const renderReason = (reason) => `- ${reason}`;
|
|
|
|
const isResolvedHandle = (adapter) => utils$1.isFunction(adapter) || adapter === null || adapter === false;
|
|
|
|
const adapters = {
|
|
getAdapter: (adapters) => {
|
|
adapters = utils$1.isArray(adapters) ? adapters : [adapters];
|
|
|
|
const {length} = adapters;
|
|
let nameOrAdapter;
|
|
let adapter;
|
|
|
|
const rejectedReasons = {};
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
nameOrAdapter = adapters[i];
|
|
let id;
|
|
|
|
adapter = nameOrAdapter;
|
|
|
|
if (!isResolvedHandle(nameOrAdapter)) {
|
|
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
|
|
|
|
if (adapter === undefined) {
|
|
throw new AxiosError(`Unknown adapter '${id}'`);
|
|
}
|
|
}
|
|
|
|
if (adapter) {
|
|
break;
|
|
}
|
|
|
|
rejectedReasons[id || '#' + i] = adapter;
|
|
}
|
|
|
|
if (!adapter) {
|
|
|
|
const reasons = Object.entries(rejectedReasons)
|
|
.map(([id, state]) => `adapter ${id} ` +
|
|
(state === false ? 'is not supported by the environment' : 'is not available in the build')
|
|
);
|
|
|
|
let s = length ?
|
|
(reasons.length > 1 ? 'since :\n' + reasons.map(renderReason).join('\n') : ' ' + renderReason(reasons[0])) :
|
|
'as no adapter specified';
|
|
|
|
throw new AxiosError(
|
|
`There is no suitable adapter to dispatch the request ` + s,
|
|
'ERR_NOT_SUPPORT'
|
|
);
|
|
}
|
|
|
|
return adapter;
|
|
},
|
|
adapters: knownAdapters
|
|
};
|
|
|
|
/**
|
|
* Throws a `CanceledError` if cancellation has been requested.
|
|
*
|
|
* @param {Object} config The config that is to be used for the request
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function throwIfCancellationRequested(config) {
|
|
if (config.cancelToken) {
|
|
config.cancelToken.throwIfRequested();
|
|
}
|
|
|
|
if (config.signal && config.signal.aborted) {
|
|
throw new CanceledError(null, config);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch a request to the server using the configured adapter.
|
|
*
|
|
* @param {object} config The config that is to be used for the request
|
|
*
|
|
* @returns {Promise} The Promise to be fulfilled
|
|
*/
|
|
function dispatchRequest(config) {
|
|
throwIfCancellationRequested(config);
|
|
|
|
config.headers = AxiosHeaders.from(config.headers);
|
|
|
|
// Transform request data
|
|
config.data = transformData.call(
|
|
config,
|
|
config.transformRequest
|
|
);
|
|
|
|
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
|
|
config.headers.setContentType('application/x-www-form-urlencoded', false);
|
|
}
|
|
|
|
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
|
|
|
|
return adapter(config).then(function onAdapterResolution(response) {
|
|
throwIfCancellationRequested(config);
|
|
|
|
// Transform response data
|
|
response.data = transformData.call(
|
|
config,
|
|
config.transformResponse,
|
|
response
|
|
);
|
|
|
|
response.headers = AxiosHeaders.from(response.headers);
|
|
|
|
return response;
|
|
}, function onAdapterRejection(reason) {
|
|
if (!isCancel(reason)) {
|
|
throwIfCancellationRequested(config);
|
|
|
|
// Transform response data
|
|
if (reason && reason.response) {
|
|
reason.response.data = transformData.call(
|
|
config,
|
|
config.transformResponse,
|
|
reason.response
|
|
);
|
|
reason.response.headers = AxiosHeaders.from(reason.response.headers);
|
|
}
|
|
}
|
|
|
|
return Promise.reject(reason);
|
|
});
|
|
}
|
|
|
|
const VERSION = "1.11.0";
|
|
|
|
const validators$1 = {};
|
|
|
|
// eslint-disable-next-line func-names
|
|
['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach((type, i) => {
|
|
validators$1[type] = function validator(thing) {
|
|
return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;
|
|
};
|
|
});
|
|
|
|
const deprecatedWarnings = {};
|
|
|
|
/**
|
|
* Transitional option validator
|
|
*
|
|
* @param {function|boolean?} validator - set to false if the transitional option has been removed
|
|
* @param {string?} version - deprecated version / removed since version
|
|
* @param {string?} message - some message with additional info
|
|
*
|
|
* @returns {function}
|
|
*/
|
|
validators$1.transitional = function transitional(validator, version, message) {
|
|
function formatMessage(opt, desc) {
|
|
return '[Axios v' + VERSION + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : '');
|
|
}
|
|
|
|
// eslint-disable-next-line func-names
|
|
return (value, opt, opts) => {
|
|
if (validator === false) {
|
|
throw new AxiosError(
|
|
formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')),
|
|
AxiosError.ERR_DEPRECATED
|
|
);
|
|
}
|
|
|
|
if (version && !deprecatedWarnings[opt]) {
|
|
deprecatedWarnings[opt] = true;
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
formatMessage(
|
|
opt,
|
|
' has been deprecated since v' + version + ' and will be removed in the near future'
|
|
)
|
|
);
|
|
}
|
|
|
|
return validator ? validator(value, opt, opts) : true;
|
|
};
|
|
};
|
|
|
|
validators$1.spelling = function spelling(correctSpelling) {
|
|
return (value, opt) => {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`${opt} is likely a misspelling of ${correctSpelling}`);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Assert object's properties type
|
|
*
|
|
* @param {object} options
|
|
* @param {object} schema
|
|
* @param {boolean?} allowUnknown
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
|
|
function assertOptions(options, schema, allowUnknown) {
|
|
if (typeof options !== 'object') {
|
|
throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE);
|
|
}
|
|
const keys = Object.keys(options);
|
|
let i = keys.length;
|
|
while (i-- > 0) {
|
|
const opt = keys[i];
|
|
const validator = schema[opt];
|
|
if (validator) {
|
|
const value = options[opt];
|
|
const result = value === undefined || validator(value, opt, options);
|
|
if (result !== true) {
|
|
throw new AxiosError('option ' + opt + ' must be ' + result, AxiosError.ERR_BAD_OPTION_VALUE);
|
|
}
|
|
continue;
|
|
}
|
|
if (allowUnknown !== true) {
|
|
throw new AxiosError('Unknown option ' + opt, AxiosError.ERR_BAD_OPTION);
|
|
}
|
|
}
|
|
}
|
|
|
|
const validator = {
|
|
assertOptions,
|
|
validators: validators$1
|
|
};
|
|
|
|
const validators = validator.validators;
|
|
|
|
/**
|
|
* Create a new instance of Axios
|
|
*
|
|
* @param {Object} instanceConfig The default config for the instance
|
|
*
|
|
* @return {Axios} A new instance of Axios
|
|
*/
|
|
class Axios {
|
|
constructor(instanceConfig) {
|
|
this.defaults = instanceConfig || {};
|
|
this.interceptors = {
|
|
request: new InterceptorManager(),
|
|
response: new InterceptorManager()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dispatch a request
|
|
*
|
|
* @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
|
|
* @param {?Object} config
|
|
*
|
|
* @returns {Promise} The Promise to be fulfilled
|
|
*/
|
|
async request(configOrUrl, config) {
|
|
try {
|
|
return await this._request(configOrUrl, config);
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
let dummy = {};
|
|
|
|
Error.captureStackTrace ? Error.captureStackTrace(dummy) : (dummy = new Error());
|
|
|
|
// slice off the Error: ... line
|
|
const stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
|
try {
|
|
if (!err.stack) {
|
|
err.stack = stack;
|
|
// match without the 2 top stack lines
|
|
} else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) {
|
|
err.stack += '\n' + stack;
|
|
}
|
|
} catch (e) {
|
|
// ignore the case where "stack" is an un-writable property
|
|
}
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
_request(configOrUrl, config) {
|
|
/*eslint no-param-reassign:0*/
|
|
// Allow for axios('example/url'[, config]) a la fetch API
|
|
if (typeof configOrUrl === 'string') {
|
|
config = config || {};
|
|
config.url = configOrUrl;
|
|
} else {
|
|
config = configOrUrl || {};
|
|
}
|
|
|
|
config = mergeConfig(this.defaults, config);
|
|
|
|
const {transitional, paramsSerializer, headers} = config;
|
|
|
|
if (transitional !== undefined) {
|
|
validator.assertOptions(transitional, {
|
|
silentJSONParsing: validators.transitional(validators.boolean),
|
|
forcedJSONParsing: validators.transitional(validators.boolean),
|
|
clarifyTimeoutError: validators.transitional(validators.boolean)
|
|
}, false);
|
|
}
|
|
|
|
if (paramsSerializer != null) {
|
|
if (utils$1.isFunction(paramsSerializer)) {
|
|
config.paramsSerializer = {
|
|
serialize: paramsSerializer
|
|
};
|
|
} else {
|
|
validator.assertOptions(paramsSerializer, {
|
|
encode: validators.function,
|
|
serialize: validators.function
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
// Set config.allowAbsoluteUrls
|
|
if (config.allowAbsoluteUrls !== undefined) ; else if (this.defaults.allowAbsoluteUrls !== undefined) {
|
|
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
|
|
} else {
|
|
config.allowAbsoluteUrls = true;
|
|
}
|
|
|
|
validator.assertOptions(config, {
|
|
baseUrl: validators.spelling('baseURL'),
|
|
withXsrfToken: validators.spelling('withXSRFToken')
|
|
}, true);
|
|
|
|
// Set config.method
|
|
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
|
|
|
// Flatten headers
|
|
let contextHeaders = headers && utils$1.merge(
|
|
headers.common,
|
|
headers[config.method]
|
|
);
|
|
|
|
headers && utils$1.forEach(
|
|
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
|
|
(method) => {
|
|
delete headers[method];
|
|
}
|
|
);
|
|
|
|
config.headers = AxiosHeaders.concat(contextHeaders, headers);
|
|
|
|
// filter out skipped interceptors
|
|
const requestInterceptorChain = [];
|
|
let synchronousRequestInterceptors = true;
|
|
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
|
|
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
|
|
return;
|
|
}
|
|
|
|
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
|
|
|
|
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
|
|
});
|
|
|
|
const responseInterceptorChain = [];
|
|
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
|
|
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
|
|
});
|
|
|
|
let promise;
|
|
let i = 0;
|
|
let len;
|
|
|
|
if (!synchronousRequestInterceptors) {
|
|
const chain = [dispatchRequest.bind(this), undefined];
|
|
chain.unshift(...requestInterceptorChain);
|
|
chain.push(...responseInterceptorChain);
|
|
len = chain.length;
|
|
|
|
promise = Promise.resolve(config);
|
|
|
|
while (i < len) {
|
|
promise = promise.then(chain[i++], chain[i++]);
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
len = requestInterceptorChain.length;
|
|
|
|
let newConfig = config;
|
|
|
|
i = 0;
|
|
|
|
while (i < len) {
|
|
const onFulfilled = requestInterceptorChain[i++];
|
|
const onRejected = requestInterceptorChain[i++];
|
|
try {
|
|
newConfig = onFulfilled(newConfig);
|
|
} catch (error) {
|
|
onRejected.call(this, error);
|
|
break;
|
|
}
|
|
}
|
|
|
|
try {
|
|
promise = dispatchRequest.call(this, newConfig);
|
|
} catch (error) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
i = 0;
|
|
len = responseInterceptorChain.length;
|
|
|
|
while (i < len) {
|
|
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
getUri(config) {
|
|
config = mergeConfig(this.defaults, config);
|
|
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
|
return buildURL(fullPath, config.params, config.paramsSerializer);
|
|
}
|
|
}
|
|
|
|
// Provide aliases for supported request methods
|
|
utils$1.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
|
|
/*eslint func-names:0*/
|
|
Axios.prototype[method] = function(url, config) {
|
|
return this.request(mergeConfig(config || {}, {
|
|
method,
|
|
url,
|
|
data: (config || {}).data
|
|
}));
|
|
};
|
|
});
|
|
|
|
utils$1.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
|
|
/*eslint func-names:0*/
|
|
|
|
function generateHTTPMethod(isForm) {
|
|
return function httpMethod(url, data, config) {
|
|
return this.request(mergeConfig(config || {}, {
|
|
method,
|
|
headers: isForm ? {
|
|
'Content-Type': 'multipart/form-data'
|
|
} : {},
|
|
url,
|
|
data
|
|
}));
|
|
};
|
|
}
|
|
|
|
Axios.prototype[method] = generateHTTPMethod();
|
|
|
|
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
|
|
});
|
|
|
|
/**
|
|
* A `CancelToken` is an object that can be used to request cancellation of an operation.
|
|
*
|
|
* @param {Function} executor The executor function.
|
|
*
|
|
* @returns {CancelToken}
|
|
*/
|
|
class CancelToken {
|
|
constructor(executor) {
|
|
if (typeof executor !== 'function') {
|
|
throw new TypeError('executor must be a function.');
|
|
}
|
|
|
|
let resolvePromise;
|
|
|
|
this.promise = new Promise(function promiseExecutor(resolve) {
|
|
resolvePromise = resolve;
|
|
});
|
|
|
|
const token = this;
|
|
|
|
// eslint-disable-next-line func-names
|
|
this.promise.then(cancel => {
|
|
if (!token._listeners) return;
|
|
|
|
let i = token._listeners.length;
|
|
|
|
while (i-- > 0) {
|
|
token._listeners[i](cancel);
|
|
}
|
|
token._listeners = null;
|
|
});
|
|
|
|
// eslint-disable-next-line func-names
|
|
this.promise.then = onfulfilled => {
|
|
let _resolve;
|
|
// eslint-disable-next-line func-names
|
|
const promise = new Promise(resolve => {
|
|
token.subscribe(resolve);
|
|
_resolve = resolve;
|
|
}).then(onfulfilled);
|
|
|
|
promise.cancel = function reject() {
|
|
token.unsubscribe(_resolve);
|
|
};
|
|
|
|
return promise;
|
|
};
|
|
|
|
executor(function cancel(message, config, request) {
|
|
if (token.reason) {
|
|
// Cancellation has already been requested
|
|
return;
|
|
}
|
|
|
|
token.reason = new CanceledError(message, config, request);
|
|
resolvePromise(token.reason);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Throws a `CanceledError` if cancellation has been requested.
|
|
*/
|
|
throwIfRequested() {
|
|
if (this.reason) {
|
|
throw this.reason;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe to the cancel signal
|
|
*/
|
|
|
|
subscribe(listener) {
|
|
if (this.reason) {
|
|
listener(this.reason);
|
|
return;
|
|
}
|
|
|
|
if (this._listeners) {
|
|
this._listeners.push(listener);
|
|
} else {
|
|
this._listeners = [listener];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from the cancel signal
|
|
*/
|
|
|
|
unsubscribe(listener) {
|
|
if (!this._listeners) {
|
|
return;
|
|
}
|
|
const index = this._listeners.indexOf(listener);
|
|
if (index !== -1) {
|
|
this._listeners.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
toAbortSignal() {
|
|
const controller = new AbortController();
|
|
|
|
const abort = (err) => {
|
|
controller.abort(err);
|
|
};
|
|
|
|
this.subscribe(abort);
|
|
|
|
controller.signal.unsubscribe = () => this.unsubscribe(abort);
|
|
|
|
return controller.signal;
|
|
}
|
|
|
|
/**
|
|
* Returns an object that contains a new `CancelToken` and a function that, when called,
|
|
* cancels the `CancelToken`.
|
|
*/
|
|
static source() {
|
|
let cancel;
|
|
const token = new CancelToken(function executor(c) {
|
|
cancel = c;
|
|
});
|
|
return {
|
|
token,
|
|
cancel
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Syntactic sugar for invoking a function and expanding an array for arguments.
|
|
*
|
|
* Common use case would be to use `Function.prototype.apply`.
|
|
*
|
|
* ```js
|
|
* function f(x, y, z) {}
|
|
* var args = [1, 2, 3];
|
|
* f.apply(null, args);
|
|
* ```
|
|
*
|
|
* With `spread` this example can be re-written.
|
|
*
|
|
* ```js
|
|
* spread(function(x, y, z) {})([1, 2, 3]);
|
|
* ```
|
|
*
|
|
* @param {Function} callback
|
|
*
|
|
* @returns {Function}
|
|
*/
|
|
function spread(callback) {
|
|
return function wrap(arr) {
|
|
return callback.apply(null, arr);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determines whether the payload is an error thrown by Axios
|
|
*
|
|
* @param {*} payload The value to test
|
|
*
|
|
* @returns {boolean} True if the payload is an error thrown by Axios, otherwise false
|
|
*/
|
|
function isAxiosError(payload) {
|
|
return utils$1.isObject(payload) && (payload.isAxiosError === true);
|
|
}
|
|
|
|
const HttpStatusCode = {
|
|
Continue: 100,
|
|
SwitchingProtocols: 101,
|
|
Processing: 102,
|
|
EarlyHints: 103,
|
|
Ok: 200,
|
|
Created: 201,
|
|
Accepted: 202,
|
|
NonAuthoritativeInformation: 203,
|
|
NoContent: 204,
|
|
ResetContent: 205,
|
|
PartialContent: 206,
|
|
MultiStatus: 207,
|
|
AlreadyReported: 208,
|
|
ImUsed: 226,
|
|
MultipleChoices: 300,
|
|
MovedPermanently: 301,
|
|
Found: 302,
|
|
SeeOther: 303,
|
|
NotModified: 304,
|
|
UseProxy: 305,
|
|
Unused: 306,
|
|
TemporaryRedirect: 307,
|
|
PermanentRedirect: 308,
|
|
BadRequest: 400,
|
|
Unauthorized: 401,
|
|
PaymentRequired: 402,
|
|
Forbidden: 403,
|
|
NotFound: 404,
|
|
MethodNotAllowed: 405,
|
|
NotAcceptable: 406,
|
|
ProxyAuthenticationRequired: 407,
|
|
RequestTimeout: 408,
|
|
Conflict: 409,
|
|
Gone: 410,
|
|
LengthRequired: 411,
|
|
PreconditionFailed: 412,
|
|
PayloadTooLarge: 413,
|
|
UriTooLong: 414,
|
|
UnsupportedMediaType: 415,
|
|
RangeNotSatisfiable: 416,
|
|
ExpectationFailed: 417,
|
|
ImATeapot: 418,
|
|
MisdirectedRequest: 421,
|
|
UnprocessableEntity: 422,
|
|
Locked: 423,
|
|
FailedDependency: 424,
|
|
TooEarly: 425,
|
|
UpgradeRequired: 426,
|
|
PreconditionRequired: 428,
|
|
TooManyRequests: 429,
|
|
RequestHeaderFieldsTooLarge: 431,
|
|
UnavailableForLegalReasons: 451,
|
|
InternalServerError: 500,
|
|
NotImplemented: 501,
|
|
BadGateway: 502,
|
|
ServiceUnavailable: 503,
|
|
GatewayTimeout: 504,
|
|
HttpVersionNotSupported: 505,
|
|
VariantAlsoNegotiates: 506,
|
|
InsufficientStorage: 507,
|
|
LoopDetected: 508,
|
|
NotExtended: 510,
|
|
NetworkAuthenticationRequired: 511,
|
|
};
|
|
|
|
Object.entries(HttpStatusCode).forEach(([key, value]) => {
|
|
HttpStatusCode[value] = key;
|
|
});
|
|
|
|
/**
|
|
* Create an instance of Axios
|
|
*
|
|
* @param {Object} defaultConfig The default config for the instance
|
|
*
|
|
* @returns {Axios} A new instance of Axios
|
|
*/
|
|
function createInstance(defaultConfig) {
|
|
const context = new Axios(defaultConfig);
|
|
const instance = bind(Axios.prototype.request, context);
|
|
|
|
// Copy axios.prototype to instance
|
|
utils$1.extend(instance, Axios.prototype, context, {allOwnKeys: true});
|
|
|
|
// Copy context to instance
|
|
utils$1.extend(instance, context, null, {allOwnKeys: true});
|
|
|
|
// Factory for creating new instances
|
|
instance.create = function create(instanceConfig) {
|
|
return createInstance(mergeConfig(defaultConfig, instanceConfig));
|
|
};
|
|
|
|
return instance;
|
|
}
|
|
|
|
// Create the default instance to be exported
|
|
const axios = createInstance(defaults);
|
|
|
|
// Expose Axios class to allow class inheritance
|
|
axios.Axios = Axios;
|
|
|
|
// Expose Cancel & CancelToken
|
|
axios.CanceledError = CanceledError;
|
|
axios.CancelToken = CancelToken;
|
|
axios.isCancel = isCancel;
|
|
axios.VERSION = VERSION;
|
|
axios.toFormData = toFormData;
|
|
|
|
// Expose AxiosError class
|
|
axios.AxiosError = AxiosError;
|
|
|
|
// alias for CanceledError for backward compatibility
|
|
axios.Cancel = axios.CanceledError;
|
|
|
|
// Expose all/spread
|
|
axios.all = function all(promises) {
|
|
return Promise.all(promises);
|
|
};
|
|
|
|
axios.spread = spread;
|
|
|
|
// Expose isAxiosError
|
|
axios.isAxiosError = isAxiosError;
|
|
|
|
// Expose mergeConfig
|
|
axios.mergeConfig = mergeConfig;
|
|
|
|
axios.AxiosHeaders = AxiosHeaders;
|
|
|
|
axios.formToJSON = thing => formDataToJSON(utils$1.isHTMLForm(thing) ? new FormData(thing) : thing);
|
|
|
|
axios.getAdapter = adapters.getAdapter;
|
|
|
|
axios.HttpStatusCode = HttpStatusCode;
|
|
|
|
axios.default = axios;
|
|
|
|
async function storeData(servers, key, value, ttl) {
|
|
for (const server of servers) {
|
|
try {
|
|
const url = new URL(`${server}/store`);
|
|
url.searchParams.append("key", key);
|
|
if (ttl !== null) {
|
|
url.searchParams.append("ttl", ttl.toString());
|
|
}
|
|
const response = await axios.post(url.toString(), value, {
|
|
headers: {
|
|
"Content-Type": "application/octet-stream"
|
|
}
|
|
});
|
|
console.log("Data stored successfully:", key);
|
|
if (response.status !== 200) {
|
|
console.error("Received response status", response.status);
|
|
continue;
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response?.status === 409) {
|
|
return null;
|
|
}
|
|
console.error("Error storing data:", error);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function retrieveData(servers, key) {
|
|
for (const server of servers) {
|
|
try {
|
|
const response = await axios.get(`${server}/retrieve/${key}`, {
|
|
responseType: "arraybuffer"
|
|
});
|
|
if (response.status !== 200) {
|
|
console.error("Received response status", response.status);
|
|
continue;
|
|
}
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error("Error retrieving data:", error);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function testData(servers, key) {
|
|
const res = {};
|
|
for (const server of servers) {
|
|
res[server] = null;
|
|
try {
|
|
const response = await axios.get(`${server}/test/${key}`);
|
|
if (response.status !== 200) {
|
|
console.error(`${server}: Test response status: ${response.status}`);
|
|
continue;
|
|
}
|
|
const data = response.data;
|
|
res[server] = data.value;
|
|
} catch (error) {
|
|
console.error("Error retrieving data:", error);
|
|
return null;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
const BASEURL = `http://localhost`;
|
|
const BOOTSTRAPURL = [`${BASEURL}:8090`];
|
|
const STORAGEURL = `${BASEURL}:8081`;
|
|
const BLINDBITURL = `${BASEURL}:8000`;
|
|
const DEFAULTAMOUNT = 1000n;
|
|
const EMPTY32BYTES = String("").padStart(64, "0");
|
|
class Services {
|
|
static initializing = null;
|
|
static instance;
|
|
processId = null;
|
|
stateId = null;
|
|
sdkClient;
|
|
processesCache = {};
|
|
myProcesses = /* @__PURE__ */ new Set();
|
|
notifications = null;
|
|
subscriptions = [];
|
|
database;
|
|
routingInstance;
|
|
relayAddresses = {};
|
|
membersList = {};
|
|
currentBlockHeight = -1;
|
|
// Private constructor to prevent direct instantiation from outside
|
|
constructor() {
|
|
}
|
|
// Method to access the singleton instance of Services
|
|
static async getInstance() {
|
|
if (Services.instance) {
|
|
return Services.instance;
|
|
}
|
|
if (!Services.initializing) {
|
|
Services.initializing = (async () => {
|
|
const instance = new Services();
|
|
await instance.init();
|
|
instance.routingInstance = await ModalService$1.getInstance();
|
|
return instance;
|
|
})();
|
|
}
|
|
console.log("initializing services");
|
|
Services.instance = await Services.initializing;
|
|
Services.initializing = null;
|
|
return Services.instance;
|
|
}
|
|
async init() {
|
|
this.notifications = this.getNotifications();
|
|
this.sdkClient = await __vitePreload(() => import('./sdk_client-CuF7MH2z.mjs'),true?[]:void 0);
|
|
this.sdkClient.setup();
|
|
const params = new URLSearchParams(window.location.search);
|
|
const isE2E = params.has("e2e");
|
|
if (isE2E) {
|
|
return;
|
|
}
|
|
for (const wsurl of Object.values(BOOTSTRAPURL)) {
|
|
this.updateRelay(wsurl, "");
|
|
}
|
|
}
|
|
setProcessId(processId) {
|
|
this.processId = processId;
|
|
}
|
|
setStateId(stateId) {
|
|
this.stateId = stateId;
|
|
}
|
|
getProcessId() {
|
|
return this.processId;
|
|
}
|
|
getStateId() {
|
|
return this.stateId;
|
|
}
|
|
/**
|
|
* Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses.
|
|
* Waits for at least one handshake message before returning.
|
|
*/
|
|
async connectAllRelays() {
|
|
const connectedUrls = [];
|
|
for (const wsurl of Object.keys(this.relayAddresses)) {
|
|
try {
|
|
console.log(`Connecting to: ${wsurl}`);
|
|
await this.addWebsocketConnection(wsurl);
|
|
connectedUrls.push(wsurl);
|
|
console.log(`Successfully connected to: ${wsurl}`);
|
|
} catch (error) {
|
|
console.error(`Failed to connect to ${wsurl}:`, error);
|
|
}
|
|
}
|
|
if (connectedUrls.length > 0) {
|
|
await this.waitForHandshakeMessage();
|
|
}
|
|
}
|
|
async addWebsocketConnection(url) {
|
|
console.log("Opening new websocket connection");
|
|
await initWebsocket(url);
|
|
}
|
|
/**
|
|
* Add or update a key/value pair in relayAddresses.
|
|
* @param wsurl - The WebSocket URL (key).
|
|
* @param spAddress - The SP Address (value).
|
|
*/
|
|
updateRelay(wsurl, spAddress) {
|
|
this.relayAddresses[wsurl] = spAddress;
|
|
console.log(`Updated: ${wsurl} -> ${spAddress}`);
|
|
}
|
|
/**
|
|
* Retrieve the spAddress for a given wsurl.
|
|
* @param wsurl - The WebSocket URL to look up.
|
|
* @returns The SP Address if found, or undefined if not.
|
|
*/
|
|
getSpAddress(wsurl) {
|
|
return this.relayAddresses[wsurl];
|
|
}
|
|
/**
|
|
* Get all key/value pairs from relayAddresses.
|
|
* @returns An array of objects containing wsurl and spAddress.
|
|
*/
|
|
getAllRelays() {
|
|
return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({
|
|
wsurl,
|
|
spAddress
|
|
}));
|
|
}
|
|
/**
|
|
* Print all key/value pairs for debugging.
|
|
*/
|
|
printAllRelays() {
|
|
console.log("Current relay addresses:");
|
|
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
|
|
console.log(`${wsurl} -> ${spAddress}`);
|
|
}
|
|
}
|
|
isPaired() {
|
|
try {
|
|
return this.sdkClient.is_paired();
|
|
} catch (e) {
|
|
throw new Error(`isPaired ~ Error: ${e}`);
|
|
}
|
|
}
|
|
async unpairDevice() {
|
|
try {
|
|
this.sdkClient.unpair_device();
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
} catch (e) {
|
|
throw new Error(`Failed to unpair device: ${e}`);
|
|
}
|
|
}
|
|
async getSecretForAddress(address) {
|
|
const db = await Database.getInstance();
|
|
return await db.getObject("shared_secrets", address);
|
|
}
|
|
async getAllSecrets() {
|
|
const db = await Database.getInstance();
|
|
const sharedSecrets = await db.dumpStore("shared_secrets");
|
|
const unconfirmedSecrets = await db.dumpStore("unconfirmed_secrets");
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets)
|
|
};
|
|
return secretsStore;
|
|
}
|
|
async getAllDiffs() {
|
|
const db = await Database.getInstance();
|
|
return await db.dumpStore("diffs");
|
|
}
|
|
async getDiffByValue(value) {
|
|
const db = await Database.getInstance();
|
|
const store = "diffs";
|
|
const res = await db.getObject(store, value);
|
|
return res;
|
|
}
|
|
async getTokensFromFaucet() {
|
|
try {
|
|
await this.ensureSufficientAmount();
|
|
} catch (e) {
|
|
console.error("Failed to get tokens from relay, check connection");
|
|
return;
|
|
}
|
|
}
|
|
async checkConnections(members) {
|
|
await this.getTokensFromFaucet();
|
|
let unconnectedAddresses = /* @__PURE__ */ new Set();
|
|
const myAddress = this.getDeviceAddress();
|
|
for (const member of members) {
|
|
const sp_addresses = member.sp_addresses;
|
|
if (!sp_addresses || sp_addresses.length === 0)
|
|
continue;
|
|
for (const address of sp_addresses) {
|
|
if (address === myAddress)
|
|
continue;
|
|
const sharedSecret = await this.getSecretForAddress(address);
|
|
if (!sharedSecret) {
|
|
unconnectedAddresses.add(address);
|
|
}
|
|
}
|
|
}
|
|
if (unconnectedAddresses && unconnectedAddresses.size != 0) {
|
|
const apiResult = await this.connectAddresses([...unconnectedAddresses]);
|
|
await this.handleApiReturn(apiResult);
|
|
}
|
|
}
|
|
async connectAddresses(addresses) {
|
|
if (addresses.length === 0) {
|
|
throw new Error("Trying to connect to empty addresses list");
|
|
}
|
|
try {
|
|
return this.sdkClient.create_transaction(addresses, 1);
|
|
} catch (e) {
|
|
console.error("Failed to connect member:", e);
|
|
throw e;
|
|
}
|
|
}
|
|
async ensureSufficientAmount() {
|
|
const availableAmt = this.getAmount();
|
|
const target = DEFAULTAMOUNT * BigInt(10);
|
|
if (availableAmt < target) {
|
|
const faucetMsg = this.createFaucetMessage();
|
|
this.sendFaucetMessage(faucetMsg);
|
|
await this.waitForAmount(target);
|
|
}
|
|
}
|
|
async waitForAmount(target) {
|
|
let attempts = 3;
|
|
while (attempts > 0) {
|
|
const amount = this.getAmount();
|
|
if (amount >= target) {
|
|
return amount;
|
|
}
|
|
attempts--;
|
|
if (attempts > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
}
|
|
}
|
|
throw new Error("Amount is still 0 after 3 attempts");
|
|
}
|
|
async createPairingProcess(userName, pairWith) {
|
|
if (this.sdkClient.is_paired()) {
|
|
throw new Error("Device already paired");
|
|
}
|
|
const myAddress = this.sdkClient.get_address();
|
|
pairWith.push(myAddress);
|
|
const privateData = {
|
|
description: "pairing",
|
|
counter: 0
|
|
};
|
|
const publicData = {
|
|
memberPublicName: userName,
|
|
pairedAddresses: pairWith
|
|
};
|
|
const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), "roles"];
|
|
const roles = {
|
|
pairing: {
|
|
members: [],
|
|
validation_rules: [
|
|
{
|
|
quorum: 1,
|
|
fields: validation_fields,
|
|
min_sig_member: 1
|
|
}
|
|
],
|
|
storages: [STORAGEURL]
|
|
}
|
|
};
|
|
try {
|
|
return this.createProcess(privateData, publicData, roles);
|
|
} catch (e) {
|
|
throw new Error(`Creating process failed:, ${e}`);
|
|
}
|
|
}
|
|
isFileBlob(value) {
|
|
return typeof value === "object" && value !== null && typeof value.type === "string" && value.data instanceof Uint8Array;
|
|
}
|
|
splitData(obj) {
|
|
const jsonCompatibleData = {};
|
|
const binaryData = {};
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (this.isFileBlob(value)) {
|
|
binaryData[key] = value;
|
|
} else {
|
|
jsonCompatibleData[key] = value;
|
|
}
|
|
}
|
|
return { jsonCompatibleData, binaryData };
|
|
}
|
|
async createProcess(privateData, publicData, roles) {
|
|
let relayAddress = this.getAllRelays()[0]?.spAddress;
|
|
if (!relayAddress || relayAddress === "") {
|
|
console.log("No relay address found, connecting to relays...");
|
|
await this.connectAllRelays();
|
|
relayAddress = this.getAllRelays()[0]?.spAddress;
|
|
if (!relayAddress || relayAddress === "") {
|
|
throw new Error("No relay address available after connecting to relays");
|
|
}
|
|
}
|
|
const feeRate = 1;
|
|
const privateSplitData = this.splitData(privateData);
|
|
const publicSplitData = this.splitData(publicData);
|
|
const encodedPrivateData = {
|
|
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(privateSplitData.binaryData)
|
|
};
|
|
const encodedPublicData = {
|
|
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(publicSplitData.binaryData)
|
|
};
|
|
console.log("encoded data:", encodedPrivateData);
|
|
console.log("encoded data:", encodedPublicData);
|
|
let members = /* @__PURE__ */ new Set();
|
|
for (const role of Object.values(roles)) {
|
|
for (const member of role.members) {
|
|
const memberAddresses = this.getAddressesForMemberId(member);
|
|
if (memberAddresses && memberAddresses.length != 0) {
|
|
members.add({ sp_addresses: memberAddresses });
|
|
}
|
|
}
|
|
}
|
|
console.log("members:", members);
|
|
await this.checkConnections([...members]);
|
|
const result = this.sdkClient.create_new_process(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, this.getAllMembers());
|
|
return result;
|
|
}
|
|
async updateProcess(process, privateData, publicData, roles) {
|
|
if (!roles) {
|
|
roles = this.getRoles(process);
|
|
} else {
|
|
console.log("Provided new roles:", JSON.stringify(roles));
|
|
}
|
|
let members = /* @__PURE__ */ new Set();
|
|
for (const role of Object.values(roles)) {
|
|
for (const member of role.members) {
|
|
members.add(member);
|
|
}
|
|
}
|
|
if (members.size === 0) {
|
|
const publicData2 = this.getPublicData(process);
|
|
if (!publicData2 || !publicData2["pairedAddresses"]) {
|
|
throw new Error("Not a pairing process");
|
|
}
|
|
const decodedAddresses = this.decodeValue(publicData2["pairedAddresses"]);
|
|
if (decodedAddresses.length === 0) {
|
|
throw new Error("Not a pairing process");
|
|
}
|
|
members.add({ sp_addresses: decodedAddresses });
|
|
}
|
|
await this.checkConnections([...members]);
|
|
const privateSplitData = this.splitData(privateData);
|
|
const publicSplitData = this.splitData(publicData);
|
|
const encodedPrivateData = {
|
|
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(privateSplitData.binaryData)
|
|
};
|
|
const encodedPublicData = {
|
|
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
|
|
...this.sdkClient.encode_binary(publicSplitData.binaryData)
|
|
};
|
|
try {
|
|
return this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers());
|
|
} catch (e) {
|
|
throw new Error(`Failed to update process: ${e}`);
|
|
}
|
|
}
|
|
async createPrdUpdate(processId, stateId) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Unknown process");
|
|
}
|
|
try {
|
|
return this.sdkClient.create_update_message(process, stateId, this.getAllMembers());
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd update: ${e}`);
|
|
}
|
|
}
|
|
async createPrdResponse(processId, stateId) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Unknown process");
|
|
}
|
|
try {
|
|
return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers());
|
|
} catch (e) {
|
|
throw new Error(`Failed to create response prd: ${e}`);
|
|
}
|
|
}
|
|
async approveChange(processId, stateId) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Failed to get process from db");
|
|
}
|
|
try {
|
|
return this.sdkClient.validate_state(process, stateId, this.getAllMembers());
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
async rejectChange(processId, stateId) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Failed to get process from db");
|
|
}
|
|
try {
|
|
return this.sdkClient.refuse_state(process, stateId);
|
|
} catch (e) {
|
|
throw new Error(`Failed to create prd response: ${e}`);
|
|
}
|
|
}
|
|
async resetDevice() {
|
|
this.sdkClient.reset_device();
|
|
const db = await Database.getInstance();
|
|
await db.clearStore("wallet");
|
|
await db.clearStore("shared_secrets");
|
|
await db.clearStore("unconfirmed_secrets");
|
|
await db.clearStore("processes");
|
|
await db.clearStore("diffs");
|
|
}
|
|
sendNewTxMessage(message) {
|
|
sendMessage("NewTx", message);
|
|
}
|
|
sendCommitMessage(message) {
|
|
sendMessage("Commit", message);
|
|
}
|
|
sendCipherMessages(ciphers) {
|
|
for (let i = 0; i < ciphers.length; i++) {
|
|
const cipher = ciphers[i];
|
|
sendMessage("Cipher", cipher);
|
|
}
|
|
}
|
|
sendFaucetMessage(message) {
|
|
sendMessage("Faucet", message);
|
|
}
|
|
async parseCipher(message) {
|
|
const membersList = this.getAllMembers();
|
|
const processes = await this.getProcesses();
|
|
try {
|
|
const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes);
|
|
await this.handleApiReturn(apiReturn);
|
|
const waitingModal = document.getElementById("waiting-modal");
|
|
if (waitingModal) {
|
|
this.device2Ready = true;
|
|
}
|
|
} catch (e) {
|
|
console.error(`Parsed cipher with error: ${e}`);
|
|
}
|
|
}
|
|
async parseNewTx(newTxMsg) {
|
|
const parsedMsg = JSON.parse(newTxMsg);
|
|
if (parsedMsg.error !== null) {
|
|
console.error("Received error in new tx message:", parsedMsg.error);
|
|
return;
|
|
}
|
|
const membersList = this.getAllMembers();
|
|
try {
|
|
const prevouts = this.sdkClient.get_prevouts(parsedMsg.transaction);
|
|
console.log("prevouts:", prevouts);
|
|
for (const process of Object.values(this.processesCache)) {
|
|
const tip = process.states[process.states.length - 1].commited_in;
|
|
if (prevouts.includes(tip)) {
|
|
const processId = process.states[0].commited_in;
|
|
const newTip = this.sdkClient.get_txid(parsedMsg.transaction);
|
|
console.log("Transaction", newTip, "spends the tip of process", processId);
|
|
const newStateId = this.sdkClient.get_opreturn(parsedMsg.transaction);
|
|
console.log("newStateId:", newStateId);
|
|
const updatedProcess = this.sdkClient.process_commit_new_state(process, newStateId, newTip);
|
|
this.processesCache[processId] = updatedProcess;
|
|
console.log("updatedProcess:", updatedProcess);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to parse new tx for commitments:", e);
|
|
}
|
|
try {
|
|
const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList);
|
|
if (parsedTx) {
|
|
try {
|
|
await this.handleApiReturn(parsedTx);
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
} catch (e) {
|
|
console.error("Failed to update device with new tx");
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.debug(e);
|
|
}
|
|
}
|
|
async handleApiReturn(apiReturn) {
|
|
console.log(apiReturn);
|
|
if (apiReturn.partial_tx) {
|
|
try {
|
|
const res = this.sdkClient.sign_transaction(apiReturn.partial_tx);
|
|
apiReturn.new_tx_to_send = res.new_tx_to_send;
|
|
} catch (e) {
|
|
console.error("Failed to sign transaction:", e);
|
|
}
|
|
}
|
|
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
|
|
this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
if (apiReturn.secrets) {
|
|
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
|
|
const confirmedSecrets = apiReturn.secrets.shared_secrets;
|
|
const db = await Database.getInstance();
|
|
for (const secret of unconfirmedSecrets) {
|
|
await db.addObject({
|
|
storeName: "unconfirmed_secrets",
|
|
object: secret,
|
|
key: null
|
|
});
|
|
}
|
|
const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value }));
|
|
for (const entry of entries) {
|
|
try {
|
|
await db.addObject({
|
|
storeName: "shared_secrets",
|
|
object: entry.value,
|
|
key: entry.key
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
if (apiReturn.updated_process) {
|
|
const updatedProcess = apiReturn.updated_process;
|
|
const processId = updatedProcess.process_id;
|
|
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
|
|
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
|
|
const blob = this.hexToBlob(cipher);
|
|
try {
|
|
await this.saveBlobToDb(hash, blob);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
await this.saveProcessToDb(processId, updatedProcess.current_process);
|
|
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
|
|
try {
|
|
await this.saveDiffsToDb(updatedProcess.diffs);
|
|
} catch (e) {
|
|
console.error("Failed to save diffs to db:", e);
|
|
}
|
|
}
|
|
}
|
|
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
|
|
for (const hash of apiReturn.push_to_storage) {
|
|
const blob = await this.getBlobFromDb(hash);
|
|
if (blob) {
|
|
await this.saveDataToStorage(hash, blob, null);
|
|
} else {
|
|
console.error("Failed to get data from db");
|
|
}
|
|
}
|
|
}
|
|
if (apiReturn.commit_to_send) {
|
|
const commit = apiReturn.commit_to_send;
|
|
this.sendCommitMessage(JSON.stringify(commit));
|
|
}
|
|
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
|
|
this.sendCipherMessages(apiReturn.ciphers_to_send);
|
|
}
|
|
}
|
|
async openPairingConfirmationModal(processId) {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) {
|
|
console.error("Failed to find pairing process");
|
|
return;
|
|
}
|
|
const firstState = process.states[0];
|
|
const roles = firstState.roles;
|
|
const stateId = firstState.state_id;
|
|
try {
|
|
await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async confirmPairing() {
|
|
if (!this.processId || !this.stateId) {
|
|
console.error("Missing process and/or state ID");
|
|
return;
|
|
}
|
|
let createPrdUpdateReturn;
|
|
try {
|
|
createPrdUpdateReturn = await this.createPrdUpdate(this.processId, this.stateId);
|
|
} catch (e) {
|
|
throw new Error(`createPrdUpdate failed: ${e}`);
|
|
}
|
|
await this.handleApiReturn(createPrdUpdateReturn);
|
|
let approveChangeReturn;
|
|
try {
|
|
approveChangeReturn = await this.approveChange(this.processId, this.stateId);
|
|
} catch (e) {
|
|
throw new Error(`approveChange failed: ${e}`);
|
|
}
|
|
await this.handleApiReturn(approveChangeReturn);
|
|
await this.pairDevice();
|
|
this.processId = null;
|
|
this.stateId = null;
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
await navigate("account");
|
|
}
|
|
async updateDevice() {
|
|
let myPairingProcessId;
|
|
try {
|
|
myPairingProcessId = this.getPairingProcessId();
|
|
} catch (e) {
|
|
console.error("Failed to get pairing process id");
|
|
return;
|
|
}
|
|
const myPairingProcess = await this.getProcess(myPairingProcessId);
|
|
if (!myPairingProcess) {
|
|
console.error("Unknown pairing process");
|
|
return;
|
|
}
|
|
const myPairingState = this.getLastCommitedState(myPairingProcess);
|
|
if (myPairingState) {
|
|
const encodedSpAddressList = myPairingState.public_data["pairedAddresses"];
|
|
const spAddressList = this.decodeValue(encodedSpAddressList);
|
|
if (spAddressList.length === 0) {
|
|
console.error("Empty pairedAddresses");
|
|
return;
|
|
}
|
|
if (!spAddressList.includes(this.getDeviceAddress())) {
|
|
await this.unpairDevice();
|
|
return;
|
|
}
|
|
this.sdkClient.unpair_device();
|
|
this.sdkClient.pair_device(myPairingProcessId, spAddressList);
|
|
const newDevice = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(newDevice);
|
|
}
|
|
}
|
|
async pairDevice() {
|
|
if (!this.processId) {
|
|
console.error("No processId set");
|
|
return;
|
|
}
|
|
const process = await this.getProcess(this.processId);
|
|
if (!process) {
|
|
console.error("Unknown process");
|
|
return;
|
|
}
|
|
let spAddressList = [];
|
|
try {
|
|
let encodedSpAddressList = [];
|
|
if (this.stateId) {
|
|
const state = process.states.find((state2) => state2.state_id === this.stateId);
|
|
if (state) {
|
|
encodedSpAddressList = state.public_data["pairedAddresses"];
|
|
}
|
|
} else {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState) {
|
|
encodedSpAddressList = lastCommitedState.public_data["pairedAddresses"];
|
|
}
|
|
}
|
|
spAddressList = this.sdkClient.decode_value(encodedSpAddressList);
|
|
if (!spAddressList || spAddressList.length == 0) {
|
|
throw new Error("Empty pairedAddresses");
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to get pairedAddresses from process: ${e}`);
|
|
}
|
|
try {
|
|
this.sdkClient.pair_device(this.processId, spAddressList);
|
|
} catch (e) {
|
|
throw new Error(`Failed to pair device: ${e}`);
|
|
}
|
|
}
|
|
getAmount() {
|
|
const amount = this.sdkClient.get_available_amount();
|
|
return amount;
|
|
}
|
|
getDeviceAddress() {
|
|
try {
|
|
return this.sdkClient.get_address();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get device address: ${e}`);
|
|
}
|
|
}
|
|
dumpDeviceFromMemory() {
|
|
try {
|
|
return this.sdkClient.dump_device();
|
|
} catch (e) {
|
|
throw new Error(`Failed to dump device: ${e}`);
|
|
}
|
|
}
|
|
dumpNeuteredDevice() {
|
|
try {
|
|
return this.sdkClient.dump_neutered_device();
|
|
} catch (e) {
|
|
console.error(`Failed to dump device: ${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
getPairingProcessId() {
|
|
try {
|
|
return this.sdkClient.get_pairing_process_id();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get pairing process: ${e}`);
|
|
}
|
|
}
|
|
async saveDeviceInDatabase(device) {
|
|
const db = await Database.getInstance();
|
|
const walletStore = "wallet";
|
|
try {
|
|
const prevDevice = await this.getDeviceFromDatabase();
|
|
if (prevDevice) {
|
|
await db.deleteObject(walletStore, "1");
|
|
}
|
|
await db.addObject({
|
|
storeName: walletStore,
|
|
object: { pre_id: "1", device },
|
|
key: null
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async getDeviceFromDatabase() {
|
|
const db = await Database.getInstance();
|
|
const walletStore = "wallet";
|
|
try {
|
|
const dbRes = await db.getObject(walletStore, "1");
|
|
if (dbRes) {
|
|
return dbRes["device"];
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve device from db: ${e}`);
|
|
}
|
|
}
|
|
async getMemberFromDevice() {
|
|
try {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (device) {
|
|
const pairedMember = device["paired_member"];
|
|
return pairedMember.sp_addresses;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to retrieve paired_member from device: ${e}`);
|
|
}
|
|
}
|
|
isChildRole(parent, child) {
|
|
try {
|
|
this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
rolesContainsUs(roles) {
|
|
let us;
|
|
try {
|
|
us = this.sdkClient.get_pairing_process_id();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
return this.rolesContainsMember(roles, us);
|
|
}
|
|
rolesContainsMember(roles, pairingProcessId) {
|
|
for (const roleDef of Object.values(roles)) {
|
|
if (roleDef.members.includes(pairingProcessId)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
async dumpWallet() {
|
|
const wallet = await this.sdkClient.dump_wallet();
|
|
return wallet;
|
|
}
|
|
createFaucetMessage() {
|
|
const message = this.sdkClient.create_faucet_msg();
|
|
return message;
|
|
}
|
|
async createNewDevice() {
|
|
let spAddress = "";
|
|
try {
|
|
spAddress = await this.sdkClient.create_new_device(0, "signet");
|
|
const device = this.dumpDeviceFromMemory();
|
|
await this.saveDeviceInDatabase(device);
|
|
} catch (e) {
|
|
console.error("Services ~ Error:", e);
|
|
}
|
|
return spAddress;
|
|
}
|
|
restoreDevice(device) {
|
|
try {
|
|
this.sdkClient.restore_device(device);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async updateDeviceBlockHeight() {
|
|
if (this.currentBlockHeight === -1) {
|
|
throw new Error("Current block height not set");
|
|
}
|
|
let device = null;
|
|
try {
|
|
device = await this.getDeviceFromDatabase();
|
|
} catch (e) {
|
|
throw new Error(`Failed to get device from database: ${e}`);
|
|
}
|
|
if (!device) {
|
|
throw new Error("Device not found");
|
|
}
|
|
const birthday = device.sp_wallet.birthday;
|
|
if (birthday === void 0 || birthday === null) {
|
|
throw new Error("Birthday not found");
|
|
}
|
|
if (birthday === 0) {
|
|
device.sp_wallet.birthday = this.currentBlockHeight;
|
|
device.sp_wallet.last_scan = this.currentBlockHeight;
|
|
try {
|
|
this.sdkClient.restore_device(device);
|
|
await this.saveDeviceInDatabase(device);
|
|
} catch (e) {
|
|
throw new Error(`Failed to save updated device: ${e}`);
|
|
}
|
|
} else {
|
|
if (device.sp_wallet.last_scan < this.currentBlockHeight) {
|
|
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
async removeProcess(processId) {
|
|
const db = await Database.getInstance();
|
|
const storeName = "processes";
|
|
try {
|
|
await db.deleteObject(storeName, processId);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async batchSaveProcessesToDb(processes) {
|
|
if (Object.keys(processes).length === 0) {
|
|
return;
|
|
}
|
|
const db = await Database.getInstance();
|
|
const storeName = "processes";
|
|
try {
|
|
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
|
|
this.processesCache = { ...this.processesCache, ...processes };
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
async saveProcessToDb(processId, process) {
|
|
const db = await Database.getInstance();
|
|
const storeName = "processes";
|
|
try {
|
|
await db.addObject({
|
|
storeName,
|
|
object: process,
|
|
key: processId
|
|
});
|
|
this.processesCache[processId] = process;
|
|
} catch (e) {
|
|
console.error(`Failed to save process ${processId}: ${e}`);
|
|
}
|
|
}
|
|
async saveBlobToDb(hash, data) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.addObject({
|
|
storeName: "data",
|
|
object: data,
|
|
key: hash
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to save data to db: ${e}`);
|
|
}
|
|
}
|
|
async getBlobFromDb(hash) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
return await db.getObject("data", hash);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
async saveDataToStorage(hash, data, ttl) {
|
|
const storages = [STORAGEURL];
|
|
try {
|
|
await storeData(storages, hash, data, ttl);
|
|
} catch (e) {
|
|
console.error(`Failed to store data with hash ${hash}: ${e}`);
|
|
}
|
|
}
|
|
async fetchValueFromStorage(hash) {
|
|
const storages = [STORAGEURL];
|
|
return await retrieveData(storages, hash);
|
|
}
|
|
async testDataInStorage(hash) {
|
|
const storages = [STORAGEURL];
|
|
return await testData(storages, hash);
|
|
}
|
|
async saveDiffsToDb(diffs) {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
for (const diff of diffs) {
|
|
await db.addObject({
|
|
storeName: "diffs",
|
|
object: diff,
|
|
key: null
|
|
});
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Failed to save process: ${e}`);
|
|
}
|
|
}
|
|
async getProcess(processId) {
|
|
if (this.processesCache[processId]) {
|
|
return this.processesCache[processId];
|
|
} else {
|
|
const db = await Database.getInstance();
|
|
const process = await db.getObject("processes", processId);
|
|
return process;
|
|
}
|
|
}
|
|
async getProcesses() {
|
|
if (Object.keys(this.processesCache).length > 0) {
|
|
return this.processesCache;
|
|
} else {
|
|
try {
|
|
const db = await Database.getInstance();
|
|
this.processesCache = await db.dumpStore("processes");
|
|
return this.processesCache;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
async restoreProcessesFromBackUp(processes) {
|
|
const db = await Database.getInstance();
|
|
const storeName = "processes";
|
|
try {
|
|
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
await this.restoreProcessesFromDB();
|
|
}
|
|
// Restore processes cache from persistent storage
|
|
async restoreProcessesFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const processes = await db.dumpStore("processes");
|
|
if (processes && Object.keys(processes).length != 0) {
|
|
console.log(`Restoring ${Object.keys(processes).length} processes`);
|
|
this.processesCache = processes;
|
|
} else {
|
|
console.log("No processes to restore!");
|
|
}
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
async clearSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
await db.clearStore("shared_secrets");
|
|
await db.clearStore("unconfirmed_secrets");
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async restoreSecretsFromBackUp(secretsStore) {
|
|
const db = await Database.getInstance();
|
|
for (const secret of secretsStore.unconfirmed_secrets) {
|
|
await db.addObject({
|
|
storeName: "unconfirmed_secrets",
|
|
object: secret,
|
|
key: null
|
|
});
|
|
}
|
|
const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
|
|
for (const entry of entries) {
|
|
await db.addObject({
|
|
storeName: "shared_secrets",
|
|
object: entry.value,
|
|
key: entry.key
|
|
});
|
|
}
|
|
await this.restoreSecretsFromDB();
|
|
}
|
|
async restoreSecretsFromDB() {
|
|
const db = await Database.getInstance();
|
|
try {
|
|
const sharedSecrets = await db.dumpStore("shared_secrets");
|
|
const unconfirmedSecrets = await db.dumpStore("unconfirmed_secrets");
|
|
const secretsStore = {
|
|
shared_secrets: sharedSecrets,
|
|
unconfirmed_secrets: Object.values(unconfirmedSecrets)
|
|
};
|
|
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
decodeValue(value) {
|
|
try {
|
|
return this.sdkClient.decode_value(value);
|
|
} catch (e) {
|
|
console.error(`Failed to decode value: ${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
async decryptAttribute(processId, state, attribute) {
|
|
let hash = state.pcd_commitment[attribute];
|
|
if (!hash) {
|
|
return null;
|
|
}
|
|
let key = state.keys[attribute];
|
|
const pairingProcessId = this.getPairingProcessId();
|
|
if (!key) {
|
|
const roles = state.roles;
|
|
let hasAccess = false;
|
|
for (const role of Object.values(roles)) {
|
|
for (const rule of Object.values(role.validation_rules)) {
|
|
if (rule.fields.includes(attribute)) {
|
|
if (role.members.includes(pairingProcessId)) {
|
|
hasAccess = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!hasAccess)
|
|
return null;
|
|
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
|
|
const maxRetries = 5;
|
|
const retryDelay = 500;
|
|
let retries = 0;
|
|
while ((!hash || !key) && retries < maxRetries) {
|
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
hash = state.pcd_commitment[attribute];
|
|
key = state.keys[attribute];
|
|
retries++;
|
|
}
|
|
}
|
|
if (hash && key) {
|
|
const blob = await this.getBlobFromDb(hash);
|
|
if (blob) {
|
|
const buf = await blob.arrayBuffer();
|
|
const cipher = new Uint8Array(buf);
|
|
const keyUIntArray = this.hexToUInt8Array(key);
|
|
try {
|
|
const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher);
|
|
if (clear) {
|
|
const decoded = this.sdkClient.decode_value(clear);
|
|
return decoded;
|
|
} else {
|
|
throw new Error("decrypt_data returned null");
|
|
}
|
|
} catch (e) {
|
|
console.error(`Failed to decrypt data: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getNotifications() {
|
|
return this.notifications;
|
|
}
|
|
setNotifications(notifications) {
|
|
this.notifications = notifications;
|
|
}
|
|
async importJSON(backup) {
|
|
const device = backup.device;
|
|
await this.resetDevice();
|
|
await this.saveDeviceInDatabase(device);
|
|
this.restoreDevice(device);
|
|
const secretsStore = backup.secrets;
|
|
await this.restoreSecretsFromBackUp(secretsStore);
|
|
const processes = backup.processes;
|
|
await this.restoreProcessesFromBackUp(processes);
|
|
}
|
|
async createBackUp() {
|
|
const device = await this.getDeviceFromDatabase();
|
|
if (!device) {
|
|
console.error("No device loaded");
|
|
return null;
|
|
}
|
|
const processes = await this.getProcesses();
|
|
const secrets = await this.getAllSecrets();
|
|
const backUp = {
|
|
device,
|
|
secrets,
|
|
processes
|
|
};
|
|
return backUp;
|
|
}
|
|
// Device 1 wait Device 2
|
|
device1 = false;
|
|
device2Ready = false;
|
|
resetState() {
|
|
this.device1 = false;
|
|
this.device2Ready = false;
|
|
}
|
|
// Handle the handshake message
|
|
async handleHandshakeMsg(url, parsedMsg) {
|
|
try {
|
|
const handshakeMsg = JSON.parse(parsedMsg);
|
|
this.updateRelay(url, handshakeMsg.sp_address);
|
|
this.currentBlockHeight = handshakeMsg.chain_tip;
|
|
if (this.membersList && Object.keys(this.membersList).length === 0) {
|
|
this.membersList = handshakeMsg.peers_list;
|
|
} else {
|
|
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
|
|
this.membersList[processId] = member;
|
|
}
|
|
}
|
|
setTimeout(async () => {
|
|
const newProcesses = handshakeMsg.processes_list;
|
|
if (!newProcesses || Object.keys(newProcesses).length === 0) {
|
|
console.debug("Received empty processes list from", url);
|
|
return;
|
|
}
|
|
if (this.processesCache && Object.keys(this.processesCache).length === 0) {
|
|
try {
|
|
await this.batchSaveProcessesToDb(newProcesses);
|
|
} catch (e) {
|
|
console.error("Failed to save processes to db:", e);
|
|
}
|
|
} else {
|
|
const toSave = {};
|
|
for (const [processId, process] of Object.entries(newProcesses)) {
|
|
const existing = await this.getProcess(processId);
|
|
if (existing) {
|
|
let new_states = [];
|
|
let roles = [];
|
|
for (const state of process.states) {
|
|
if (!state.state_id || state.state_id === EMPTY32BYTES) {
|
|
continue;
|
|
}
|
|
if (!this.lookForStateId(existing, state.state_id)) {
|
|
if (this.rolesContainsUs(state.roles)) {
|
|
new_states.push(state.state_id);
|
|
roles.push(state.roles);
|
|
}
|
|
}
|
|
}
|
|
if (new_states.length != 0) {
|
|
await this.requestDataFromPeers(processId, new_states, roles);
|
|
toSave[processId] = process;
|
|
}
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data["pairedAddresses"]) {
|
|
try {
|
|
const pairedAddresses = this.decodeValue(lastCommitedState.public_data["pairedAddresses"]);
|
|
if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) {
|
|
await this.saveProcessToDb(processId, process);
|
|
await this.updateDevice();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to check for pairing process:", e);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`Saving ${processId} to db`);
|
|
toSave[processId] = process;
|
|
}
|
|
}
|
|
await this.batchSaveProcessesToDb(toSave);
|
|
}
|
|
}, 500);
|
|
} catch (e) {
|
|
console.error("Failed to parse init message:", e);
|
|
}
|
|
}
|
|
lookForStateId(process, stateId) {
|
|
for (const state of process.states) {
|
|
if (state.state_id === stateId) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Waits for at least one handshake message to be received from any connected relay.
|
|
* This ensures that the relay addresses are fully populated and the member list is updated.
|
|
* @returns A promise that resolves when at least one handshake message is received.
|
|
*/
|
|
async waitForHandshakeMessage(timeoutMs = 1e4) {
|
|
const startTime = Date.now();
|
|
const pollInterval = 100;
|
|
return new Promise((resolve, reject) => {
|
|
const checkForHandshake = () => {
|
|
if (Object.keys(this.membersList).length > 0) {
|
|
console.log("Handshake message received, members list populated");
|
|
resolve();
|
|
return;
|
|
}
|
|
if (Date.now() - startTime >= timeoutMs) {
|
|
reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`));
|
|
return;
|
|
}
|
|
setTimeout(checkForHandshake, pollInterval);
|
|
};
|
|
checkForHandshake();
|
|
});
|
|
}
|
|
/**
|
|
* Retourne la liste de tous les membres ordonnés par leur process id
|
|
* @returns Un tableau contenant tous les membres
|
|
*/
|
|
getAllMembersSorted() {
|
|
return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
|
|
}
|
|
getAllMembers() {
|
|
return this.membersList;
|
|
}
|
|
getAddressesForMemberId(memberId) {
|
|
try {
|
|
return this.membersList[memberId].sp_addresses;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
compareMembers(memberA, memberB) {
|
|
if (!memberA || !memberB) {
|
|
return false;
|
|
}
|
|
if (memberA.length !== memberB.length) {
|
|
return false;
|
|
}
|
|
const res = memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
|
|
return res;
|
|
}
|
|
async handleCommitError(response) {
|
|
const content = JSON.parse(response);
|
|
const error = content.error;
|
|
const errorMsg = error["GenericError"];
|
|
const dontRetry = ["State is identical to the previous state", "Not enough valid proofs", "Not enough members to validate"];
|
|
if (dontRetry.includes(errorMsg)) {
|
|
return;
|
|
}
|
|
setTimeout(async () => {
|
|
this.sendCommitMessage(JSON.stringify(content));
|
|
}, 1e3);
|
|
}
|
|
getRoles(process) {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
|
|
return lastCommitedState.roles;
|
|
} else if (process.states.length === 2) {
|
|
const firstState = process.states[0];
|
|
if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) {
|
|
return firstState.roles;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getPublicData(process) {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0) {
|
|
return lastCommitedState.public_data;
|
|
} else if (process.states.length === 2) {
|
|
const firstState = process.states[0];
|
|
if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) {
|
|
return firstState.public_data;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getProcessName(process) {
|
|
const lastCommitedState = this.getLastCommitedState(process);
|
|
if (lastCommitedState && lastCommitedState.public_data) {
|
|
const processName = lastCommitedState.public_data["processName"];
|
|
if (processName) {
|
|
return this.decodeValue(processName);
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
async getMyProcesses() {
|
|
let pairingProcessId = null;
|
|
try {
|
|
pairingProcessId = this.getPairingProcessId();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
if (!pairingProcessId) {
|
|
return null;
|
|
}
|
|
try {
|
|
const processes = await this.getProcesses();
|
|
const newMyProcesses = new Set(this.myProcesses || []);
|
|
newMyProcesses.add(pairingProcessId);
|
|
for (const [processId, process] of Object.entries(processes)) {
|
|
if (newMyProcesses.has(processId)) {
|
|
continue;
|
|
}
|
|
try {
|
|
const roles = this.getRoles(process);
|
|
if (roles && this.rolesContainsUs(roles)) {
|
|
newMyProcesses.add(processId);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
this.myProcesses = newMyProcesses;
|
|
return Array.from(this.myProcesses);
|
|
} catch (e) {
|
|
console.error("Failed to get processes:", e);
|
|
return null;
|
|
}
|
|
}
|
|
async requestDataFromPeers(processId, stateIds, roles) {
|
|
console.log("Requesting data from peers");
|
|
const membersList = this.getAllMembers();
|
|
try {
|
|
const res = this.sdkClient.request_data(processId, stateIds, roles, membersList);
|
|
await this.handleApiReturn(res);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
hexToBlob(hexString) {
|
|
const uint8Array = this.hexToUInt8Array(hexString);
|
|
const ab = new ArrayBuffer(uint8Array.length);
|
|
const view = new Uint8Array(ab);
|
|
view.set(uint8Array);
|
|
return new Blob([ab], { type: "application/octet-stream" });
|
|
}
|
|
hexToUInt8Array(hexString) {
|
|
if (hexString.length % 2 !== 0) {
|
|
throw new Error("Invalid hex string: length must be even");
|
|
}
|
|
const uint8Array = new Uint8Array(hexString.length / 2);
|
|
for (let i = 0; i < hexString.length; i += 2) {
|
|
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
|
|
}
|
|
return uint8Array;
|
|
}
|
|
async blobToHex(blob) {
|
|
const buffer = await blob.arrayBuffer();
|
|
const bytes = new Uint8Array(buffer);
|
|
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
getHashForFile(commitedIn, label, fileBlob) {
|
|
return this.sdkClient.hash_value(fileBlob, commitedIn, label);
|
|
}
|
|
getMerkleProofForFile(processState, attributeName) {
|
|
return this.sdkClient.get_merkle_proof(processState, attributeName);
|
|
}
|
|
validateMerkleProof(proof, hash) {
|
|
try {
|
|
return this.sdkClient.validate_merkle_proof(proof, hash);
|
|
} catch (e) {
|
|
throw new Error(`Failed to validate merkle proof: ${e}`);
|
|
}
|
|
}
|
|
getLastCommitedState(process) {
|
|
if (process.states.length === 0)
|
|
return null;
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
const lastCommitedState = process.states.findLast((state) => state.commited_in !== processTip);
|
|
if (lastCommitedState) {
|
|
return lastCommitedState;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
getLastCommitedStateIndex(process) {
|
|
if (process.states.length === 0)
|
|
return null;
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
for (let i = process.states.length - 1; i >= 0; i--) {
|
|
if (process.states[i].commited_in !== processTip) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getUncommitedStates(process) {
|
|
if (process.states.length === 0)
|
|
return [];
|
|
const processTip = process.states[process.states.length - 1].commited_in;
|
|
const res = process.states.filter((state) => state.commited_in === processTip);
|
|
return res.filter((state) => state.state_id !== EMPTY32BYTES);
|
|
}
|
|
getStateFromId(process, stateId) {
|
|
if (process.states.length === 0)
|
|
return null;
|
|
const state = process.states.find((state2) => state2.state_id === stateId);
|
|
if (state) {
|
|
return state;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
getNextStateAfterId(process, stateId) {
|
|
if (process.states.length === 0)
|
|
return null;
|
|
const index = process.states.findIndex((state) => state.state_id === stateId);
|
|
if (index !== -1 && index < process.states.length - 1) {
|
|
return process.states[index + 1];
|
|
}
|
|
return null;
|
|
}
|
|
isPairingProcess(roles) {
|
|
if (Object.keys(roles).length != 1) {
|
|
return false;
|
|
}
|
|
const pairingRole = roles["pairing"];
|
|
if (pairingRole) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
async updateMemberPublicName(process, newName) {
|
|
const publicData = {
|
|
memberPublicName: newName
|
|
};
|
|
return await this.updateProcess(process, {}, publicData, null);
|
|
}
|
|
}
|
|
|
|
class ModalService {
|
|
static instance;
|
|
stateId = null;
|
|
processId = null;
|
|
constructor() {
|
|
}
|
|
paired_addresses = [];
|
|
modal = null;
|
|
// Method to access the singleton instance of Services
|
|
static async getInstance() {
|
|
if (!ModalService.instance) {
|
|
ModalService.instance = new ModalService();
|
|
}
|
|
return ModalService.instance;
|
|
}
|
|
openLoginModal(myAddress, receiverAddress) {
|
|
const container = document.querySelector(".page-container");
|
|
let html = modalHtml;
|
|
html = html.replace("{{device1}}", myAddress);
|
|
html = html.replace("{{device2}}", receiverAddress);
|
|
if (container)
|
|
container.innerHTML += html;
|
|
const modal = document.getElementById("login-modal");
|
|
if (modal)
|
|
modal.style.display = "flex";
|
|
const newScript = document.createElement("script");
|
|
newScript.setAttribute("type", "module");
|
|
newScript.textContent = modalScript;
|
|
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
|
|
}
|
|
async injectModal(members) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/confirmation-modal.html").then((res) => res.text());
|
|
html = html.replace("{{device1}}", await addressToEmoji(members[0]["sp_addresses"][0]));
|
|
html = html.replace("{{device2}}", await addressToEmoji(members[0]["sp_addresses"][1]));
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.src = "/src/components/modal/confirmation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
async injectCreationModal(members) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/creation-modal.html").then((res) => res.text());
|
|
html = html.replace("{{device1}}", await addressToEmoji(members[0]["sp_addresses"][0]));
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.src = "/src/components/modal/confirmation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
// Device 1 wait Device 2
|
|
async injectWaitingModal() {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/modal/waiting-modal.html").then((res) => res.text());
|
|
container.innerHTML += html;
|
|
}
|
|
}
|
|
async injectValidationModal(processDiff) {
|
|
const container = document.querySelector("#containerId");
|
|
if (container) {
|
|
let html = await fetch("/src/components/validation-modal/validation-modal.html").then((res) => res.text());
|
|
html = interpolate(html, { processId: processDiff.processId });
|
|
container.innerHTML += html;
|
|
const script = document.createElement("script");
|
|
script.id = "validation-modal-script";
|
|
script.src = "/src/components/validation-modal/validation-modal.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
const css = document.createElement("style");
|
|
css.id = "validation-modal-css";
|
|
css.innerText = validationModalStyle;
|
|
document.head.appendChild(css);
|
|
initValidationModal(processDiff);
|
|
}
|
|
}
|
|
async closeValidationModal() {
|
|
const script = document.querySelector("#validation-modal-script");
|
|
const css = document.querySelector("#validation-modal-css");
|
|
const component = document.querySelector("#validation-modal");
|
|
script?.remove();
|
|
css?.remove();
|
|
component?.remove();
|
|
}
|
|
async openPairingConfirmationModal(roleDefinition, processId, stateId) {
|
|
let memberOutPoints;
|
|
if (roleDefinition["pairing"]) {
|
|
const owner = roleDefinition["pairing"];
|
|
memberOutPoints = owner.members;
|
|
} else {
|
|
throw new Error('No "pairing" role');
|
|
}
|
|
if (memberOutPoints.length != 1) {
|
|
throw new Error("Must have exactly 1 member");
|
|
}
|
|
console.log("MEMBER OUTPOINTS:", memberOutPoints);
|
|
const service = await Services.getInstance();
|
|
const localAddress = service.getDeviceAddress();
|
|
const members = [];
|
|
for (const outPoint of memberOutPoints) {
|
|
const member = { sp_addresses: [] };
|
|
members.push(member);
|
|
}
|
|
for (const member of members) {
|
|
if (member.sp_addresses) {
|
|
for (const address of member.sp_addresses) {
|
|
if (address !== localAddress) {
|
|
this.paired_addresses.push(address);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.processId = processId;
|
|
this.stateId = stateId;
|
|
if (members[0].sp_addresses.length === 1) {
|
|
await this.injectCreationModal(members);
|
|
this.modal = document.getElementById("creation-modal");
|
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
|
} else {
|
|
await this.injectModal(members);
|
|
this.modal = document.getElementById("modal");
|
|
console.log("LENGTH:", members[0].sp_addresses.length);
|
|
}
|
|
if (this.modal)
|
|
this.modal.style.display = "flex";
|
|
window.onclick = (event) => {
|
|
if (event.target === this.modal) {
|
|
this.closeConfirmationModal();
|
|
}
|
|
};
|
|
}
|
|
confirmLogin() {
|
|
console.log("=============> Confirm Login");
|
|
}
|
|
async closeLoginModal() {
|
|
if (this.modal)
|
|
this.modal.style.display = "none";
|
|
}
|
|
async showConfirmationModal(options, fullscreen = false) {
|
|
const modalElement = document.createElement("div");
|
|
modalElement.id = "confirmation-modal";
|
|
modalElement.innerHTML = `
|
|
<div class="modal-overlay">
|
|
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ""}>
|
|
<h2>${options.title}</h2>
|
|
<div class="modal-body">
|
|
${options.content}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || "Annuler"}</button>
|
|
<button id="confirm-button" class="btn btn-primary">${options.confirmText || "Confirmer"}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modalElement);
|
|
return new Promise((resolve) => {
|
|
const confirmButton = modalElement.querySelector("#confirm-button");
|
|
const cancelButton = modalElement.querySelector("#cancel-button");
|
|
const modalOverlay = modalElement.querySelector(".modal-overlay");
|
|
const cleanup = () => {
|
|
modalElement.remove();
|
|
};
|
|
confirmButton?.addEventListener("click", () => {
|
|
cleanup();
|
|
resolve(true);
|
|
});
|
|
cancelButton?.addEventListener("click", () => {
|
|
cleanup();
|
|
resolve(false);
|
|
});
|
|
modalOverlay?.addEventListener("click", (e) => {
|
|
if (e.target === modalOverlay) {
|
|
cleanup();
|
|
resolve(false);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async closeConfirmationModal() {
|
|
const service = await Services.getInstance();
|
|
await service.unpairDevice();
|
|
if (this.modal)
|
|
this.modal.style.display = "none";
|
|
}
|
|
}
|
|
|
|
async function unpair() {
|
|
const service = await Services.getInstance();
|
|
await service.unpairDevice();
|
|
await navigate("home");
|
|
}
|
|
window.unpair = unpair;
|
|
function toggleMenu() {
|
|
const menu = document.getElementById("menu");
|
|
if (menu) {
|
|
if (menu.style.display === "block") {
|
|
menu.style.display = "none";
|
|
} else {
|
|
menu.style.display = "block";
|
|
}
|
|
}
|
|
}
|
|
window.toggleMenu = toggleMenu;
|
|
function openCloseNotifications() {
|
|
const notifications2 = document.querySelector(".notification-board");
|
|
notifications2.style.display = notifications2?.style.display === "none" ? "block" : "none";
|
|
}
|
|
window.openCloseNotifications = openCloseNotifications;
|
|
async function initHeader() {
|
|
if (currentRoute === "account") {
|
|
const profileContainer = document.getElementById("profile-header-container");
|
|
if (profileContainer) {
|
|
const profileHeaderHtml = await fetch("/src/components/profile-header/profile-header.html").then((res) => res.text());
|
|
profileContainer.innerHTML = profileHeaderHtml;
|
|
loadUserProfile();
|
|
}
|
|
}
|
|
if (currentRoute === "home") {
|
|
hideSomeFunctionnalities();
|
|
} else {
|
|
fetchNotifications();
|
|
setInterval(fetchNotifications, 2 * 60 * 1e3);
|
|
}
|
|
}
|
|
function hideSomeFunctionnalities() {
|
|
const bell = document.querySelector(".bell-icon");
|
|
if (bell)
|
|
bell.style.display = "none";
|
|
const notifBadge = document.querySelector(".notification-badge");
|
|
if (notifBadge)
|
|
notifBadge.style.display = "none";
|
|
const actions = document.querySelectorAll(".menu-content a");
|
|
const excludedActions = ["Import", "Export"];
|
|
for (const action of actions) {
|
|
if (!excludedActions.includes(action.innerHTML)) {
|
|
action.style.display = "none";
|
|
}
|
|
}
|
|
}
|
|
async function setNotification(notifications2) {
|
|
const badge = document.querySelector(".notification-badge");
|
|
const noNotifications = document.querySelector(".no-notification");
|
|
if (notifications2?.length) {
|
|
badge.innerText = notifications2.length.toString();
|
|
const notificationBoard = document.querySelector(".notification-board");
|
|
notificationBoard.querySelectorAll(".notification-element")?.forEach((elem) => elem.remove());
|
|
noNotifications.style.display = "none";
|
|
for (const notif of notifications2) {
|
|
const notifElement = document.createElement("div");
|
|
notifElement.className = "notification-element";
|
|
notifElement.setAttribute("notif-id", notif.processId);
|
|
notifElement.innerHTML = `
|
|
<div>Validation required : </div>
|
|
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
|
|
`;
|
|
notificationBoard.appendChild(notifElement);
|
|
notifElement.addEventListener("click", async () => {
|
|
const modalService = await ModalService.getInstance();
|
|
modalService.injectValidationModal(notif);
|
|
});
|
|
}
|
|
} else {
|
|
noNotifications.style.display = "block";
|
|
}
|
|
}
|
|
async function fetchNotifications() {
|
|
const service = await Services.getInstance();
|
|
const data = service.getNotifications() || [];
|
|
await setNotification(data);
|
|
}
|
|
async function loadUserProfile() {
|
|
const userName = localStorage.getItem("userName");
|
|
const userLastName = localStorage.getItem("userLastName");
|
|
const userAvatar = localStorage.getItem("userAvatar") || "https://via.placeholder.com/150";
|
|
const userBanner = localStorage.getItem("userBanner") || "https://via.placeholder.com/800x200";
|
|
const nameElement = document.querySelector(".user-name");
|
|
const lastNameElement = document.querySelector(".user-lastname");
|
|
const avatarElement = document.querySelector(".avatar");
|
|
const bannerElement = document.querySelector(".banner-image");
|
|
if (nameElement)
|
|
nameElement.textContent = userName;
|
|
if (lastNameElement)
|
|
lastNameElement.textContent = userLastName;
|
|
if (avatarElement)
|
|
avatarElement.src = userAvatar;
|
|
if (bannerElement)
|
|
bannerElement.src = userBanner;
|
|
}
|
|
async function importJSON() {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".json";
|
|
input.onchange = async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = async (e2) => {
|
|
try {
|
|
const content = JSON.parse(e2.target?.result);
|
|
const service = await Services.getInstance();
|
|
await service.importJSON(content);
|
|
alert("Import réussi");
|
|
window.location.reload();
|
|
} catch (error) {
|
|
alert("Erreur lors de l'import: " + error);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
input.click();
|
|
}
|
|
window.importJSON = importJSON;
|
|
async function createBackUp() {
|
|
const service = await Services.getInstance();
|
|
const backUp = await service.createBackUp();
|
|
if (!backUp) {
|
|
console.error("No device to backup");
|
|
return;
|
|
}
|
|
try {
|
|
const backUpJson = JSON.stringify(backUp, null, 2);
|
|
const blob = new Blob([backUpJson], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "4nk-backup.json";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
console.log("Backup successfully prepared for download");
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
window.createBackUp = createBackUp;
|
|
async function disconnect() {
|
|
console.log("Disconnecting...");
|
|
try {
|
|
localStorage.clear();
|
|
await new Promise((resolve, reject) => {
|
|
const request = indexedDB.deleteDatabase("4nk");
|
|
request.onsuccess = () => {
|
|
console.log("IndexedDB deleted successfully");
|
|
resolve();
|
|
};
|
|
request.onerror = () => reject(request.error);
|
|
request.onblocked = () => {
|
|
console.log("Database deletion was blocked");
|
|
resolve();
|
|
};
|
|
});
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
await Promise.all(registrations.map((registration) => registration.unregister()));
|
|
console.log("Service worker unregistered");
|
|
await navigate("home");
|
|
setTimeout(() => {
|
|
window.location.href = window.location.origin;
|
|
}, 100);
|
|
} catch (error) {
|
|
console.error("Error during disconnect:", error);
|
|
window.location.href = window.location.origin;
|
|
}
|
|
}
|
|
window.disconnect = disconnect;
|
|
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
function concat(...buffers) {
|
|
const size = buffers.reduce((acc, { length }) => acc + length, 0);
|
|
const buf = new Uint8Array(size);
|
|
let i = 0;
|
|
for (const buffer of buffers) {
|
|
buf.set(buffer, i);
|
|
i += buffer.length;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
function encodeBase64(input) {
|
|
if (Uint8Array.prototype.toBase64) {
|
|
return input.toBase64();
|
|
}
|
|
const CHUNK_SIZE = 0x8000;
|
|
const arr = [];
|
|
for (let i = 0; i < input.length; i += CHUNK_SIZE) {
|
|
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
}
|
|
return btoa(arr.join(''));
|
|
}
|
|
function decodeBase64(encoded) {
|
|
if (Uint8Array.fromBase64) {
|
|
return Uint8Array.fromBase64(encoded);
|
|
}
|
|
const binary = atob(encoded);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function decode(input) {
|
|
if (Uint8Array.fromBase64) {
|
|
return Uint8Array.fromBase64(typeof input === 'string' ? input : decoder.decode(input), {
|
|
alphabet: 'base64url',
|
|
});
|
|
}
|
|
let encoded = input;
|
|
if (encoded instanceof Uint8Array) {
|
|
encoded = decoder.decode(encoded);
|
|
}
|
|
encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
|
|
try {
|
|
return decodeBase64(encoded);
|
|
}
|
|
catch {
|
|
throw new TypeError('The input to be decoded is not correctly encoded.');
|
|
}
|
|
}
|
|
function encode(input) {
|
|
let unencoded = input;
|
|
if (typeof unencoded === 'string') {
|
|
unencoded = encoder.encode(unencoded);
|
|
}
|
|
if (Uint8Array.prototype.toBase64) {
|
|
return unencoded.toBase64({ alphabet: 'base64url', omitPadding: true });
|
|
}
|
|
return encodeBase64(unencoded).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
}
|
|
|
|
class JOSEError extends Error {
|
|
static code = 'ERR_JOSE_GENERIC';
|
|
code = 'ERR_JOSE_GENERIC';
|
|
constructor(message, options) {
|
|
super(message, options);
|
|
this.name = this.constructor.name;
|
|
Error.captureStackTrace?.(this, this.constructor);
|
|
}
|
|
}
|
|
class JWTClaimValidationFailed extends JOSEError {
|
|
static code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';
|
|
code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';
|
|
claim;
|
|
reason;
|
|
payload;
|
|
constructor(message, payload, claim = 'unspecified', reason = 'unspecified') {
|
|
super(message, { cause: { claim, reason, payload } });
|
|
this.claim = claim;
|
|
this.reason = reason;
|
|
this.payload = payload;
|
|
}
|
|
}
|
|
class JWTExpired extends JOSEError {
|
|
static code = 'ERR_JWT_EXPIRED';
|
|
code = 'ERR_JWT_EXPIRED';
|
|
claim;
|
|
reason;
|
|
payload;
|
|
constructor(message, payload, claim = 'unspecified', reason = 'unspecified') {
|
|
super(message, { cause: { claim, reason, payload } });
|
|
this.claim = claim;
|
|
this.reason = reason;
|
|
this.payload = payload;
|
|
}
|
|
}
|
|
class JOSENotSupported extends JOSEError {
|
|
static code = 'ERR_JOSE_NOT_SUPPORTED';
|
|
code = 'ERR_JOSE_NOT_SUPPORTED';
|
|
}
|
|
class JWSInvalid extends JOSEError {
|
|
static code = 'ERR_JWS_INVALID';
|
|
code = 'ERR_JWS_INVALID';
|
|
}
|
|
class JWTInvalid extends JOSEError {
|
|
static code = 'ERR_JWT_INVALID';
|
|
code = 'ERR_JWT_INVALID';
|
|
}
|
|
class JWSSignatureVerificationFailed extends JOSEError {
|
|
static code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
|
|
code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
|
|
constructor(message = 'signature verification failed', options) {
|
|
super(message, options);
|
|
}
|
|
}
|
|
|
|
function unusable(name, prop = 'algorithm.name') {
|
|
return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
}
|
|
function isAlgorithm(algorithm, name) {
|
|
return algorithm.name === name;
|
|
}
|
|
function getHashLength(hash) {
|
|
return parseInt(hash.name.slice(4), 10);
|
|
}
|
|
function getNamedCurve(alg) {
|
|
switch (alg) {
|
|
case 'ES256':
|
|
return 'P-256';
|
|
case 'ES384':
|
|
return 'P-384';
|
|
case 'ES512':
|
|
return 'P-521';
|
|
default:
|
|
throw new Error('unreachable');
|
|
}
|
|
}
|
|
function checkUsage(key, usage) {
|
|
if (usage && !key.usages.includes(usage)) {
|
|
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
}
|
|
}
|
|
function checkSigCryptoKey(key, alg, usage) {
|
|
switch (alg) {
|
|
case 'HS256':
|
|
case 'HS384':
|
|
case 'HS512': {
|
|
if (!isAlgorithm(key.algorithm, 'HMAC'))
|
|
throw unusable('HMAC');
|
|
const expected = parseInt(alg.slice(2), 10);
|
|
const actual = getHashLength(key.algorithm.hash);
|
|
if (actual !== expected)
|
|
throw unusable(`SHA-${expected}`, 'algorithm.hash');
|
|
break;
|
|
}
|
|
case 'RS256':
|
|
case 'RS384':
|
|
case 'RS512': {
|
|
if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5'))
|
|
throw unusable('RSASSA-PKCS1-v1_5');
|
|
const expected = parseInt(alg.slice(2), 10);
|
|
const actual = getHashLength(key.algorithm.hash);
|
|
if (actual !== expected)
|
|
throw unusable(`SHA-${expected}`, 'algorithm.hash');
|
|
break;
|
|
}
|
|
case 'PS256':
|
|
case 'PS384':
|
|
case 'PS512': {
|
|
if (!isAlgorithm(key.algorithm, 'RSA-PSS'))
|
|
throw unusable('RSA-PSS');
|
|
const expected = parseInt(alg.slice(2), 10);
|
|
const actual = getHashLength(key.algorithm.hash);
|
|
if (actual !== expected)
|
|
throw unusable(`SHA-${expected}`, 'algorithm.hash');
|
|
break;
|
|
}
|
|
case 'Ed25519':
|
|
case 'EdDSA': {
|
|
if (!isAlgorithm(key.algorithm, 'Ed25519'))
|
|
throw unusable('Ed25519');
|
|
break;
|
|
}
|
|
case 'ES256':
|
|
case 'ES384':
|
|
case 'ES512': {
|
|
if (!isAlgorithm(key.algorithm, 'ECDSA'))
|
|
throw unusable('ECDSA');
|
|
const expected = getNamedCurve(alg);
|
|
const actual = key.algorithm.namedCurve;
|
|
if (actual !== expected)
|
|
throw unusable(expected, 'algorithm.namedCurve');
|
|
break;
|
|
}
|
|
default:
|
|
throw new TypeError('CryptoKey does not support this operation');
|
|
}
|
|
checkUsage(key, usage);
|
|
}
|
|
|
|
function message(msg, actual, ...types) {
|
|
types = types.filter(Boolean);
|
|
if (types.length > 2) {
|
|
const last = types.pop();
|
|
msg += `one of type ${types.join(', ')}, or ${last}.`;
|
|
}
|
|
else if (types.length === 2) {
|
|
msg += `one of type ${types[0]} or ${types[1]}.`;
|
|
}
|
|
else {
|
|
msg += `of type ${types[0]}.`;
|
|
}
|
|
if (actual == null) {
|
|
msg += ` Received ${actual}`;
|
|
}
|
|
else if (typeof actual === 'function' && actual.name) {
|
|
msg += ` Received function ${actual.name}`;
|
|
}
|
|
else if (typeof actual === 'object' && actual != null) {
|
|
if (actual.constructor?.name) {
|
|
msg += ` Received an instance of ${actual.constructor.name}`;
|
|
}
|
|
}
|
|
return msg;
|
|
}
|
|
const invalidKeyInput = (actual, ...types) => {
|
|
return message('Key must be ', actual, ...types);
|
|
};
|
|
function withAlg(alg, actual, ...types) {
|
|
return message(`Key for the ${alg} algorithm must be `, actual, ...types);
|
|
}
|
|
|
|
function isCryptoKey(key) {
|
|
return key?.[Symbol.toStringTag] === 'CryptoKey';
|
|
}
|
|
function isKeyObject(key) {
|
|
return key?.[Symbol.toStringTag] === 'KeyObject';
|
|
}
|
|
const isKeyLike = (key) => {
|
|
return isCryptoKey(key) || isKeyObject(key);
|
|
};
|
|
|
|
const isDisjoint = (...headers) => {
|
|
const sources = headers.filter(Boolean);
|
|
if (sources.length === 0 || sources.length === 1) {
|
|
return true;
|
|
}
|
|
let acc;
|
|
for (const header of sources) {
|
|
const parameters = Object.keys(header);
|
|
if (!acc || acc.size === 0) {
|
|
acc = new Set(parameters);
|
|
continue;
|
|
}
|
|
for (const parameter of parameters) {
|
|
if (acc.has(parameter)) {
|
|
return false;
|
|
}
|
|
acc.add(parameter);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
function isObjectLike(value) {
|
|
return typeof value === 'object' && value !== null;
|
|
}
|
|
const isObject = (input) => {
|
|
if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') {
|
|
return false;
|
|
}
|
|
if (Object.getPrototypeOf(input) === null) {
|
|
return true;
|
|
}
|
|
let proto = input;
|
|
while (Object.getPrototypeOf(proto) !== null) {
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
return Object.getPrototypeOf(input) === proto;
|
|
};
|
|
|
|
const checkKeyLength = (alg, key) => {
|
|
if (alg.startsWith('RS') || alg.startsWith('PS')) {
|
|
const { modulusLength } = key.algorithm;
|
|
if (typeof modulusLength !== 'number' || modulusLength < 2048) {
|
|
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
}
|
|
}
|
|
};
|
|
|
|
function subtleMapping(jwk) {
|
|
let algorithm;
|
|
let keyUsages;
|
|
switch (jwk.kty) {
|
|
case 'RSA': {
|
|
switch (jwk.alg) {
|
|
case 'PS256':
|
|
case 'PS384':
|
|
case 'PS512':
|
|
algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'RS256':
|
|
case 'RS384':
|
|
case 'RS512':
|
|
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'RSA-OAEP':
|
|
case 'RSA-OAEP-256':
|
|
case 'RSA-OAEP-384':
|
|
case 'RSA-OAEP-512':
|
|
algorithm = {
|
|
name: 'RSA-OAEP',
|
|
hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`,
|
|
};
|
|
keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey'];
|
|
break;
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
|
}
|
|
break;
|
|
}
|
|
case 'EC': {
|
|
switch (jwk.alg) {
|
|
case 'ES256':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'ES384':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'ES512':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'ECDH-ES':
|
|
case 'ECDH-ES+A128KW':
|
|
case 'ECDH-ES+A192KW':
|
|
case 'ECDH-ES+A256KW':
|
|
algorithm = { name: 'ECDH', namedCurve: jwk.crv };
|
|
keyUsages = jwk.d ? ['deriveBits'] : [];
|
|
break;
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
|
}
|
|
break;
|
|
}
|
|
case 'OKP': {
|
|
switch (jwk.alg) {
|
|
case 'Ed25519':
|
|
case 'EdDSA':
|
|
algorithm = { name: 'Ed25519' };
|
|
keyUsages = jwk.d ? ['sign'] : ['verify'];
|
|
break;
|
|
case 'ECDH-ES':
|
|
case 'ECDH-ES+A128KW':
|
|
case 'ECDH-ES+A192KW':
|
|
case 'ECDH-ES+A256KW':
|
|
algorithm = { name: jwk.crv };
|
|
keyUsages = jwk.d ? ['deriveBits'] : [];
|
|
break;
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value');
|
|
}
|
|
return { algorithm, keyUsages };
|
|
}
|
|
const importJWK = async (jwk) => {
|
|
if (!jwk.alg) {
|
|
throw new TypeError('"alg" argument is required when "jwk.alg" is not present');
|
|
}
|
|
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
const keyData = { ...jwk };
|
|
delete keyData.alg;
|
|
delete keyData.use;
|
|
return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages);
|
|
};
|
|
|
|
const validateCrit = (Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) => {
|
|
if (joseHeader.crit !== undefined && protectedHeader?.crit === undefined) {
|
|
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
|
|
}
|
|
if (!protectedHeader || protectedHeader.crit === undefined) {
|
|
return new Set();
|
|
}
|
|
if (!Array.isArray(protectedHeader.crit) ||
|
|
protectedHeader.crit.length === 0 ||
|
|
protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) {
|
|
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
|
|
}
|
|
let recognized;
|
|
if (recognizedOption !== undefined) {
|
|
recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
|
|
}
|
|
else {
|
|
recognized = recognizedDefault;
|
|
}
|
|
for (const parameter of protectedHeader.crit) {
|
|
if (!recognized.has(parameter)) {
|
|
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
|
|
}
|
|
if (joseHeader[parameter] === undefined) {
|
|
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
|
|
}
|
|
if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {
|
|
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
|
|
}
|
|
}
|
|
return new Set(protectedHeader.crit);
|
|
};
|
|
|
|
function isJWK(key) {
|
|
return isObject(key) && typeof key.kty === 'string';
|
|
}
|
|
function isPrivateJWK(key) {
|
|
return key.kty !== 'oct' && typeof key.d === 'string';
|
|
}
|
|
function isPublicJWK(key) {
|
|
return key.kty !== 'oct' && typeof key.d === 'undefined';
|
|
}
|
|
function isSecretJWK(key) {
|
|
return key.kty === 'oct' && typeof key.k === 'string';
|
|
}
|
|
|
|
let cache;
|
|
const handleJWK = async (key, jwk, alg, freeze = false) => {
|
|
cache ||= new WeakMap();
|
|
let cached = cache.get(key);
|
|
if (cached?.[alg]) {
|
|
return cached[alg];
|
|
}
|
|
const cryptoKey = await importJWK({ ...jwk, alg });
|
|
if (freeze)
|
|
Object.freeze(key);
|
|
if (!cached) {
|
|
cache.set(key, { [alg]: cryptoKey });
|
|
}
|
|
else {
|
|
cached[alg] = cryptoKey;
|
|
}
|
|
return cryptoKey;
|
|
};
|
|
const handleKeyObject = (keyObject, alg) => {
|
|
cache ||= new WeakMap();
|
|
let cached = cache.get(keyObject);
|
|
if (cached?.[alg]) {
|
|
return cached[alg];
|
|
}
|
|
const isPublic = keyObject.type === 'public';
|
|
const extractable = isPublic ? true : false;
|
|
let cryptoKey;
|
|
if (keyObject.asymmetricKeyType === 'x25519') {
|
|
switch (alg) {
|
|
case 'ECDH-ES':
|
|
case 'ECDH-ES+A128KW':
|
|
case 'ECDH-ES+A192KW':
|
|
case 'ECDH-ES+A256KW':
|
|
break;
|
|
default:
|
|
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
|
|
}
|
|
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ['deriveBits']);
|
|
}
|
|
if (keyObject.asymmetricKeyType === 'ed25519') {
|
|
if (alg !== 'EdDSA' && alg !== 'Ed25519') {
|
|
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
|
|
}
|
|
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [
|
|
isPublic ? 'verify' : 'sign',
|
|
]);
|
|
}
|
|
if (keyObject.asymmetricKeyType === 'rsa') {
|
|
let hash;
|
|
switch (alg) {
|
|
case 'RSA-OAEP':
|
|
hash = 'SHA-1';
|
|
break;
|
|
case 'RS256':
|
|
case 'PS256':
|
|
case 'RSA-OAEP-256':
|
|
hash = 'SHA-256';
|
|
break;
|
|
case 'RS384':
|
|
case 'PS384':
|
|
case 'RSA-OAEP-384':
|
|
hash = 'SHA-384';
|
|
break;
|
|
case 'RS512':
|
|
case 'PS512':
|
|
case 'RSA-OAEP-512':
|
|
hash = 'SHA-512';
|
|
break;
|
|
default:
|
|
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
|
|
}
|
|
if (alg.startsWith('RSA-OAEP')) {
|
|
return keyObject.toCryptoKey({
|
|
name: 'RSA-OAEP',
|
|
hash,
|
|
}, extractable, isPublic ? ['encrypt'] : ['decrypt']);
|
|
}
|
|
cryptoKey = keyObject.toCryptoKey({
|
|
name: alg.startsWith('PS') ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5',
|
|
hash,
|
|
}, extractable, [isPublic ? 'verify' : 'sign']);
|
|
}
|
|
if (keyObject.asymmetricKeyType === 'ec') {
|
|
const nist = new Map([
|
|
['prime256v1', 'P-256'],
|
|
['secp384r1', 'P-384'],
|
|
['secp521r1', 'P-521'],
|
|
]);
|
|
const namedCurve = nist.get(keyObject.asymmetricKeyDetails?.namedCurve);
|
|
if (!namedCurve) {
|
|
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
|
|
}
|
|
if (alg === 'ES256' && namedCurve === 'P-256') {
|
|
cryptoKey = keyObject.toCryptoKey({
|
|
name: 'ECDSA',
|
|
namedCurve,
|
|
}, extractable, [isPublic ? 'verify' : 'sign']);
|
|
}
|
|
if (alg === 'ES384' && namedCurve === 'P-384') {
|
|
cryptoKey = keyObject.toCryptoKey({
|
|
name: 'ECDSA',
|
|
namedCurve,
|
|
}, extractable, [isPublic ? 'verify' : 'sign']);
|
|
}
|
|
if (alg === 'ES512' && namedCurve === 'P-521') {
|
|
cryptoKey = keyObject.toCryptoKey({
|
|
name: 'ECDSA',
|
|
namedCurve,
|
|
}, extractable, [isPublic ? 'verify' : 'sign']);
|
|
}
|
|
if (alg.startsWith('ECDH-ES')) {
|
|
cryptoKey = keyObject.toCryptoKey({
|
|
name: 'ECDH',
|
|
namedCurve,
|
|
}, extractable, isPublic ? [] : ['deriveBits']);
|
|
}
|
|
}
|
|
if (!cryptoKey) {
|
|
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
|
|
}
|
|
if (!cached) {
|
|
cache.set(keyObject, { [alg]: cryptoKey });
|
|
}
|
|
else {
|
|
cached[alg] = cryptoKey;
|
|
}
|
|
return cryptoKey;
|
|
};
|
|
const normalizeKey = async (key, alg) => {
|
|
if (key instanceof Uint8Array) {
|
|
return key;
|
|
}
|
|
if (isCryptoKey(key)) {
|
|
return key;
|
|
}
|
|
if (isKeyObject(key)) {
|
|
if (key.type === 'secret') {
|
|
return key.export();
|
|
}
|
|
if ('toCryptoKey' in key && typeof key.toCryptoKey === 'function') {
|
|
try {
|
|
return handleKeyObject(key, alg);
|
|
}
|
|
catch (err) {
|
|
if (err instanceof TypeError) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
let jwk = key.export({ format: 'jwk' });
|
|
return handleJWK(key, jwk, alg);
|
|
}
|
|
if (isJWK(key)) {
|
|
if (key.k) {
|
|
return decode(key.k);
|
|
}
|
|
return handleJWK(key, key, alg, true);
|
|
}
|
|
throw new Error('unreachable');
|
|
};
|
|
|
|
const tag = (key) => key?.[Symbol.toStringTag];
|
|
const jwkMatchesOp = (alg, key, usage) => {
|
|
if (key.use !== undefined) {
|
|
let expected;
|
|
switch (usage) {
|
|
case 'sign':
|
|
case 'verify':
|
|
expected = 'sig';
|
|
break;
|
|
case 'encrypt':
|
|
case 'decrypt':
|
|
expected = 'enc';
|
|
break;
|
|
}
|
|
if (key.use !== expected) {
|
|
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
|
|
}
|
|
}
|
|
if (key.alg !== undefined && key.alg !== alg) {
|
|
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
|
|
}
|
|
if (Array.isArray(key.key_ops)) {
|
|
let expectedKeyOp;
|
|
switch (true) {
|
|
case usage === 'sign' || usage === 'verify':
|
|
case alg === 'dir':
|
|
case alg.includes('CBC-HS'):
|
|
expectedKeyOp = usage;
|
|
break;
|
|
case alg.startsWith('PBES2'):
|
|
expectedKeyOp = 'deriveBits';
|
|
break;
|
|
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
|
|
if (!alg.includes('GCM') && alg.endsWith('KW')) {
|
|
expectedKeyOp = usage === 'encrypt' ? 'wrapKey' : 'unwrapKey';
|
|
}
|
|
else {
|
|
expectedKeyOp = usage;
|
|
}
|
|
break;
|
|
case usage === 'encrypt' && alg.startsWith('RSA'):
|
|
expectedKeyOp = 'wrapKey';
|
|
break;
|
|
case usage === 'decrypt':
|
|
expectedKeyOp = alg.startsWith('RSA') ? 'unwrapKey' : 'deriveBits';
|
|
break;
|
|
}
|
|
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) {
|
|
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
const symmetricTypeCheck = (alg, key, usage) => {
|
|
if (key instanceof Uint8Array)
|
|
return;
|
|
if (isJWK(key)) {
|
|
if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
return;
|
|
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
|
|
}
|
|
if (!isKeyLike(key)) {
|
|
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array'));
|
|
}
|
|
if (key.type !== 'secret') {
|
|
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
|
|
}
|
|
};
|
|
const asymmetricTypeCheck = (alg, key, usage) => {
|
|
if (isJWK(key)) {
|
|
switch (usage) {
|
|
case 'decrypt':
|
|
case 'sign':
|
|
if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
return;
|
|
throw new TypeError(`JSON Web Key for this operation be a private JWK`);
|
|
case 'encrypt':
|
|
case 'verify':
|
|
if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
return;
|
|
throw new TypeError(`JSON Web Key for this operation be a public JWK`);
|
|
}
|
|
}
|
|
if (!isKeyLike(key)) {
|
|
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
|
|
}
|
|
if (key.type === 'secret') {
|
|
throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
|
|
}
|
|
if (key.type === 'public') {
|
|
switch (usage) {
|
|
case 'sign':
|
|
throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
|
|
case 'decrypt':
|
|
throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
|
|
}
|
|
}
|
|
if (key.type === 'private') {
|
|
switch (usage) {
|
|
case 'verify':
|
|
throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
|
|
case 'encrypt':
|
|
throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
|
|
}
|
|
}
|
|
};
|
|
const checkKeyType = (alg, key, usage) => {
|
|
const symmetric = alg.startsWith('HS') ||
|
|
alg === 'dir' ||
|
|
alg.startsWith('PBES2') ||
|
|
/^A(?:128|192|256)(?:GCM)?(?:KW)?$/.test(alg) ||
|
|
/^A(?:128|192|256)CBC-HS(?:256|384|512)$/.test(alg);
|
|
if (symmetric) {
|
|
symmetricTypeCheck(alg, key, usage);
|
|
}
|
|
else {
|
|
asymmetricTypeCheck(alg, key, usage);
|
|
}
|
|
};
|
|
|
|
const subtleAlgorithm = (alg, algorithm) => {
|
|
const hash = `SHA-${alg.slice(-3)}`;
|
|
switch (alg) {
|
|
case 'HS256':
|
|
case 'HS384':
|
|
case 'HS512':
|
|
return { hash, name: 'HMAC' };
|
|
case 'PS256':
|
|
case 'PS384':
|
|
case 'PS512':
|
|
return { hash, name: 'RSA-PSS', saltLength: parseInt(alg.slice(-3), 10) >> 3 };
|
|
case 'RS256':
|
|
case 'RS384':
|
|
case 'RS512':
|
|
return { hash, name: 'RSASSA-PKCS1-v1_5' };
|
|
case 'ES256':
|
|
case 'ES384':
|
|
case 'ES512':
|
|
return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve };
|
|
case 'Ed25519':
|
|
case 'EdDSA':
|
|
return { name: 'Ed25519' };
|
|
default:
|
|
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
}
|
|
};
|
|
|
|
const getSignKey = async (alg, key, usage) => {
|
|
if (key instanceof Uint8Array) {
|
|
if (!alg.startsWith('HS')) {
|
|
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
|
|
}
|
|
return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]);
|
|
}
|
|
checkSigCryptoKey(key, alg, usage);
|
|
return key;
|
|
};
|
|
|
|
const verify = async (alg, key, signature, data) => {
|
|
const cryptoKey = await getSignKey(alg, key, 'verify');
|
|
checkKeyLength(alg, cryptoKey);
|
|
const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
|
|
try {
|
|
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
async function flattenedVerify(jws, key, options) {
|
|
if (!isObject(jws)) {
|
|
throw new JWSInvalid('Flattened JWS must be an object');
|
|
}
|
|
if (jws.protected === undefined && jws.header === undefined) {
|
|
throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members');
|
|
}
|
|
if (jws.protected !== undefined && typeof jws.protected !== 'string') {
|
|
throw new JWSInvalid('JWS Protected Header incorrect type');
|
|
}
|
|
if (jws.payload === undefined) {
|
|
throw new JWSInvalid('JWS Payload missing');
|
|
}
|
|
if (typeof jws.signature !== 'string') {
|
|
throw new JWSInvalid('JWS Signature missing or incorrect type');
|
|
}
|
|
if (jws.header !== undefined && !isObject(jws.header)) {
|
|
throw new JWSInvalid('JWS Unprotected Header incorrect type');
|
|
}
|
|
let parsedProt = {};
|
|
if (jws.protected) {
|
|
try {
|
|
const protectedHeader = decode(jws.protected);
|
|
parsedProt = JSON.parse(decoder.decode(protectedHeader));
|
|
}
|
|
catch {
|
|
throw new JWSInvalid('JWS Protected Header is invalid');
|
|
}
|
|
}
|
|
if (!isDisjoint(parsedProt, jws.header)) {
|
|
throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
|
|
}
|
|
const joseHeader = {
|
|
...parsedProt,
|
|
...jws.header,
|
|
};
|
|
const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options?.crit, parsedProt, joseHeader);
|
|
let b64 = true;
|
|
if (extensions.has('b64')) {
|
|
b64 = parsedProt.b64;
|
|
if (typeof b64 !== 'boolean') {
|
|
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
|
|
}
|
|
}
|
|
const { alg } = joseHeader;
|
|
if (typeof alg !== 'string' || !alg) {
|
|
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
|
|
}
|
|
if (b64) {
|
|
if (typeof jws.payload !== 'string') {
|
|
throw new JWSInvalid('JWS Payload must be a string');
|
|
}
|
|
}
|
|
else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) {
|
|
throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance');
|
|
}
|
|
let resolvedKey = false;
|
|
if (typeof key === 'function') {
|
|
key = await key(parsedProt, jws);
|
|
resolvedKey = true;
|
|
}
|
|
checkKeyType(alg, key, 'verify');
|
|
const data = concat(encoder.encode(jws.protected ?? ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload);
|
|
let signature;
|
|
try {
|
|
signature = decode(jws.signature);
|
|
}
|
|
catch {
|
|
throw new JWSInvalid('Failed to base64url decode the signature');
|
|
}
|
|
const k = await normalizeKey(key, alg);
|
|
const verified = await verify(alg, k, signature, data);
|
|
if (!verified) {
|
|
throw new JWSSignatureVerificationFailed();
|
|
}
|
|
let payload;
|
|
if (b64) {
|
|
try {
|
|
payload = decode(jws.payload);
|
|
}
|
|
catch {
|
|
throw new JWSInvalid('Failed to base64url decode the payload');
|
|
}
|
|
}
|
|
else if (typeof jws.payload === 'string') {
|
|
payload = encoder.encode(jws.payload);
|
|
}
|
|
else {
|
|
payload = jws.payload;
|
|
}
|
|
const result = { payload };
|
|
if (jws.protected !== undefined) {
|
|
result.protectedHeader = parsedProt;
|
|
}
|
|
if (jws.header !== undefined) {
|
|
result.unprotectedHeader = jws.header;
|
|
}
|
|
if (resolvedKey) {
|
|
return { ...result, key: k };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function compactVerify(jws, key, options) {
|
|
if (jws instanceof Uint8Array) {
|
|
jws = decoder.decode(jws);
|
|
}
|
|
if (typeof jws !== 'string') {
|
|
throw new JWSInvalid('Compact JWS must be a string or Uint8Array');
|
|
}
|
|
const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');
|
|
if (length !== 3) {
|
|
throw new JWSInvalid('Invalid Compact JWS');
|
|
}
|
|
const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options);
|
|
const result = { payload: verified.payload, protectedHeader: verified.protectedHeader };
|
|
if (typeof key === 'function') {
|
|
return { ...result, key: verified.key };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const epoch = (date) => Math.floor(date.getTime() / 1000);
|
|
|
|
const minute = 60;
|
|
const hour = minute * 60;
|
|
const day = hour * 24;
|
|
const week = day * 7;
|
|
const year = day * 365.25;
|
|
const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
|
|
const secs = (str) => {
|
|
const matched = REGEX.exec(str);
|
|
if (!matched || (matched[4] && matched[1])) {
|
|
throw new TypeError('Invalid time period format');
|
|
}
|
|
const value = parseFloat(matched[2]);
|
|
const unit = matched[3].toLowerCase();
|
|
let numericDate;
|
|
switch (unit) {
|
|
case 'sec':
|
|
case 'secs':
|
|
case 'second':
|
|
case 'seconds':
|
|
case 's':
|
|
numericDate = Math.round(value);
|
|
break;
|
|
case 'minute':
|
|
case 'minutes':
|
|
case 'min':
|
|
case 'mins':
|
|
case 'm':
|
|
numericDate = Math.round(value * minute);
|
|
break;
|
|
case 'hour':
|
|
case 'hours':
|
|
case 'hr':
|
|
case 'hrs':
|
|
case 'h':
|
|
numericDate = Math.round(value * hour);
|
|
break;
|
|
case 'day':
|
|
case 'days':
|
|
case 'd':
|
|
numericDate = Math.round(value * day);
|
|
break;
|
|
case 'week':
|
|
case 'weeks':
|
|
case 'w':
|
|
numericDate = Math.round(value * week);
|
|
break;
|
|
default:
|
|
numericDate = Math.round(value * year);
|
|
break;
|
|
}
|
|
if (matched[1] === '-' || matched[4] === 'ago') {
|
|
return -numericDate;
|
|
}
|
|
return numericDate;
|
|
};
|
|
|
|
function validateInput(label, input) {
|
|
if (!Number.isFinite(input)) {
|
|
throw new TypeError(`Invalid ${label} input`);
|
|
}
|
|
return input;
|
|
}
|
|
const normalizeTyp = (value) => {
|
|
if (value.includes('/')) {
|
|
return value.toLowerCase();
|
|
}
|
|
return `application/${value.toLowerCase()}`;
|
|
};
|
|
const checkAudiencePresence = (audPayload, audOption) => {
|
|
if (typeof audPayload === 'string') {
|
|
return audOption.includes(audPayload);
|
|
}
|
|
if (Array.isArray(audPayload)) {
|
|
return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
|
|
}
|
|
return false;
|
|
};
|
|
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(decoder.decode(encodedPayload));
|
|
}
|
|
catch {
|
|
}
|
|
if (!isObject(payload)) {
|
|
throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');
|
|
}
|
|
const { typ } = options;
|
|
if (typ &&
|
|
(typeof protectedHeader.typ !== 'string' ||
|
|
normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
|
|
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, 'typ', 'check_failed');
|
|
}
|
|
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
|
|
const presenceCheck = [...requiredClaims];
|
|
if (maxTokenAge !== undefined)
|
|
presenceCheck.push('iat');
|
|
if (audience !== undefined)
|
|
presenceCheck.push('aud');
|
|
if (subject !== undefined)
|
|
presenceCheck.push('sub');
|
|
if (issuer !== undefined)
|
|
presenceCheck.push('iss');
|
|
for (const claim of new Set(presenceCheck.reverse())) {
|
|
if (!(claim in payload)) {
|
|
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, 'missing');
|
|
}
|
|
}
|
|
if (issuer &&
|
|
!(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
|
|
throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, 'iss', 'check_failed');
|
|
}
|
|
if (subject && payload.sub !== subject) {
|
|
throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, 'sub', 'check_failed');
|
|
}
|
|
if (audience &&
|
|
!checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {
|
|
throw new JWTClaimValidationFailed('unexpected "aud" claim value', payload, 'aud', 'check_failed');
|
|
}
|
|
let tolerance;
|
|
switch (typeof options.clockTolerance) {
|
|
case 'string':
|
|
tolerance = secs(options.clockTolerance);
|
|
break;
|
|
case 'number':
|
|
tolerance = options.clockTolerance;
|
|
break;
|
|
case 'undefined':
|
|
tolerance = 0;
|
|
break;
|
|
default:
|
|
throw new TypeError('Invalid clockTolerance option type');
|
|
}
|
|
const { currentDate } = options;
|
|
const now = epoch(currentDate || new Date());
|
|
if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {
|
|
throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, 'iat', 'invalid');
|
|
}
|
|
if (payload.nbf !== undefined) {
|
|
if (typeof payload.nbf !== 'number') {
|
|
throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, 'nbf', 'invalid');
|
|
}
|
|
if (payload.nbf > now + tolerance) {
|
|
throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, 'nbf', 'check_failed');
|
|
}
|
|
}
|
|
if (payload.exp !== undefined) {
|
|
if (typeof payload.exp !== 'number') {
|
|
throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, 'exp', 'invalid');
|
|
}
|
|
if (payload.exp <= now - tolerance) {
|
|
throw new JWTExpired('"exp" claim timestamp check failed', payload, 'exp', 'check_failed');
|
|
}
|
|
}
|
|
if (maxTokenAge) {
|
|
const age = now - payload.iat;
|
|
const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge);
|
|
if (age - tolerance > max) {
|
|
throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, 'iat', 'check_failed');
|
|
}
|
|
if (age < 0 - tolerance) {
|
|
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', 'check_failed');
|
|
}
|
|
}
|
|
return payload;
|
|
}
|
|
class JWTClaimsBuilder {
|
|
#payload;
|
|
constructor(payload) {
|
|
if (!isObject(payload)) {
|
|
throw new TypeError('JWT Claims Set MUST be an object');
|
|
}
|
|
this.#payload = structuredClone(payload);
|
|
}
|
|
data() {
|
|
return encoder.encode(JSON.stringify(this.#payload));
|
|
}
|
|
get iss() {
|
|
return this.#payload.iss;
|
|
}
|
|
set iss(value) {
|
|
this.#payload.iss = value;
|
|
}
|
|
get sub() {
|
|
return this.#payload.sub;
|
|
}
|
|
set sub(value) {
|
|
this.#payload.sub = value;
|
|
}
|
|
get aud() {
|
|
return this.#payload.aud;
|
|
}
|
|
set aud(value) {
|
|
this.#payload.aud = value;
|
|
}
|
|
set jti(value) {
|
|
this.#payload.jti = value;
|
|
}
|
|
set nbf(value) {
|
|
if (typeof value === 'number') {
|
|
this.#payload.nbf = validateInput('setNotBefore', value);
|
|
}
|
|
else if (value instanceof Date) {
|
|
this.#payload.nbf = validateInput('setNotBefore', epoch(value));
|
|
}
|
|
else {
|
|
this.#payload.nbf = epoch(new Date()) + secs(value);
|
|
}
|
|
}
|
|
set exp(value) {
|
|
if (typeof value === 'number') {
|
|
this.#payload.exp = validateInput('setExpirationTime', value);
|
|
}
|
|
else if (value instanceof Date) {
|
|
this.#payload.exp = validateInput('setExpirationTime', epoch(value));
|
|
}
|
|
else {
|
|
this.#payload.exp = epoch(new Date()) + secs(value);
|
|
}
|
|
}
|
|
set iat(value) {
|
|
if (typeof value === 'undefined') {
|
|
this.#payload.iat = epoch(new Date());
|
|
}
|
|
else if (value instanceof Date) {
|
|
this.#payload.iat = validateInput('setIssuedAt', epoch(value));
|
|
}
|
|
else if (typeof value === 'string') {
|
|
this.#payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value));
|
|
}
|
|
else {
|
|
this.#payload.iat = validateInput('setIssuedAt', value);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function jwtVerify(jwt, key, options) {
|
|
const verified = await compactVerify(jwt, key, options);
|
|
if (verified.protectedHeader.crit?.includes('b64') && verified.protectedHeader.b64 === false) {
|
|
throw new JWTInvalid('JWTs MUST NOT use unencoded payload');
|
|
}
|
|
const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options);
|
|
const result = { payload, protectedHeader: verified.protectedHeader };
|
|
if (typeof key === 'function') {
|
|
return { ...result, key: verified.key };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const sign = async (alg, key, data) => {
|
|
const cryptoKey = await getSignKey(alg, key, 'sign');
|
|
checkKeyLength(alg, cryptoKey);
|
|
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
|
|
return new Uint8Array(signature);
|
|
};
|
|
|
|
class FlattenedSign {
|
|
#payload;
|
|
#protectedHeader;
|
|
#unprotectedHeader;
|
|
constructor(payload) {
|
|
if (!(payload instanceof Uint8Array)) {
|
|
throw new TypeError('payload must be an instance of Uint8Array');
|
|
}
|
|
this.#payload = payload;
|
|
}
|
|
setProtectedHeader(protectedHeader) {
|
|
if (this.#protectedHeader) {
|
|
throw new TypeError('setProtectedHeader can only be called once');
|
|
}
|
|
this.#protectedHeader = protectedHeader;
|
|
return this;
|
|
}
|
|
setUnprotectedHeader(unprotectedHeader) {
|
|
if (this.#unprotectedHeader) {
|
|
throw new TypeError('setUnprotectedHeader can only be called once');
|
|
}
|
|
this.#unprotectedHeader = unprotectedHeader;
|
|
return this;
|
|
}
|
|
async sign(key, options) {
|
|
if (!this.#protectedHeader && !this.#unprotectedHeader) {
|
|
throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()');
|
|
}
|
|
if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) {
|
|
throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
|
|
}
|
|
const joseHeader = {
|
|
...this.#protectedHeader,
|
|
...this.#unprotectedHeader,
|
|
};
|
|
const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options?.crit, this.#protectedHeader, joseHeader);
|
|
let b64 = true;
|
|
if (extensions.has('b64')) {
|
|
b64 = this.#protectedHeader.b64;
|
|
if (typeof b64 !== 'boolean') {
|
|
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
|
|
}
|
|
}
|
|
const { alg } = joseHeader;
|
|
if (typeof alg !== 'string' || !alg) {
|
|
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
|
|
}
|
|
checkKeyType(alg, key, 'sign');
|
|
let payload = this.#payload;
|
|
if (b64) {
|
|
payload = encoder.encode(encode(payload));
|
|
}
|
|
let protectedHeader;
|
|
if (this.#protectedHeader) {
|
|
protectedHeader = encoder.encode(encode(JSON.stringify(this.#protectedHeader)));
|
|
}
|
|
else {
|
|
protectedHeader = encoder.encode('');
|
|
}
|
|
const data = concat(protectedHeader, encoder.encode('.'), payload);
|
|
const k = await normalizeKey(key, alg);
|
|
const signature = await sign(alg, k, data);
|
|
const jws = {
|
|
signature: encode(signature),
|
|
payload: '',
|
|
};
|
|
if (b64) {
|
|
jws.payload = decoder.decode(payload);
|
|
}
|
|
if (this.#unprotectedHeader) {
|
|
jws.header = this.#unprotectedHeader;
|
|
}
|
|
if (this.#protectedHeader) {
|
|
jws.protected = decoder.decode(protectedHeader);
|
|
}
|
|
return jws;
|
|
}
|
|
}
|
|
|
|
class CompactSign {
|
|
#flattened;
|
|
constructor(payload) {
|
|
this.#flattened = new FlattenedSign(payload);
|
|
}
|
|
setProtectedHeader(protectedHeader) {
|
|
this.#flattened.setProtectedHeader(protectedHeader);
|
|
return this;
|
|
}
|
|
async sign(key, options) {
|
|
const jws = await this.#flattened.sign(key, options);
|
|
if (jws.payload === undefined) {
|
|
throw new TypeError('use the flattened module for creating JWS with b64: false');
|
|
}
|
|
return `${jws.protected}.${jws.payload}.${jws.signature}`;
|
|
}
|
|
}
|
|
|
|
class SignJWT {
|
|
#protectedHeader;
|
|
#jwt;
|
|
constructor(payload = {}) {
|
|
this.#jwt = new JWTClaimsBuilder(payload);
|
|
}
|
|
setIssuer(issuer) {
|
|
this.#jwt.iss = issuer;
|
|
return this;
|
|
}
|
|
setSubject(subject) {
|
|
this.#jwt.sub = subject;
|
|
return this;
|
|
}
|
|
setAudience(audience) {
|
|
this.#jwt.aud = audience;
|
|
return this;
|
|
}
|
|
setJti(jwtId) {
|
|
this.#jwt.jti = jwtId;
|
|
return this;
|
|
}
|
|
setNotBefore(input) {
|
|
this.#jwt.nbf = input;
|
|
return this;
|
|
}
|
|
setExpirationTime(input) {
|
|
this.#jwt.exp = input;
|
|
return this;
|
|
}
|
|
setIssuedAt(input) {
|
|
this.#jwt.iat = input;
|
|
return this;
|
|
}
|
|
setProtectedHeader(protectedHeader) {
|
|
this.#protectedHeader = protectedHeader;
|
|
return this;
|
|
}
|
|
async sign(key, options) {
|
|
const sig = new CompactSign(this.#jwt.data());
|
|
sig.setProtectedHeader(this.#protectedHeader);
|
|
if (Array.isArray(this.#protectedHeader?.crit) &&
|
|
this.#protectedHeader.crit.includes('b64') &&
|
|
this.#protectedHeader.b64 === false) {
|
|
throw new JWTInvalid('JWTs MUST NOT use unencoded payload');
|
|
}
|
|
return sig.sign(key, options);
|
|
}
|
|
}
|
|
|
|
class TokenService {
|
|
static instance;
|
|
SECRET_KEY = (() => {
|
|
const viteEnv = (() => {
|
|
try {
|
|
return (0, eval)("import.meta")?.env;
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
})();
|
|
const viteKey = viteEnv?.VITE_JWT_SECRET_KEY;
|
|
const nodeKey = globalThis?.process?.env?.VITE_JWT_SECRET_KEY;
|
|
const resolved = viteKey || nodeKey || "";
|
|
return resolved;
|
|
})();
|
|
ACCESS_TOKEN_EXPIRATION = "30s";
|
|
REFRESH_TOKEN_EXPIRATION = "7d";
|
|
encoder = new TextEncoder();
|
|
constructor() {
|
|
}
|
|
static async getInstance() {
|
|
if (!TokenService.instance) {
|
|
TokenService.instance = new TokenService();
|
|
}
|
|
return TokenService.instance;
|
|
}
|
|
async generateSessionToken(origin) {
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
const accessToken = await new SignJWT({ origin, type: "access" }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(this.ACCESS_TOKEN_EXPIRATION).sign(secret);
|
|
const refreshToken = await new SignJWT({ origin, type: "refresh" }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(this.REFRESH_TOKEN_EXPIRATION).sign(secret);
|
|
return { accessToken, refreshToken };
|
|
}
|
|
async validateToken(token, origin) {
|
|
try {
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
const { payload } = await jwtVerify(token, secret);
|
|
return payload.origin === origin;
|
|
} catch (error) {
|
|
if (error?.code === "ERR_JWT_EXPIRED") {
|
|
console.log("Token expiré");
|
|
return false;
|
|
}
|
|
console.error("Erreur de validation du token:", error);
|
|
return false;
|
|
}
|
|
}
|
|
async refreshAccessToken(refreshToken, origin) {
|
|
try {
|
|
const isValid = await this.validateToken(refreshToken, origin);
|
|
if (!isValid) {
|
|
return null;
|
|
}
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
const { payload } = await jwtVerify(refreshToken, secret);
|
|
if (payload.type !== "refresh") {
|
|
return null;
|
|
}
|
|
const newAccessToken = await new SignJWT({ origin, type: "access" }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(this.ACCESS_TOKEN_EXPIRATION).sign(secret);
|
|
return newAccessToken;
|
|
} catch (error) {
|
|
console.error("Erreur lors du refresh du token:", error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const loginHtml = "<div class=\"title-container\">\r\n <h1>Create Account / New Session</h1>\r\n</div>\r\n\r\n<div class=\"tab-container\">\r\n <div class=\"tabs\">\r\n <div class=\"tab active\" data-tab=\"tab1\">Create an account</div>\r\n <div class=\"tab\" data-tab=\"tab2\">Add a device for an existing memeber</div>\r\n </div>\r\n</div>\r\n\r\n<div class=\"page-container\">\r\n <div id=\"tab1\" class=\"card tab-content active\">\r\n <div class=\"card-description\">Create an account :</div>\r\n <div class=\"pairing-request\"></div>\r\n <!-- <div class=\"card-image qr-code\">\r\n <img src=\"assets/qr_code.png\" alt=\"QR Code\" width=\"150\" height=\"150\" />\r\n </div> -->\r\n <button id=\"createButton\" class=\"create-btn\"></button>\r\n </div>\r\n <div class=\"separator\"></div>\r\n <div id=\"tab2\" class=\"card tab-content\">\r\n <div class=\"card-description\">Add a device for an existing member :</div>\r\n <div class=\"card-image camera-card\">\r\n <img id=\"scanner\" src=\"assets/camera.jpg\" alt=\"QR Code\" width=\"150\" height=\"150\" />\r\n <button id=\"scan-btn\" onclick=\"scanDevice()\">Scan</button>\r\n <div class=\"qr-code-scanner\">\r\n <div id=\"qr-reader\" style=\"width: 200px; display: contents\"></div>\r\n <div id=\"qr-reader-results\"></div>\r\n </div>\r\n </div>\r\n <p>Or</p>\r\n <!-- <input type=\"text\" id=\"addressInput\" placeholder=\"Paste address\" />\r\n <div id=\"emoji-display-2\"></div> -->\r\n <div class=\"card-description\">Chose a member :</div>\r\n <select name=\"memberSelect\" id=\"memberSelect\" size=\"5\" class=\"custom-select\">\r\n <!-- Options -->\r\n </select>\r\n\r\n <button id=\"okButton\" style=\"display: none\">OK</button>\r\n </div>\r\n</div>\r\n";
|
|
|
|
const loginScript = "import Routing from '../../services/modal.service';\r\nimport Services from '../../services/service';\r\nimport { addSubscription } from '../../utils/subscription.utils';\r\nimport { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';\r\nimport { getCorrectDOM } from '../../utils/html.utils';\r\nimport QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';\r\nimport { navigate, registerAllListeners } from '../../router';\r\n\r\nexport { QrScannerComponent };\r\nexport async function initHomePage(): Promise<void> {\r\n console.log('INIT-HOME');\r\n const container = getCorrectDOM('login-4nk-component') as HTMLElement;\r\n container.querySelectorAll('.tab').forEach((tab) => {\r\n addSubscription(tab, 'click', () => {\r\n container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));\r\n tab.classList.add('active');\r\n\r\n container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));\r\n container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active');\r\n });\r\n });\r\n\r\n const service = await Services.getInstance();\r\n const spAddress = await service.getDeviceAddress();\r\n // generateQRCode(spAddress);\r\n generateCreateBtn();\r\n displayEmojis(spAddress);\r\n\r\n // Add this line to populate the select when the page loads\r\n await populateMemberSelect();\r\n}\r\n\r\n//// Modal\r\nexport async function openModal(myAddress: string, receiverAddress: string) {\r\n const router = await Routing.getInstance();\r\n router.openLoginModal(myAddress, receiverAddress);\r\n}\r\n\r\n// const service = await Services.getInstance()\r\n// service.setNotification()\r\n\r\nfunction scanDevice() {\r\n const container = getCorrectDOM('login-4nk-component') as HTMLElement;\r\n const scannerImg = container.querySelector('#scanner') as HTMLElement;\r\n if (scannerImg) scannerImg.style.display = 'none';\r\n const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;\r\n if (scannerQrCode) scannerQrCode.style.display = 'block';\r\n const scanButton = container?.querySelector('#scan-btn') as HTMLElement;\r\n if (scanButton) scanButton.style.display = 'none';\r\n const reader = container?.querySelector('#qr-reader');\r\n if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';\r\n}\r\n\r\nasync function populateMemberSelect() {\r\n const container = getCorrectDOM('login-4nk-component') as HTMLElement;\r\n const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;\r\n\r\n if (!memberSelect) {\r\n console.error('Could not find memberSelect element');\r\n return;\r\n }\r\n\r\n const service = await Services.getInstance();\r\n const members = await service.getAllMembersSorted();\r\n\r\n for (const [processId, member] of Object.entries(members)) {\r\n const process = await service.getProcess(processId);\r\n let memberPublicName;\r\n\r\n if (process) {\r\n const publicMemberData = service.getPublicData(process);\r\n if (publicMemberData) {\r\n const extractedName = publicMemberData['memberPublicName'];\r\n if (extractedName !== undefined && extractedName !== null) {\r\n memberPublicName = extractedName;\r\n }\r\n }\r\n }\r\n\r\n if (!memberPublicName) {\r\n memberPublicName = 'Unnamed Member';\r\n }\r\n\r\n // Récupérer les emojis pour ce processId\r\n const emojis = await addressToEmoji(processId);\r\n\r\n const option = document.createElement('option');\r\n option.value = processId;\r\n option.textContent = `${memberPublicName} (${emojis})`;\r\n memberSelect.appendChild(option);\r\n }\r\n}\r\n\r\n(window as any).populateMemberSelect = populateMemberSelect;\r\n\r\n(window as any).scanDevice = scanDevice;\r\n";
|
|
|
|
const loginCss = ":host {\r\n --primary-color: #3a506b;\r\n /* Bleu métallique */\r\n --secondary-color: #b0bec5;\r\n /* Gris acier */\r\n --accent-color: #d68c45;\r\n /* Cuivre */\r\n font-family: Arial, sans-serif;\r\n height: 100vh;\r\n font-size: 16px;\r\n}\r\nbody {\r\n font-family: Arial, sans-serif;\r\n margin: 0;\r\n padding: 0;\r\n background-color: #f4f4f4;\r\n background-image: url(../assets/bgd.webp);\r\n background-repeat: no-repeat;\r\n background-size: cover;\r\n background-blend-mode: soft-light;\r\n height: 100vh;\r\n}\r\n.message {\r\n margin: 30px 0;\r\n font-size: 14px;\r\n overflow-wrap: anywhere;\r\n}\r\n\r\n.message strong {\r\n font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;\r\n font-size: 20px;\r\n}\r\n\r\n/** Modal Css */\r\n.modal {\r\n display: none;\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n background-color: rgba(0, 0, 0, 0.5);\r\n justify-content: center;\r\n align-items: center;\r\n z-index: 3;\r\n}\r\n\r\n.modal-content {\r\n width: 55%;\r\n height: 30%;\r\n background-color: white;\r\n border-radius: 4px;\r\n padding: 20px;\r\n text-align: center;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n}\r\n\r\n.modal-title {\r\n margin: 0;\r\n padding-bottom: 8px;\r\n width: 100%;\r\n font-size: 0.9rem;\r\n border-bottom: 1px solid #ccc;\r\n}\r\n\r\n.confirmation-box {\r\n /* margin-top: 20px; */\r\n align-content: center;\r\n width: 70%;\r\n height: 20%;\r\n /* padding: 20px; */\r\n font-size: 1.5em;\r\n color: #333333;\r\n top: 5%;\r\n position: relative;\r\n}\r\n\r\n.nav-wrapper {\r\n position: fixed;\r\n background: radial-gradient(circle, white, var(--primary-color));\r\n /* background-color: #CFD8DC; */\r\n display: flex;\r\n justify-content: flex-end;\r\n align-items: center;\r\n color: #37474f;\r\n height: 9vh;\r\n width: 100vw;\r\n left: 0;\r\n top: 0;\r\n box-shadow:\r\n 0px 8px 10px -5px rgba(0, 0, 0, 0.2),\r\n 0px 16px 24px 2px rgba(0, 0, 0, 0.14),\r\n 0px 6px 30px 5px rgba(0, 0, 0, 0.12);\r\n\r\n .nav-right-icons {\r\n display: flex;\r\n .notification-container {\r\n position: relative;\r\n display: inline-block;\r\n }\r\n .notification-bell,\r\n .burger-menu {\r\n z-index: 3;\r\n height: 20px;\r\n width: 20px;\r\n margin-right: 1rem;\r\n }\r\n .notification-badge {\r\n position: absolute;\r\n top: -0.7rem;\r\n left: -0.8rem;\r\n background-color: red;\r\n color: white;\r\n border-radius: 50%;\r\n padding: 2.5px 6px;\r\n font-size: 0.8rem;\r\n font-weight: bold;\r\n }\r\n }\r\n .notification-board {\r\n position: absolute;\r\n width: 20rem;\r\n min-height: 8rem;\r\n background-color: white;\r\n right: 0.5rem;\r\n display: none;\r\n border-radius: 4px;\r\n text-align: center;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\r\n display: none;\r\n\r\n .notification-element {\r\n padding: 0.8rem 0;\r\n width: 100%;\r\n &:hover {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n }\r\n .notification-element:not(:last-child) {\r\n border-bottom: 1px solid;\r\n }\r\n }\r\n}\r\n\r\n.brand-logo {\r\n height: 100%;\r\n width: 100vw;\r\n align-content: center;\r\n position: relative;\r\n display: flex;\r\n position: absolute;\r\n align-items: center;\r\n justify-content: center;\r\n text-align: center;\r\n font-size: 1.5em;\r\n font-weight: bold;\r\n}\r\n\r\n.container {\r\n text-align: center;\r\n display: grid;\r\n height: 100vh;\r\n grid-template-columns: repeat(7, 1fr);\r\n gap: 10px;\r\n grid-auto-rows: 10vh 15vh 1fr;\r\n}\r\n.title-container {\r\n grid-column: 2 / 7;\r\n grid-row: 2;\r\n}\r\n.page-container {\r\n grid-column: 2 / 7;\r\n grid-row: 3;\r\n justify-content: center;\r\n display: flex;\r\n padding: 1rem;\r\n box-sizing: border-box;\r\n max-height: 60vh;\r\n}\r\n\r\nh1 {\r\n font-size: 2em;\r\n margin: 20px 0;\r\n}\r\n@media only screen and (min-width: 600px) {\r\n .tab-container {\r\n display: none;\r\n }\r\n .page-container {\r\n display: flex;\r\n align-items: center;\r\n }\r\n .process-container {\r\n grid-column: 3 / 6;\r\n grid-row: 3;\r\n\r\n .card {\r\n min-width: 40vw;\r\n }\r\n }\r\n .separator {\r\n width: 2px;\r\n background-color: #78909c;\r\n height: 80%;\r\n margin: 0 0.5em;\r\n }\r\n .tab-content {\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: space-evenly;\r\n align-items: center;\r\n height: 80%;\r\n }\r\n}\r\n\r\n@media only screen and (max-width: 600px) {\r\n .process-container {\r\n grid-column: 2 / 7;\r\n grid-row: 3;\r\n }\r\n .container {\r\n grid-auto-rows: 10vh 15vh 15vh 1fr;\r\n }\r\n .tab-container {\r\n grid-column: 1 / 8;\r\n grid-row: 3;\r\n }\r\n .page-container {\r\n grid-column: 2 / 7;\r\n grid-row: 4;\r\n }\r\n .separator {\r\n display: none;\r\n }\r\n .tabs {\r\n display: flex;\r\n flex-grow: 1;\r\n overflow: hidden;\r\n z-index: 1;\r\n border-bottom-style: solid;\r\n border-bottom-width: 1px;\r\n border-bottom-color: #e0e4d6;\r\n }\r\n\r\n .tab {\r\n flex: 1;\r\n text-align: center;\r\n padding: 10px 0;\r\n cursor: pointer;\r\n font-size: 1rem;\r\n color: #6200ea;\r\n &:hover {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n }\r\n .tab.active {\r\n border-bottom: 2px solid #6200ea;\r\n font-weight: bold;\r\n }\r\n\r\n .card.tab-content {\r\n display: none;\r\n }\r\n\r\n .tab-content.active {\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n align-items: center;\r\n height: 80%;\r\n }\r\n .modal-content {\r\n width: 80%;\r\n height: 20%;\r\n }\r\n}\r\n\r\n.qr-code {\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n height: 200px;\r\n}\r\n\r\n.emoji-display {\r\n font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;\r\n font-size: 20px;\r\n}\r\n\r\n#emoji-display-2 {\r\n margin-top: 30px;\r\n font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;\r\n font-size: 20px;\r\n}\r\n\r\n#okButton {\r\n margin-bottom: 2em;\r\n cursor: pointer;\r\n background-color: #d0d0d7;\r\n color: white;\r\n border-style: none;\r\n border-radius: 5px;\r\n color: #000;\r\n padding: 2px;\r\n margin-top: 10px;\r\n}\r\n\r\n.pairing-request {\r\n font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;\r\n font-size: 14px;\r\n margin-top: 0px;\r\n}\r\n\r\n.create-btn {\r\n margin-bottom: 2em;\r\n cursor: pointer;\r\n background-color: #d0d0d7;\r\n color: white;\r\n border-style: none;\r\n border-radius: 5px;\r\n color: #000;\r\n padding: 2px;\r\n}\r\n\r\n.camera-card {\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n flex-direction: column;\r\n /* height: 200px; */\r\n}\r\n\r\n.btn {\r\n display: inline-block;\r\n padding: 10px 20px;\r\n background-color: var(--primary-color);\r\n color: white;\r\n text-align: center;\r\n border-radius: 5px;\r\n cursor: pointer;\r\n text-decoration: none;\r\n}\r\n\r\n.btn:hover {\r\n background-color: #3700b3;\r\n}\r\n\r\n.card {\r\n min-width: 300px;\r\n border: 1px solid #e0e0e0;\r\n border-radius: 8px;\r\n background-color: white;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\r\n box-sizing: border-box;\r\n overflow: hidden;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n text-align: center;\r\n height: 60vh;\r\n justify-content: flex-start;\r\n padding: 1rem;\r\n overflow-y: auto;\r\n}\r\n\r\n.card-content {\r\n flex-grow: 1;\r\n flex-direction: column;\r\n display: flex;\r\n justify-content: flex-start;\r\n align-items: center;\r\n text-align: left;\r\n font-size: 0.8em;\r\n position: relative;\r\n left: 2vw;\r\n width: 90%;\r\n .process-title {\r\n font-weight: bold;\r\n padding: 1rem 0;\r\n }\r\n .process-element {\r\n padding: 0.4rem 0;\r\n &:hover {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n &.selected {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n }\r\n}\r\n\r\n.card-description {\r\n padding: 20px;\r\n font-size: 1rem;\r\n color: #333;\r\n width: 90%;\r\n height: 50px;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n margin-bottom: 0px;\r\n}\r\n\r\n.card-action {\r\n width: 100%;\r\n}\r\n\r\n.menu-content {\r\n display: none;\r\n position: absolute;\r\n top: 3.4rem;\r\n right: 1rem;\r\n background-color: white;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\r\n border-radius: 5px;\r\n overflow: hidden;\r\n}\r\n\r\n.menu-content a {\r\n display: block;\r\n padding: 10px 20px;\r\n text-decoration: none;\r\n color: #333;\r\n border-bottom: 1px solid #e0e0e0;\r\n &:hover {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n}\r\n\r\n.menu-content a:last-child {\r\n border-bottom: none;\r\n}\r\n\r\n.qr-code-scanner {\r\n display: none;\r\n}\r\n\r\n/* QR READER */\r\n#qr-reader div {\r\n position: inherit;\r\n}\r\n\r\n#qr-reader div img {\r\n top: 15px;\r\n right: 25px;\r\n margin-top: 5px;\r\n}\r\n\r\n/* INPUT CSS **/\r\n.input-container {\r\n position: relative;\r\n width: 100%;\r\n background-color: #eceff1;\r\n}\r\n\r\n.input-field {\r\n width: 36vw;\r\n padding: 10px 0;\r\n font-size: 1rem;\r\n border: none;\r\n border-bottom: 1px solid #ccc;\r\n outline: none;\r\n background: transparent;\r\n transition: border-color 0.3s;\r\n}\r\n\r\n.input-field:focus {\r\n border-bottom: 2px solid #6200ea;\r\n}\r\n\r\n.input-label {\r\n position: absolute;\r\n margin-top: -0.5em;\r\n top: 0;\r\n left: 0;\r\n padding: 10px 0;\r\n font-size: 1rem;\r\n color: #999;\r\n pointer-events: none;\r\n transition:\r\n transform 0.3s,\r\n color 0.3s,\r\n font-size 0.3s;\r\n}\r\n\r\n.input-field:focus + .input-label,\r\n.input-field:not(:placeholder-shown) + .input-label {\r\n transform: translateY(-20px);\r\n font-size: 0.8em;\r\n color: #6200ea;\r\n}\r\n\r\n.input-underline {\r\n position: absolute;\r\n bottom: 0;\r\n left: 50%;\r\n width: 0;\r\n height: 2px;\r\n background-color: #6200ea;\r\n transition:\r\n width 0.3s,\r\n left 0.3s;\r\n}\r\n\r\n.input-field:focus ~ .input-underline {\r\n width: 100%;\r\n left: 0;\r\n}\r\n\r\n.dropdown-content {\r\n position: absolute;\r\n flex-direction: column;\r\n top: 100%;\r\n left: 0;\r\n width: 100%;\r\n max-height: 150px;\r\n overflow-y: auto;\r\n border: 1px solid #ccc;\r\n border-radius: 4px;\r\n background-color: white;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\r\n display: none;\r\n z-index: 1;\r\n}\r\n\r\n.dropdown-content span {\r\n padding: 10px;\r\n cursor: pointer;\r\n list-style: none;\r\n}\r\n\r\n.dropdown-content span:hover {\r\n background-color: #f0f0f0;\r\n}\r\n\r\n/** AUTOCOMPLETE **/\r\n\r\nselect[data-multi-select-plugin] {\r\n display: none !important;\r\n}\r\n\r\n.multi-select-component {\r\n width: 36vw;\r\n padding: 5px 0;\r\n font-size: 1rem;\r\n border: none;\r\n border-bottom: 1px solid #ccc;\r\n outline: none;\r\n background: transparent;\r\n display: flex;\r\n flex-direction: row;\r\n height: auto;\r\n width: 100%;\r\n -o-transition:\r\n border-color ease-in-out 0.15s,\r\n box-shadow ease-in-out 0.15s;\r\n transition:\r\n border-color ease-in-out 0.15s,\r\n box-shadow ease-in-out 0.15s;\r\n}\r\n\r\n.autocomplete-list {\r\n border-radius: 4px 0px 0px 4px;\r\n}\r\n\r\n.multi-select-component:focus-within {\r\n box-shadow: inset 0px 0px 0px 2px #78abfe;\r\n}\r\n\r\n.multi-select-component .btn-group {\r\n display: none !important;\r\n}\r\n\r\n.multiselect-native-select .multiselect-container {\r\n width: 100%;\r\n}\r\n\r\n.selected-processes {\r\n background-color: white;\r\n padding: 0.4em;\r\n}\r\n\r\n.selected-wrapper {\r\n -webkit-box-sizing: border-box;\r\n -moz-box-sizing: border-box;\r\n box-sizing: border-box;\r\n -webkit-border-radius: 3px;\r\n -moz-border-radius: 3px;\r\n border-radius: 3px;\r\n display: inline-block;\r\n border: 1px solid #d9d9d9;\r\n background-color: #ededed;\r\n white-space: nowrap;\r\n margin: 1px 5px 5px 0;\r\n height: 22px;\r\n vertical-align: top;\r\n cursor: default;\r\n}\r\n\r\n.selected-wrapper .selected-label {\r\n max-width: 514px;\r\n display: inline-block;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n padding-left: 4px;\r\n vertical-align: top;\r\n}\r\n\r\n.selected-wrapper .selected-close {\r\n display: inline-block;\r\n text-decoration: none;\r\n font-size: 14px;\r\n line-height: 1.49rem;\r\n margin-left: 5px;\r\n padding-bottom: 10px;\r\n height: 100%;\r\n vertical-align: top;\r\n padding-right: 4px;\r\n opacity: 0.2;\r\n color: #000;\r\n text-shadow: 0 1px 0 #fff;\r\n font-weight: 700;\r\n}\r\n\r\n.search-container {\r\n display: flex;\r\n flex-direction: row;\r\n}\r\n\r\n.search-container .selected-input {\r\n background: none;\r\n border: 0;\r\n height: 20px;\r\n width: 60px;\r\n padding: 0;\r\n margin-bottom: 6px;\r\n -webkit-box-shadow: none;\r\n box-shadow: none;\r\n}\r\n\r\n.search-container .selected-input:focus {\r\n outline: none;\r\n}\r\n\r\n.dropdown-icon.active {\r\n transform: rotateX(180deg);\r\n}\r\n\r\n.search-container .dropdown-icon {\r\n display: inline-block;\r\n padding: 10px 5px;\r\n position: absolute;\r\n top: 5px;\r\n right: 5px;\r\n width: 10px;\r\n height: 10px;\r\n border: 0 !important;\r\n /* needed */\r\n -webkit-appearance: none;\r\n -moz-appearance: none;\r\n /* SVG background image */\r\n background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');\r\n background-position: center;\r\n background-size: 10px;\r\n background-repeat: no-repeat;\r\n}\r\n\r\n.search-container ul {\r\n position: absolute;\r\n list-style: none;\r\n padding: 0;\r\n z-index: 3;\r\n margin-top: 29px;\r\n width: 100%;\r\n right: 0px;\r\n background: #fff;\r\n border: 1px solid #ccc;\r\n border-top: none;\r\n border-bottom: none;\r\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\r\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\r\n}\r\n\r\n.search-container ul :focus {\r\n outline: none;\r\n}\r\n\r\n.search-container ul li {\r\n display: block;\r\n text-align: left;\r\n padding: 8px 29px 2px 12px;\r\n border-bottom: 1px solid #ccc;\r\n font-size: 14px;\r\n min-height: 31px;\r\n}\r\n\r\n.search-container ul li:first-child {\r\n border-top: 1px solid #ccc;\r\n border-radius: 4px 0px 0 0;\r\n}\r\n\r\n.search-container ul li:last-child {\r\n border-radius: 4px 0px 0 0;\r\n}\r\n\r\n.search-container ul li:hover.not-cursor {\r\n cursor: default;\r\n}\r\n\r\n.search-container ul li:hover {\r\n color: #333;\r\n background-color: #f0f0f0;\r\n border-color: #adadad;\r\n cursor: pointer;\r\n}\r\n\r\n/* Adding scrool to select options */\r\n.autocomplete-list {\r\n max-height: 130px;\r\n overflow-y: auto;\r\n}\r\n\r\n/**************************************** Process page card ******************************************************/\r\n.process-card {\r\n min-width: 300px;\r\n border: 1px solid #e0e0e0;\r\n border-radius: 8px;\r\n background-color: white;\r\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\r\n overflow: hidden;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n text-align: center;\r\n min-height: 40vh;\r\n max-height: 60vh;\r\n justify-content: space-between;\r\n padding: 1rem;\r\n overflow-y: auto;\r\n}\r\n\r\n.process-card-content {\r\n text-align: left;\r\n font-size: 0.8em;\r\n position: relative;\r\n left: 2vw;\r\n width: 90%;\r\n .process-title {\r\n font-weight: bold;\r\n padding: 1rem 0;\r\n }\r\n .process-element {\r\n padding: 0.4rem 0;\r\n &:hover {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n &.selected {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n }\r\n .selected-process-zone {\r\n background-color: rgba(26, 28, 24, 0.08);\r\n }\r\n}\r\n\r\n.process-card-description {\r\n padding: 20px;\r\n font-size: 1rem;\r\n color: #333;\r\n width: 90%;\r\n}\r\n\r\n.process-card-action {\r\n width: 100%;\r\n}\r\n\r\n/**************************************** Select Member Home Page ******************************************************/\r\n.custom-select {\r\n width: 100%;\r\n max-height: 150px;\r\n overflow-y: auto;\r\n direction: ltr;\r\n background-color: white;\r\n border: 1px solid #ccc;\r\n border-radius: 4px;\r\n margin: 10px 0;\r\n}\r\n\r\n.custom-select option {\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n}\r\n\r\n.custom-select option:hover {\r\n background-color: #f0f0f0;\r\n}\r\n\r\n.custom-select::-webkit-scrollbar {\r\n width: 8px;\r\n}\r\n\r\n.custom-select::-webkit-scrollbar-track {\r\n background: #f1f1f1;\r\n}\r\n\r\n.custom-select::-webkit-scrollbar-thumb {\r\n background: #888;\r\n border-radius: 4px;\r\n}\r\n\r\n.custom-select::-webkit-scrollbar-thumb:hover {\r\n background: #555;\r\n}\r\n";
|
|
|
|
class e{constructor(a,b,c,d,f){this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredCamera="environment";this._maxScansPerSecond=25;this._lastScanTimestamp=-1;this._destroyed=this._flashOn=this._paused=this._active=!1;this.$video=a;this.$canvas=document.createElement("canvas");c&&"object"===typeof c?this._onDecode=b:(c||d||f?console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future"):console.warn("Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true."),
|
|
this._legacyOnDecode=b);b="object"===typeof c?c:{};this._onDecodeError=b.onDecodeError||("function"===typeof c?c:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;this._maxScansPerSecond=b.maxScansPerSecond||this._maxScansPerSecond;this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=
|
|
this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateOverlay=this._updateOverlay.bind(this);a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);c=a.parentElement;if(b.highlightScanRegion||b.highlightCodeOutline){d=!!b.overlay;this.$overlay=b.overlay||document.createElement("div");f=this.$overlay.style;f.position="absolute";f.display="none";
|
|
f.pointerEvents="none";this.$overlay.classList.add("scan-region-highlight");if(!d&&b.highlightScanRegion){this.$overlay.innerHTML='<svg class="scan-region-highlight-svg" viewBox="0 0 238 238" preserveAspectRatio="none" style="position:absolute;width:100%;height:100%;left:0;top:0;fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round"><path d="M31 2H10a8 8 0 0 0-8 8v21M207 2h21a8 8 0 0 1 8 8v21m0 176v21a8 8 0 0 1-8 8h-21m-176 0H10a8 8 0 0 1-8-8v-21"/></svg>';try{this.$overlay.firstElementChild.animate({transform:["scale(.98)",
|
|
"scale(1.01)"]},{duration:400,iterations:Infinity,direction:"alternate",easing:"ease-in-out"});}catch(m){}c.insertBefore(this.$overlay,this.$video.nextSibling);}b.highlightCodeOutline&&(this.$overlay.insertAdjacentHTML("beforeend",'<svg class="code-outline-highlight" preserveAspectRatio="none" style="display:none;width:100%;height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;stroke-linecap:round;stroke-linejoin:round"><polygon/></svg>'),this.$codeOutlineHighlight=this.$overlay.lastElementChild);}this._scanRegion=
|
|
this._calculateScanRegion(a);requestAnimationFrame(()=>{let m=window.getComputedStyle(a);"none"===m.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==m.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity="0",a.style.width="0",a.style.height="0",this.$overlay&&this.$overlay.parentElement&&this.$overlay.parentElement.removeChild(this.$overlay),
|
|
delete this.$overlay,delete this.$codeOutlineHighlight);this.$overlay&&this._updateOverlay();});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateOverlay);this._qrEnginePromise=e.createQrEngine();}static set WORKER_PATH(a){console.warn("Setting QrScanner.WORKER_PATH is not required and not supported anymore. Have a look at the README for new setup instructions.");}static async hasCamera(){try{return !!(await e.listCameras(!1)).length}catch(a){return !1}}static async listCameras(a=
|
|
!1){if(!navigator.mediaDevices)return [];let b=async()=>(await navigator.mediaDevices.enumerateDevices()).filter(d=>"videoinput"===d.kind),c;try{a&&(await b()).every(d=>!d.label)&&(c=await navigator.mediaDevices.getUserMedia({audio:!1,video:!0}));}catch(d){}try{return (await b()).map((d,f)=>({id:d.deviceId,label:d.label||(0===f?"Default Camera":`Camera ${f+1}`)}))}finally{c&&(console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"),e._stopVideoStream(c));}}async hasFlash(){let a;
|
|
try{if(this.$video.srcObject){if(!(this.$video.srcObject instanceof MediaStream))return !1;a=this.$video.srcObject;}else a=(await this._getCameraStream()).stream;return "torch"in a.getVideoTracks()[0].getSettings()}catch(b){return !1}finally{a&&a!==this.$video.srcObject&&(console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"),e._stopVideoStream(a));}}isFlashOn(){return this._flashOn}async toggleFlash(){this._flashOn?await this.turnFlashOff():await this.turnFlashOn();}async turnFlashOn(){if(!this._flashOn&&
|
|
!this._destroyed&&(this._flashOn=!0,this._active&&!this._paused))try{if(!await this.hasFlash())throw "No flash available";await this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]});}catch(a){throw this._flashOn=!1,a;}}async turnFlashOff(){this._flashOn&&(this._flashOn=!1,await this._restartVideoStream());}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",
|
|
this._onVisibilityChange);window.removeEventListener("resize",this._updateOverlay);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close");}async start(){if(this._destroyed)throw Error("The QR scanner can not be started as it had been destroyed.");if(!this._active||this._paused)if("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,!document.hidden)if(this._paused=
|
|
!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,facingMode:b}=await this._getCameraStream();!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})));}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1;}async pause(a=!1){this._paused=!0;if(!this._active)return !0;this.$video.pause();this.$overlay&&(this.$overlay.style.display=
|
|
"none");let b=()=>{this.$video.srcObject instanceof MediaStream&&(e._stopVideoStream(this.$video.srcObject),this.$video.srcObject=null);};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return !1;b();return !0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream());}static async scanImage(a,b,c,d,f=!1,h=!1){let m,n=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in
|
|
b||"returnDetailedScanResult"in b)?(m=b.scanRegion,c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,h=b.alsoTryWithoutScanRegion||!1,n=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let p,k;[c,p]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);
|
|
[d,k]=e._drawToCanvas(p,m,d,f);let q;if(c instanceof Worker){let g=c;b||e._postWorkerMessageSync(g,"inversionMode","both");q=await new Promise((l,v)=>{let w,u,r,y=-1;u=t=>{t.data.id===y&&(g.removeEventListener("message",u),g.removeEventListener("error",r),clearTimeout(w),null!==t.data.data?l({data:t.data.data,cornerPoints:e._convertPoints(t.data.cornerPoints,m)}):v(e.NO_QR_CODE_FOUND));};r=t=>{g.removeEventListener("message",u);g.removeEventListener("error",r);clearTimeout(w);v("Scanner error: "+(t?
|
|
t.message||t:"Unknown Error"));};g.addEventListener("message",u);g.addEventListener("error",r);w=setTimeout(()=>r("timeout"),1E4);let x=k.getImageData(0,0,d.width,d.height);y=e._postWorkerMessageSync(g,"decode",x,[x.data.buffer]);});}else q=await Promise.race([new Promise((g,l)=>window.setTimeout(()=>l("Scanner error: timeout"),1E4)),(async()=>{try{var [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return {data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,m)}}catch(l){g=l.message||l;
|
|
if(/not implemented|service unavailable/.test(g))return e._disableBarcodeDetector=!0,e.scanImage(a,{scanRegion:m,canvas:d,disallowCanvasResizing:f,alsoTryWithoutScanRegion:h});throw `Scanner error: ${g}`;}})()]);return n?q:q.data}catch(p){if(!m||!h)throw p;let k=await e.scanImage(a,{qrEngine:c,canvas:d,disallowCanvasResizing:f});return n?k:k.data}finally{b||e._postWorkerMessage(c,"close");}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,
|
|
blue:c,useIntegerApproximation:d});}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a);}static async createQrEngine(a){a&&console.warn("Specifying a worker path is not required and not supported anymore.");a=()=>__vitePreload(() => import('./qr-scanner-worker.min-Dy0qkKA4.mjs'),true?[]:void 0).then(c=>c.createWorker());if(!(!e._disableBarcodeDetector&&"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&(await BarcodeDetector.getSupportedFormats()).includes("qr_code")))return a();let b=navigator.userAgentData;
|
|
return b&&b.brands.some(({brand:c})=>/Chromium/i.test(c))&&/mac ?OS/i.test(b.platform)&&await b.getHighEntropyValues(["architecture","platformVersion"]).then(({architecture:c,platformVersion:d})=>/arm/i.test(c||"arm")&&13<=parseInt(d||"13")).catch(()=>!0)?a():new BarcodeDetector({formats:["qr_code"]})}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateOverlay();this.$overlay&&(this.$overlay.style.display="");this._scanFrame();}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);
|
|
this._updateOverlay();}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start();}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return {x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateOverlay(){requestAnimationFrame(()=>{if(this.$overlay){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,
|
|
m=a.offsetTop,n=window.getComputedStyle(a),p=n.objectFit,k=b/c,q=d/f;switch(p){case "none":var g=b;var l=c;break;case "fill":g=d;l=f;break;default:("cover"===p?k>q:k<q)?(l=f,g=l*k):(g=d,l=g/k),"scale-down"===p&&(g=Math.min(g,b),l=Math.min(l,c));}var [v,w]=n.objectPosition.split(" ").map((r,y)=>{const x=parseFloat(r);return r.endsWith("%")?(y?f-l:d-g)*x/100:x});n=this._scanRegion.width||b;q=this._scanRegion.height||c;p=this._scanRegion.x||0;var u=this._scanRegion.y||0;k=this.$overlay.style;k.width=
|
|
`${n/b*g}px`;k.height=`${q/c*l}px`;k.top=`${m+w+u/c*l}px`;c=/scaleX\(-1\)/.test(a.style.transform);k.left=`${h+(c?d-v-g:v)+(c?b-p-n:p)/b*g}px`;k.transform=a.style.transform;}});}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||("requestVideoFrameCallback"in
|
|
this.$video?this.$video.requestVideoFrameCallback.bind(this.$video):requestAnimationFrame)(async()=>{if(!(1>=this.$video.readyState)){var a=Date.now()-this._lastScanTimestamp,b=1E3/this._maxScansPerSecond;a<b&&await new Promise(d=>setTimeout(d,b-a));this._lastScanTimestamp=Date.now();try{var c=await e.scanImage(this.$video,{scanRegion:this._scanRegion,qrEngine:this._qrEnginePromise,canvas:this.$canvas});}catch(d){if(!this._active)return;this._onDecodeError(d);}!e._disableBarcodeDetector||await this._qrEnginePromise instanceof
|
|
Worker||(this._qrEnginePromise=e.createQrEngine());c?(this._onDecode?this._onDecode(c):this._legacyOnDecode&&this._legacyOnDecode(c.data),this.$codeOutlineHighlight&&(clearTimeout(this._codeOutlineHighlightRemovalTimeout),this._codeOutlineHighlightRemovalTimeout=void 0,this.$codeOutlineHighlight.setAttribute("viewBox",`${this._scanRegion.x||0} `+`${this._scanRegion.y||0} `+`${this._scanRegion.width||this.$video.videoWidth} `+`${this._scanRegion.height||this.$video.videoHeight}`),this.$codeOutlineHighlight.firstElementChild.setAttribute("points",
|
|
c.cornerPoints.map(({x:d,y:f})=>`${d},${f}`).join(" ")),this.$codeOutlineHighlight.style.display="")):this.$codeOutlineHighlight&&!this._codeOutlineHighlightRemovalTimeout&&(this._codeOutlineHighlightRemovalTimeout=setTimeout(()=>this.$codeOutlineHighlight.style.display="none",100));}this._scanFrame();});}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a);}async _getCameraStream(){if(!navigator.mediaDevices)throw "Camera not found.";let a=/^(environment|user)$/.test(this._preferredCamera)?"facingMode":
|
|
"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of [...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:"environment"===this._preferredCamera?"user":"environment");return {stream:f,facingMode:h}}catch(f){}throw "Camera not found.";}async _restartVideoStream(){let a=this._paused;await this.pause(!0)&&!a&&this._active&&await this.start();}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),
|
|
a.removeTrack(b);}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")";}_getFacingMode(a){return (a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,m=b&&b.width?b.width:a.videoWidth||a.width,n=b&&b.height?b.height:a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:m,b=b&&b.downScaledHeight?
|
|
b.downScaledHeight:n,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,m,n,0,0,c.width,c.height);return [c,b]}static async _loadImage(a){if(a instanceof Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||"OffscreenCanvas"in window&&a instanceof OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof
|
|
File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);}}else throw "Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||await new Promise((b,c)=>{let d=f=>{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?
|
|
c("Image load error"):b();};a.addEventListener("load",d);a.addEventListener("error",d);});}static async _postWorkerMessage(a,b,c,d){return e._postWorkerMessageSync(await a,b,c,d)}static _postWorkerMessageSync(a,b,c,d){if(!(a instanceof Worker))return -1;let f=e._workerMessageId++;a.postMessage({id:f,type:b,data:c},d);return f}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e._disableBarcodeDetector=!1;e._workerMessageId=0;
|
|
|
|
async function prepareAndSendPairingTx() {
|
|
const service = await Services.getInstance();
|
|
try {
|
|
await service.checkConnections([]);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
try {
|
|
const relayAddress = service.getAllRelays();
|
|
const createPairingProcessReturn = await service.createPairingProcess("", []);
|
|
if (!createPairingProcessReturn.updated_process) {
|
|
throw new Error("createPairingProcess returned an empty new process");
|
|
}
|
|
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
|
|
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
|
|
await service.handleApiReturn(createPairingProcessReturn);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
class QrScannerComponent extends HTMLElement {
|
|
videoElement;
|
|
wrapper;
|
|
qrScanner;
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: "open" });
|
|
this.wrapper = document.createElement("div");
|
|
this.wrapper.style.position = "relative";
|
|
this.wrapper.style.width = "150px";
|
|
this.wrapper.style.height = "150px";
|
|
this.videoElement = document.createElement("video");
|
|
this.videoElement.style.width = "100%";
|
|
document.body?.append(this.wrapper);
|
|
this.wrapper.prepend(this.videoElement);
|
|
}
|
|
connectedCallback() {
|
|
this.initializeScanner();
|
|
}
|
|
async initializeScanner() {
|
|
if (!this.videoElement) {
|
|
console.error("Video element not found!");
|
|
return;
|
|
}
|
|
console.log("🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:", this.videoElement);
|
|
this.qrScanner = new e(this.videoElement, (result) => this.onQrCodeScanned(result), {
|
|
highlightScanRegion: true,
|
|
highlightCodeOutline: true
|
|
});
|
|
try {
|
|
await e.hasCamera();
|
|
this.qrScanner.start();
|
|
this.videoElement.style = "height: 200px; width: 200px";
|
|
this.shadowRoot?.appendChild(this.wrapper);
|
|
} catch (e) {
|
|
console.error("No camera found or error starting the QR scanner", e);
|
|
}
|
|
}
|
|
async onQrCodeScanned(result) {
|
|
console.log(`QR Code detected:`, result);
|
|
const data = result.data;
|
|
const scannedUrl = new URL(data);
|
|
const spAddress = scannedUrl.searchParams.get("sp_address");
|
|
if (spAddress) {
|
|
try {
|
|
await prepareAndSendPairingTx();
|
|
} catch (e) {
|
|
console.error("Failed to pair:", e);
|
|
}
|
|
}
|
|
this.qrScanner.stop();
|
|
}
|
|
disconnectedCallback() {
|
|
if (this.qrScanner) {
|
|
this.qrScanner.destroy();
|
|
}
|
|
}
|
|
}
|
|
customElements.define("qr-scanner", QrScannerComponent);
|
|
|
|
async function initHomePage() {
|
|
console.log("INIT-HOME");
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
container.querySelectorAll(".tab").forEach((tab) => {
|
|
addSubscription(tab, "click", () => {
|
|
container.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
|
|
tab.classList.add("active");
|
|
container.querySelectorAll(".tab-content").forEach((content) => content.classList.remove("active"));
|
|
container.querySelector(`#${tab.getAttribute("data-tab")}`)?.classList.add("active");
|
|
});
|
|
});
|
|
const service = await Services.getInstance();
|
|
const spAddress = await service.getDeviceAddress();
|
|
generateCreateBtn();
|
|
displayEmojis(spAddress);
|
|
await populateMemberSelect();
|
|
}
|
|
function scanDevice() {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const scannerImg = container.querySelector("#scanner");
|
|
if (scannerImg)
|
|
scannerImg.style.display = "none";
|
|
const scannerQrCode = container.querySelector(".qr-code-scanner");
|
|
if (scannerQrCode)
|
|
scannerQrCode.style.display = "block";
|
|
const scanButton = container?.querySelector("#scan-btn");
|
|
if (scanButton)
|
|
scanButton.style.display = "none";
|
|
const reader = container?.querySelector("#qr-reader");
|
|
if (reader)
|
|
reader.innerHTML = "<qr-scanner></qr-scanner>";
|
|
}
|
|
async function populateMemberSelect() {
|
|
const container = getCorrectDOM("login-4nk-component");
|
|
const memberSelect = container.querySelector("#memberSelect");
|
|
if (!memberSelect) {
|
|
console.error("Could not find memberSelect element");
|
|
return;
|
|
}
|
|
const service = await Services.getInstance();
|
|
const members = await service.getAllMembersSorted();
|
|
for (const [processId, member] of Object.entries(members)) {
|
|
const process = await service.getProcess(processId);
|
|
let memberPublicName;
|
|
if (process) {
|
|
const publicMemberData = service.getPublicData(process);
|
|
if (publicMemberData) {
|
|
const extractedName = publicMemberData["memberPublicName"];
|
|
if (extractedName !== void 0 && extractedName !== null) {
|
|
memberPublicName = extractedName;
|
|
}
|
|
}
|
|
}
|
|
if (!memberPublicName) {
|
|
memberPublicName = "Unnamed Member";
|
|
}
|
|
const emojis = await addressToEmoji(processId);
|
|
const option = document.createElement("option");
|
|
option.value = processId;
|
|
option.textContent = `${memberPublicName} (${emojis})`;
|
|
memberSelect.appendChild(option);
|
|
}
|
|
}
|
|
window.populateMemberSelect = populateMemberSelect;
|
|
window.scanDevice = scanDevice;
|
|
|
|
class LoginComponent extends HTMLElement {
|
|
_callback;
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: "open" });
|
|
}
|
|
connectedCallback() {
|
|
console.log("CALLBACK LOGIN PAGE");
|
|
this.render();
|
|
setTimeout(() => {
|
|
initHomePage();
|
|
}, 500);
|
|
}
|
|
set callback(fn) {
|
|
if (typeof fn === "function") {
|
|
this._callback = fn;
|
|
} else {
|
|
console.error("Callback is not a function");
|
|
}
|
|
}
|
|
get callback() {
|
|
return this._callback;
|
|
}
|
|
render() {
|
|
if (this.shadowRoot)
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
${loginCss}
|
|
</style>${loginHtml}
|
|
<script type="module">
|
|
${loginScript}
|
|
</scipt>
|
|
|
|
`;
|
|
}
|
|
}
|
|
if (!customElements.get("login-4nk-component")) {
|
|
customElements.define("login-4nk-component", LoginComponent);
|
|
}
|
|
|
|
var MessageType;
|
|
(function(MessageType2) {
|
|
MessageType2["LISTENING"] = "LISTENING";
|
|
MessageType2["REQUEST_LINK"] = "REQUEST_LINK";
|
|
MessageType2["LINK_ACCEPTED"] = "LINK_ACCEPTED";
|
|
MessageType2["CREATE_PAIRING"] = "CREATE_PAIRING";
|
|
MessageType2["PAIRING_CREATED"] = "PAIRING_CREATED";
|
|
MessageType2["ERROR"] = "ERROR";
|
|
MessageType2["VALIDATE_TOKEN"] = "VALIDATE_TOKEN";
|
|
MessageType2["RENEW_TOKEN"] = "RENEW_TOKEN";
|
|
MessageType2["GET_PAIRING_ID"] = "GET_PAIRING_ID";
|
|
MessageType2["GET_PROCESSES"] = "GET_PROCESSES";
|
|
MessageType2["GET_MY_PROCESSES"] = "GET_MY_PROCESSES";
|
|
MessageType2["PROCESSES_RETRIEVED"] = "PROCESSES_RETRIEVED";
|
|
MessageType2["RETRIEVE_DATA"] = "RETRIEVE_DATA";
|
|
MessageType2["DATA_RETRIEVED"] = "DATA_RETRIEVED";
|
|
MessageType2["DECODE_PUBLIC_DATA"] = "DECODE_PUBLIC_DATA";
|
|
MessageType2["PUBLIC_DATA_DECODED"] = "PUBLIC_DATA_DECODED";
|
|
MessageType2["GET_MEMBER_ADDRESSES"] = "GET_MEMBER_ADDRESSES";
|
|
MessageType2["MEMBER_ADDRESSES_RETRIEVED"] = "MEMBER_ADDRESSES_RETRIEVED";
|
|
MessageType2["CREATE_PROCESS"] = "CREATE_PROCESS";
|
|
MessageType2["PROCESS_CREATED"] = "PROCESS_CREATED";
|
|
MessageType2["UPDATE_PROCESS"] = "UPDATE_PROCESS";
|
|
MessageType2["PROCESS_UPDATED"] = "PROCESS_UPDATED";
|
|
MessageType2["NOTIFY_UPDATE"] = "NOTIFY_UPDATE";
|
|
MessageType2["UPDATE_NOTIFIED"] = "UPDATE_NOTIFIED";
|
|
MessageType2["VALIDATE_STATE"] = "VALIDATE_STATE";
|
|
MessageType2["STATE_VALIDATED"] = "STATE_VALIDATED";
|
|
MessageType2["HASH_VALUE"] = "HASH_VALUE";
|
|
MessageType2["VALUE_HASHED"] = "VALUE_HASHED";
|
|
MessageType2["GET_MERKLE_PROOF"] = "GET_MERKLE_PROOF";
|
|
MessageType2["MERKLE_PROOF_RETRIEVED"] = "MERKLE_PROOF_RETRIEVED";
|
|
MessageType2["VALIDATE_MERKLE_PROOF"] = "VALIDATE_MERKLE_PROOF";
|
|
MessageType2["MERKLE_PROOF_VALIDATED"] = "MERKLE_PROOF_VALIDATED";
|
|
MessageType2["ADD_DEVICE"] = "ADD_DEVICE";
|
|
MessageType2["DEVICE_ADDED"] = "DEVICE_ADDED";
|
|
})(MessageType || (MessageType = {}));
|
|
|
|
function splitPrivateData(data, privateFields) {
|
|
const privateData = {};
|
|
const publicData = {};
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (privateFields.includes(key)) {
|
|
privateData[key] = value;
|
|
} else {
|
|
publicData[key] = value;
|
|
}
|
|
}
|
|
return { privateData, publicData };
|
|
}
|
|
function isValid32ByteHex(value) {
|
|
if (value.length !== 64) {
|
|
return false;
|
|
}
|
|
return /^[0-9a-fA-F]{64}$/.test(value);
|
|
}
|
|
|
|
const routes = {
|
|
home: "/src/pages/home/home.html",
|
|
account: "/src/pages/account/account.html"
|
|
};
|
|
let currentRoute = "";
|
|
async function navigate(path) {
|
|
cleanSubscriptions();
|
|
cleanPage();
|
|
path = path.replace(/^\//, "");
|
|
if (path.includes("/")) {
|
|
const parsedPath = path.split("/")[0];
|
|
if (!routes[parsedPath]) {
|
|
path = "home";
|
|
}
|
|
}
|
|
await handleLocation(path);
|
|
}
|
|
async function handleLocation(path) {
|
|
const parsedPath = path.split("/");
|
|
if (path.includes("/")) {
|
|
path = parsedPath[0];
|
|
}
|
|
currentRoute = path;
|
|
const routeHtml = routes[path] || routes["home"];
|
|
const content = document.getElementById("containerId");
|
|
if (content) {
|
|
if (path === "home") {
|
|
const container = document.querySelector("#containerId");
|
|
const accountComponent = document.createElement("login-4nk-component");
|
|
accountComponent.setAttribute("style", "width: 100vw; height: 100vh; position: relative; grid-row: 2;");
|
|
if (container)
|
|
container.appendChild(accountComponent);
|
|
} else {
|
|
const html = await fetch(routeHtml).then((data) => data.text());
|
|
content.innerHTML = html;
|
|
}
|
|
await new Promise(requestAnimationFrame);
|
|
injectHeader();
|
|
switch (path) {
|
|
case "account":
|
|
const { AccountComponent } = await __vitePreload(async () => { const { AccountComponent } = await import('./account-component-DbdHSqFJ.mjs');return { AccountComponent }},true?[]:void 0);
|
|
const accountContainer = document.querySelector(".parameter-list");
|
|
if (accountContainer) {
|
|
if (!customElements.get("account-component")) {
|
|
customElements.define("account-component", AccountComponent);
|
|
}
|
|
const accountComponent = document.createElement("account-component");
|
|
accountContainer.appendChild(accountComponent);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
window.onpopstate = async () => {
|
|
const services = await Services.getInstance();
|
|
if (!services.isPaired()) {
|
|
handleLocation("home");
|
|
} else {
|
|
handleLocation("account");
|
|
}
|
|
};
|
|
async function init() {
|
|
try {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const isE2E = params.has("e2e");
|
|
if (isE2E && window.self !== window.top) {
|
|
await registerAllListeners();
|
|
await navigate("home");
|
|
return;
|
|
}
|
|
const services = await Services.getInstance();
|
|
window.myService = services;
|
|
const db = await Database.getInstance();
|
|
db.registerServiceWorker("/src/service-workers/database.worker.js");
|
|
const device = await services.getDeviceFromDatabase();
|
|
console.log("🚀 ~ setTimeout ~ device:", device);
|
|
if (!device) {
|
|
await services.createNewDevice();
|
|
} else {
|
|
services.restoreDevice(device);
|
|
}
|
|
await services.restoreProcessesFromDB();
|
|
await services.restoreSecretsFromDB();
|
|
if (!isE2E) {
|
|
await services.connectAllRelays();
|
|
await services.updateDeviceBlockHeight();
|
|
}
|
|
if (window.self !== window.top) {
|
|
await registerAllListeners();
|
|
}
|
|
await navigate("home");
|
|
} catch (error) {
|
|
console.error(error);
|
|
await navigate("home");
|
|
}
|
|
}
|
|
async function registerAllListeners() {
|
|
const isE2E = new URLSearchParams(window.location.search).has("e2e");
|
|
if (isE2E && window.self !== window.top) {
|
|
window.parent.postMessage({ type: MessageType.LISTENING }, "*");
|
|
}
|
|
const services = await Services.getInstance();
|
|
const tokenService = await TokenService.getInstance();
|
|
const errorResponse = (errorMsg, origin, messageId) => {
|
|
window.parent.postMessage({
|
|
type: MessageType.ERROR,
|
|
error: errorMsg,
|
|
messageId
|
|
}, origin);
|
|
};
|
|
const handleRequestLink = async (event) => {
|
|
if (event.data.type !== MessageType.REQUEST_LINK) {
|
|
return;
|
|
}
|
|
if (isE2E) {
|
|
const acceptedMsg = {
|
|
type: MessageType.LINK_ACCEPTED,
|
|
accessToken: "e2e-access",
|
|
refreshToken: "e2e-refresh",
|
|
messageId: event.data.messageId
|
|
};
|
|
window.parent.postMessage(acceptedMsg, event.origin);
|
|
return;
|
|
} else {
|
|
const modalService = await ModalService$1.getInstance();
|
|
const result = await modalService.showConfirmationModal({
|
|
title: "Confirmation de liaison",
|
|
content: `
|
|
<div class="modal-confirmation">
|
|
<h3>Liaison avec ${event.origin}</h3>
|
|
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
|
|
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
|
|
<p>Voulez-vous continuer ?</p>
|
|
</div>
|
|
`,
|
|
confirmText: "Ajouter un service",
|
|
cancelText: "Annuler"
|
|
}, true);
|
|
if (!result) {
|
|
const errorMsg = "Failed to pair device: User refused to link";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
}
|
|
try {
|
|
const tokens = await tokenService.generateSessionToken(event.origin);
|
|
const acceptedMsg = {
|
|
type: MessageType.LINK_ACCEPTED,
|
|
accessToken: tokens.accessToken,
|
|
refreshToken: tokens.refreshToken,
|
|
messageId: event.data.messageId
|
|
};
|
|
window.parent.postMessage(acceptedMsg, event.origin);
|
|
} catch (error) {
|
|
const errorMsg = `Failed to generate tokens: ${error}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleCreatePairing = async (event) => {
|
|
if (event.data.type !== MessageType.CREATE_PAIRING) {
|
|
return;
|
|
}
|
|
if (services.isPaired()) {
|
|
const errorMsg = "Device already paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
console.log("🚀 Starting pairing process");
|
|
await prepareAndSendPairingTx$1();
|
|
await services.confirmPairing();
|
|
const pairingId = services.getPairingProcessId();
|
|
if (!pairingId) {
|
|
throw new Error("Failed to get pairing process id");
|
|
}
|
|
const successMsg = {
|
|
type: MessageType.PAIRING_CREATED,
|
|
pairingId,
|
|
messageId: event.data.messageId
|
|
};
|
|
window.parent.postMessage(successMsg, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to create pairing process: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleGetMyProcesses = async (event) => {
|
|
if (event.data.type !== MessageType.GET_MY_PROCESSES) {
|
|
return;
|
|
}
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const myProcesses = await services.getMyProcesses();
|
|
window.parent.postMessage({
|
|
type: MessageType.GET_MY_PROCESSES,
|
|
myProcesses,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to get processes: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleGetProcesses = async (event) => {
|
|
if (event.data.type !== MessageType.GET_PROCESSES) {
|
|
return;
|
|
}
|
|
const tokenService2 = await TokenService.getInstance();
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { accessToken } = event.data;
|
|
if (!accessToken || !await tokenService2.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const processes = await services.getProcesses();
|
|
window.parent.postMessage({
|
|
type: MessageType.PROCESSES_RETRIEVED,
|
|
processes,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to get processes: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleDecryptState = async (event) => {
|
|
if (event.data.type !== MessageType.RETRIEVE_DATA) {
|
|
return;
|
|
}
|
|
const tokenService2 = await TokenService.getInstance();
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { processId, stateId, accessToken } = event.data;
|
|
if (!accessToken || !await tokenService2.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const process = await services.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Can't find process");
|
|
}
|
|
const state = services.getStateFromId(process, stateId);
|
|
let res = {};
|
|
if (state) {
|
|
for (const attribute of Object.keys(state.pcd_commitment)) {
|
|
if (attribute === "roles" || state.public_data[attribute]) {
|
|
continue;
|
|
}
|
|
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
|
|
if (decryptedAttribute) {
|
|
res[attribute] = decryptedAttribute;
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error("Unknown state for process", processId);
|
|
}
|
|
window.parent.postMessage({
|
|
type: MessageType.DATA_RETRIEVED,
|
|
data: res,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to retrieve data: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleValidateToken = async (event) => {
|
|
if (event.data.type !== MessageType.VALIDATE_TOKEN) {
|
|
return;
|
|
}
|
|
const accessToken = event.data.accessToken;
|
|
const refreshToken = event.data.refreshToken;
|
|
if (!accessToken || !refreshToken) {
|
|
errorResponse("Failed to validate token: missing access, refresh token or both", event.origin, event.data.messageId);
|
|
}
|
|
if (isE2E) {
|
|
window.parent.postMessage({
|
|
type: MessageType.VALIDATE_TOKEN,
|
|
accessToken,
|
|
refreshToken,
|
|
isValid: true,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
return;
|
|
}
|
|
const isValid = await tokenService.validateToken(accessToken, event.origin);
|
|
window.parent.postMessage({
|
|
type: MessageType.VALIDATE_TOKEN,
|
|
accessToken,
|
|
refreshToken,
|
|
isValid,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
};
|
|
const handleRenewToken = async (event) => {
|
|
if (event.data.type !== MessageType.RENEW_TOKEN) {
|
|
return;
|
|
}
|
|
try {
|
|
const refreshToken = event.data.refreshToken;
|
|
if (!refreshToken) {
|
|
throw new Error("No refresh token provided");
|
|
}
|
|
if (isE2E) {
|
|
window.parent.postMessage({
|
|
type: MessageType.RENEW_TOKEN,
|
|
accessToken: "e2e-access-2",
|
|
refreshToken,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
return;
|
|
}
|
|
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
|
|
if (!newAccessToken) {
|
|
throw new Error("Failed to refresh token");
|
|
}
|
|
window.parent.postMessage({
|
|
type: MessageType.RENEW_TOKEN,
|
|
accessToken: newAccessToken,
|
|
refreshToken,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (error) {
|
|
const errorMsg = `Failed to renew token: ${error}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleGetPairingId = async (event) => {
|
|
if (event.data.type !== MessageType.GET_PAIRING_ID)
|
|
return;
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const userPairingId = services.getPairingProcessId();
|
|
window.parent.postMessage({
|
|
type: MessageType.GET_PAIRING_ID,
|
|
userPairingId,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to get pairing id: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleCreateProcess = async (event) => {
|
|
if (event.data.type !== MessageType.CREATE_PROCESS)
|
|
return;
|
|
if (!services.isPaired() && !isE2E) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { processData, privateFields, roles, accessToken } = event.data;
|
|
if (isE2E) {
|
|
window.parent.postMessage({
|
|
type: MessageType.PROCESS_CREATED,
|
|
processCreated: { processId: "e2e-process", process: {}, processData },
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
return;
|
|
}
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const { privateData, publicData } = splitPrivateData(processData, privateFields);
|
|
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
|
|
if (!createProcessReturn.updated_process) {
|
|
throw new Error("Empty updated_process in createProcessReturn");
|
|
}
|
|
const processId = createProcessReturn.updated_process.process_id;
|
|
const process = createProcessReturn.updated_process.current_process;
|
|
await services.handleApiReturn(createProcessReturn);
|
|
const res = { processId, process, processData };
|
|
window.parent.postMessage({
|
|
type: MessageType.PROCESS_CREATED,
|
|
processCreated: res,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to create process: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleNotifyUpdate = async (event) => {
|
|
if (event.data.type !== MessageType.NOTIFY_UPDATE)
|
|
return;
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { processId, stateId, accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
if (!isValid32ByteHex(stateId)) {
|
|
throw new Error("Invalid state id");
|
|
}
|
|
const res = await services.createPrdUpdate(processId, stateId);
|
|
await services.handleApiReturn(res);
|
|
window.parent.postMessage({
|
|
type: MessageType.UPDATE_NOTIFIED,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to notify update for process: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleValidateState = async (event) => {
|
|
if (event.data.type !== MessageType.VALIDATE_STATE)
|
|
return;
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { processId, stateId, accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const res = await services.approveChange(processId, stateId);
|
|
await services.handleApiReturn(res);
|
|
window.parent.postMessage({
|
|
type: MessageType.STATE_VALIDATED,
|
|
validatedProcess: res.updated_process,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to validate process: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleUpdateProcess = async (event) => {
|
|
if (event.data.type !== MessageType.UPDATE_PROCESS)
|
|
return;
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
try {
|
|
const { processId, newData, privateFields, roles, accessToken } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const process = await services.getProcess(processId);
|
|
if (!process) {
|
|
throw new Error("Process not found");
|
|
}
|
|
let lastState = services.getLastCommitedState(process);
|
|
if (!lastState) {
|
|
const firstState = process.states[0];
|
|
const roles2 = firstState.roles;
|
|
if (services.rolesContainsUs(roles2)) {
|
|
const approveChangeRes = await services.approveChange(processId, firstState.state_id);
|
|
await services.handleApiReturn(approveChangeRes);
|
|
const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id);
|
|
await services.handleApiReturn(prdUpdateRes);
|
|
} else {
|
|
if (firstState.validation_tokens.length > 0) {
|
|
const res2 = await services.createPrdUpdate(processId, firstState.state_id);
|
|
await services.handleApiReturn(res2);
|
|
}
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
lastState = services.getLastCommitedState(process);
|
|
if (!lastState) {
|
|
throw new Error("Process doesn't have a commited state yet");
|
|
}
|
|
}
|
|
const lastStateIndex = services.getLastCommitedStateIndex(process);
|
|
if (lastStateIndex === null) {
|
|
throw new Error("Process doesn't have a commited state yet");
|
|
}
|
|
const privateData = {};
|
|
const publicData = {};
|
|
for (const field of Object.keys(newData)) {
|
|
if (lastState.public_data[field]) {
|
|
publicData[field] = newData[field];
|
|
continue;
|
|
}
|
|
if (privateFields.includes(field)) {
|
|
privateData[field] = newData[field];
|
|
continue;
|
|
}
|
|
for (let i = lastStateIndex; i >= 0; i--) {
|
|
const state = process.states[i];
|
|
if (state.pcd_commitment[field]) {
|
|
privateData[field] = newData[field];
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
if (privateData[field])
|
|
continue;
|
|
publicData[field] = newData[field];
|
|
}
|
|
const res = await services.updateProcess(process, privateData, publicData, roles);
|
|
await services.handleApiReturn(res);
|
|
window.parent.postMessage({
|
|
type: MessageType.PROCESS_UPDATED,
|
|
updatedProcess: res.updated_process,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to update process: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleDecodePublicData = async (event) => {
|
|
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA)
|
|
return;
|
|
if (!services.isPaired()) {
|
|
const errorMsg = "Device not paired";
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
return;
|
|
}
|
|
try {
|
|
const { accessToken, encodedData } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const decodedData = services.decodeValue(encodedData);
|
|
window.parent.postMessage({
|
|
type: MessageType.PUBLIC_DATA_DECODED,
|
|
decodedData,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to decode data: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleHashValue = async (event) => {
|
|
if (event.data.type !== MessageType.HASH_VALUE)
|
|
return;
|
|
console.log("handleHashValue", event.data);
|
|
try {
|
|
const { accessToken, commitedIn, label, fileBlob } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const hash = services.getHashForFile(commitedIn, label, fileBlob);
|
|
window.parent.postMessage({
|
|
type: MessageType.VALUE_HASHED,
|
|
hash,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to hash value: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleGetMerkleProof = async (event) => {
|
|
if (event.data.type !== MessageType.GET_MERKLE_PROOF)
|
|
return;
|
|
try {
|
|
const { accessToken, processState, attributeName } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
const proof = services.getMerkleProofForFile(processState, attributeName);
|
|
window.parent.postMessage({
|
|
type: MessageType.MERKLE_PROOF_RETRIEVED,
|
|
proof,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to get merkle proof: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
const handleValidateMerkleProof = async (event) => {
|
|
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF)
|
|
return;
|
|
try {
|
|
const { accessToken, merkleProof, documentHash } = event.data;
|
|
if (!accessToken || !await tokenService.validateToken(accessToken, event.origin)) {
|
|
throw new Error("Invalid or expired session token");
|
|
}
|
|
let parsedMerkleProof;
|
|
try {
|
|
parsedMerkleProof = JSON.parse(merkleProof);
|
|
} catch (e) {
|
|
throw new Error("Provided merkleProof is not a valid json object");
|
|
}
|
|
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
|
|
window.parent.postMessage({
|
|
type: MessageType.MERKLE_PROOF_VALIDATED,
|
|
isValid: res,
|
|
messageId: event.data.messageId
|
|
}, event.origin);
|
|
} catch (e) {
|
|
const errorMsg = `Failed to get merkle proof: ${e}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
};
|
|
window.removeEventListener("message", handleMessage);
|
|
window.addEventListener("message", handleMessage);
|
|
async function handleMessage(event) {
|
|
try {
|
|
switch (event.data.type) {
|
|
case MessageType.REQUEST_LINK:
|
|
await handleRequestLink(event);
|
|
break;
|
|
case MessageType.CREATE_PAIRING:
|
|
await handleCreatePairing(event);
|
|
break;
|
|
case MessageType.GET_MY_PROCESSES:
|
|
await handleGetMyProcesses(event);
|
|
break;
|
|
case MessageType.GET_PROCESSES:
|
|
await handleGetProcesses(event);
|
|
break;
|
|
case MessageType.RETRIEVE_DATA:
|
|
await handleDecryptState(event);
|
|
break;
|
|
case MessageType.VALIDATE_TOKEN:
|
|
await handleValidateToken(event);
|
|
break;
|
|
case MessageType.RENEW_TOKEN:
|
|
await handleRenewToken(event);
|
|
break;
|
|
case MessageType.GET_PAIRING_ID:
|
|
await handleGetPairingId(event);
|
|
break;
|
|
case MessageType.CREATE_PROCESS:
|
|
await handleCreateProcess(event);
|
|
break;
|
|
case MessageType.NOTIFY_UPDATE:
|
|
await handleNotifyUpdate(event);
|
|
break;
|
|
case MessageType.VALIDATE_STATE:
|
|
await handleValidateState(event);
|
|
break;
|
|
case MessageType.UPDATE_PROCESS:
|
|
await handleUpdateProcess(event);
|
|
break;
|
|
case MessageType.DECODE_PUBLIC_DATA:
|
|
await handleDecodePublicData(event);
|
|
break;
|
|
case MessageType.HASH_VALUE:
|
|
await handleHashValue(event);
|
|
break;
|
|
case MessageType.GET_MERKLE_PROOF:
|
|
await handleGetMerkleProof(event);
|
|
break;
|
|
case MessageType.VALIDATE_MERKLE_PROOF:
|
|
await handleValidateMerkleProof(event);
|
|
break;
|
|
default:
|
|
console.warn(`Unhandled message type: ${event.data.type}`);
|
|
}
|
|
} catch (error) {
|
|
const errorMsg = `Error handling message: ${error}`;
|
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
|
}
|
|
}
|
|
let emitCount = 0;
|
|
const emitListening = () => {
|
|
if (emitCount >= 100)
|
|
return;
|
|
window.parent.postMessage({ type: MessageType.LISTENING }, "*");
|
|
emitCount += 1;
|
|
setTimeout(emitListening, 100);
|
|
};
|
|
if (isE2E && window.self !== window.top) {
|
|
emitListening();
|
|
}
|
|
}
|
|
async function cleanPage() {
|
|
const container = document.querySelector("#containerId");
|
|
if (container)
|
|
container.innerHTML = "";
|
|
}
|
|
async function injectHeader() {
|
|
const headerContainer = document.getElementById("header-container");
|
|
if (headerContainer) {
|
|
const headerHtml = await fetch("/src/components/header/header.html").then((res) => res.text());
|
|
headerContainer.innerHTML = headerHtml;
|
|
const script = document.createElement("script");
|
|
script.src = "/src/components/header/header.ts";
|
|
script.type = "module";
|
|
document.head.appendChild(script);
|
|
initHeader();
|
|
}
|
|
}
|
|
window.navigate = navigate;
|
|
document.addEventListener("navigate", (e) => {
|
|
const event = e;
|
|
if (event.detail.page === "chat") {
|
|
const container = document.querySelector(".container");
|
|
if (container)
|
|
container.innerHTML = "";
|
|
const chatElement = document.querySelector("chat-element");
|
|
if (chatElement) {
|
|
chatElement.setAttribute("process-id", event.detail.processId || "");
|
|
}
|
|
}
|
|
});
|
|
|
|
(async () => {
|
|
await init();
|
|
})();
|
|
|
|
export { Services as S, addressToEmoji as a };
|