parent
8a0be83783
commit
0146a726bd
|
@ -1,4 +1,4 @@
|
||||||
distribution
|
dist
|
||||||
.idea
|
.idea
|
||||||
node_modules
|
node_modules
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
};
|
17
package.json
17
package.json
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "sessions-ext",
|
"name": "sessions-ext",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "parcel build src/manifest.json --no-content-hash --no-source-maps --dist-dir distribution --no-cache --detailed-report 0",
|
"build": "parcel build src/manifest.json --no-content-hash --no-source-maps --dist-dir dist --detailed-report 0",
|
||||||
"watch": "parcel watch src/manifest.json --dist-dir distribution --no-cache --no-hmr",
|
"watch": "parcel watch src/manifest.json --dist-dir dist --no-cache --no-hmr",
|
||||||
"run:chromium": "web-ext run -t chromium"
|
"run:chromium": "web-ext run -t chromium"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -17,14 +17,21 @@
|
||||||
"@parcel/config-webextension": "^2.8.0",
|
"@parcel/config-webextension": "^2.8.0",
|
||||||
"@types/chrome": "^0.0.200",
|
"@types/chrome": "^0.0.200",
|
||||||
"parcel": "^2.8.0",
|
"parcel": "^2.8.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"since 2017-06"
|
||||||
|
],
|
||||||
"webExt": {
|
"webExt": {
|
||||||
"sourceDir": "distribution",
|
"sourceDir": "dist",
|
||||||
"run": {
|
"run": {
|
||||||
"startUrl": [
|
"startUrl": [
|
||||||
"https://pl181.plemiona.pl/game.php?screen=info_player"
|
"https://www.tribalwars.net"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"webext-options-sync": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,140 @@
|
||||||
chrome.cookies.onChanged.addListener(console.log);
|
import { LoginMessage, LoginResponse, Message, MessageType } from './message';
|
||||||
|
import { optionsStorage } from './options-storage';
|
||||||
|
import { decrypt, encrypt } from './crypto';
|
||||||
|
|
||||||
|
const COOKIE_NAME = 'sid';
|
||||||
|
const HTTP_STATUS_OK = 200;
|
||||||
|
const API_KEY_HEADER = 'X-Api-Key';
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
async (message: Message, sender, sendResponse) => {
|
||||||
|
try {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.LOGIN:
|
||||||
|
await handleLogin(message);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
sendResponse({ error: err.message } as LoginResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLogin = async (message: LoginMessage) => {
|
||||||
|
const sid = await chrome.cookies.get({
|
||||||
|
name: COOKIE_NAME,
|
||||||
|
url: message.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sid) {
|
||||||
|
const success = await tryOpenOverview(message.url);
|
||||||
|
if (success) {
|
||||||
|
await createOrUpdateCookie(message.server, await encryptCookie(sid));
|
||||||
|
await openOverview(message.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidFromApi = await getSid(message.server);
|
||||||
|
if (sidFromApi.length > 0) {
|
||||||
|
await setSid(message.url, sidFromApi);
|
||||||
|
if (await tryOpenOverview(message.url)) {
|
||||||
|
await openOverview(message.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(message.loginUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSid = await chrome.cookies.get({
|
||||||
|
name: COOKIE_NAME,
|
||||||
|
url: message.url,
|
||||||
|
});
|
||||||
|
if (!newSid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOrUpdateCookie(message.server, await encryptCookie(newSid));
|
||||||
|
await openOverview(message.url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryOpenOverview = async (base: string) => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(buildUrlToOverview(base), {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
redirect: 'error',
|
||||||
|
});
|
||||||
|
return resp.status == HTTP_STATUS_OK;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openOverview = async (base: string) => {
|
||||||
|
await chrome.tabs.update({
|
||||||
|
url: buildUrlToOverview(base).toString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUrlToOverview = (base: string) => {
|
||||||
|
const url = new URL(base);
|
||||||
|
url.pathname = '/game.php';
|
||||||
|
url.searchParams.set('screen', 'overview_villages');
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrUpdateCookie = async (server: string, sid: string) => {
|
||||||
|
const opts = await optionsStorage.getAll();
|
||||||
|
|
||||||
|
const url = new URL(opts.apiUrl);
|
||||||
|
url.pathname = `/api/v1/user/sessions/${server}`;
|
||||||
|
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: sid,
|
||||||
|
headers: {
|
||||||
|
[API_KEY_HEADER]: opts.apiKey,
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSid = async (server: string): Promise<string> => {
|
||||||
|
const opts = await optionsStorage.getAll();
|
||||||
|
|
||||||
|
const url = new URL(opts.apiUrl);
|
||||||
|
url.pathname = `/api/v1/user/sessions/${server}`;
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
[API_KEY_HEADER]: opts.apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const respBody = await resp.json();
|
||||||
|
if (!respBody.data?.sid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody.data.sid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptCookie = async (cookie: chrome.cookies.Cookie) => {
|
||||||
|
const opts = await optionsStorage.getAll();
|
||||||
|
return await encrypt(cookie.value, opts.encryptionPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSid = async (url: string, sid: string) => {
|
||||||
|
const opts = await optionsStorage.getAll();
|
||||||
|
await chrome.cookies.set({
|
||||||
|
url,
|
||||||
|
name: COOKIE_NAME,
|
||||||
|
value: await decrypt(sid, opts.encryptionPassword),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { LoginMessage, LoginResponse, MessageType } from './message';
|
||||||
|
|
||||||
|
let isLoggingIn = false;
|
||||||
|
|
||||||
|
const renderUI = () => {
|
||||||
|
document
|
||||||
|
.querySelectorAll('.worlds-container:first-of-type .world-select')
|
||||||
|
.forEach((el) => {
|
||||||
|
const cloned = el.cloneNode(true);
|
||||||
|
if (!(cloned instanceof HTMLAnchorElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned.addEventListener('click', handleClick);
|
||||||
|
|
||||||
|
const span = cloned.childNodes.item(1);
|
||||||
|
if (!span) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
span.textContent += ' (SS)';
|
||||||
|
|
||||||
|
el.parentNode?.insertBefore(cloned, el.nextSibling);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = async (e: MouseEvent) => {
|
||||||
|
if (!(e.currentTarget instanceof HTMLAnchorElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isLoggingIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggingIn = true;
|
||||||
|
|
||||||
|
const url = new URL(e.currentTarget.href);
|
||||||
|
const server = extractServerFromURL(url);
|
||||||
|
chrome.runtime
|
||||||
|
.sendMessage({
|
||||||
|
type: MessageType.LOGIN,
|
||||||
|
server,
|
||||||
|
loginUrl: url.toString(),
|
||||||
|
url: url.protocol + '//' + url.host.replace('www', server),
|
||||||
|
} as LoginMessage)
|
||||||
|
.then((resp: LoginResponse) => {
|
||||||
|
isLoggingIn = false;
|
||||||
|
if (!resp?.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(resp.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractServerFromURL = (url: URL): string => {
|
||||||
|
const split = url.pathname.split('/');
|
||||||
|
if (split.length <= 0) {
|
||||||
|
throw new Error(`invalid path: ${url.pathname}`);
|
||||||
|
}
|
||||||
|
return split[split.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
renderUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
|
@ -0,0 +1,85 @@
|
||||||
|
const ITERATIONS = 100000;
|
||||||
|
const SALT_LENGTH = 16;
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
|
||||||
|
export const encrypt = async (data: string, password: string) => {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||||
|
const passwordKey = await getPasswordKey(password);
|
||||||
|
const aesKey = await deriveKey(passwordKey, salt, ['encrypt']);
|
||||||
|
const encryptedContent = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
aesKey,
|
||||||
|
enc.encode(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedContentArr = new Uint8Array(encryptedContent);
|
||||||
|
const buf = new Uint8Array(
|
||||||
|
salt.byteLength + iv.byteLength + encryptedContentArr.byteLength
|
||||||
|
);
|
||||||
|
buf.set(salt, 0);
|
||||||
|
buf.set(iv, salt.byteLength);
|
||||||
|
buf.set(encryptedContentArr, salt.byteLength + iv.byteLength);
|
||||||
|
return bufToBase64(buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decrypt = async (encryptedData: string, password: string) => {
|
||||||
|
const encryptedDataBuff = base64ToBuf(encryptedData);
|
||||||
|
const salt = encryptedDataBuff.slice(0, SALT_LENGTH);
|
||||||
|
const iv = encryptedDataBuff.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
||||||
|
const data = encryptedDataBuff.slice(SALT_LENGTH + IV_LENGTH);
|
||||||
|
const passwordKey = await getPasswordKey(password);
|
||||||
|
const aesKey = await deriveKey(passwordKey, salt, ['decrypt']);
|
||||||
|
const decryptedContent = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
aesKey,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return dec.decode(decryptedContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPasswordKey = (password: string) => {
|
||||||
|
return crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, [
|
||||||
|
'deriveKey',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deriveKey = (
|
||||||
|
passwordKey: CryptoKey,
|
||||||
|
salt: Uint8Array,
|
||||||
|
keyUsages: KeyUsage[]
|
||||||
|
) => {
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: salt,
|
||||||
|
iterations: ITERATIONS,
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
passwordKey,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
keyUsages
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bufToBase64 = (buf: Uint8Array) => {
|
||||||
|
let s = '';
|
||||||
|
buf.forEach((c) => {
|
||||||
|
s += String.fromCharCode(c);
|
||||||
|
});
|
||||||
|
return btoa(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
const base64ToBuf = (b64: string) => {
|
||||||
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||||
|
};
|
BIN
src/icon.png
BIN
src/icon.png
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
@ -1,26 +1,29 @@
|
||||||
{
|
{
|
||||||
"name": "Awesome Extension",
|
"name": "Sessions",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"description": "An awesome new browser extension",
|
"description": "A browser extension aiming to simplify coplaying",
|
||||||
"homepage_url": "https://tribalwarshelp.com/",
|
"homepage_url": "https://tribalwarshelp.com/",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"minimum_chrome_version": "100",
|
"minimum_chrome_version": "100",
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "icon.png"
|
"128": "icon.png"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"action": {
|
||||||
"storage",
|
"default_title": "Sessions",
|
||||||
"cookies"
|
"default_popup": "popup.html"
|
||||||
],
|
|
||||||
"host_permissions": [
|
|
||||||
"https://*.plemiona.pl/*"
|
|
||||||
],
|
|
||||||
"options_ui": {
|
|
||||||
"browser_style": true,
|
|
||||||
"page": "options.html"
|
|
||||||
},
|
},
|
||||||
|
"permissions": ["storage", "cookies", "activeTab", "tabs"],
|
||||||
|
"host_permissions": ["https://*.tribalwars.net/*", "https://*.plemiona.pl/*"],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.tribalwars.net/*", "https://www.plemiona.pl/*"],
|
||||||
|
"js": ["content.ts"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.ts",
|
"service_worker": "background.ts",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
},
|
||||||
|
"author": "Dawid Wysokiński"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export enum MessageType {
|
||||||
|
LOGIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginMessage = {
|
||||||
|
type: MessageType.LOGIN;
|
||||||
|
url: string;
|
||||||
|
loginUrl: string;
|
||||||
|
server: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = LoginMessage;
|
||||||
|
|
||||||
|
export type LoginResponse = {
|
||||||
|
error?: string;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import OptionsSync from 'webext-options-sync';
|
||||||
|
|
||||||
|
export const optionsStorage = new OptionsSync({
|
||||||
|
defaults: {
|
||||||
|
apiUrl: 'https://sessions.tribalwarshelp.com',
|
||||||
|
apiKey: '',
|
||||||
|
encryptionPassword: 'password',
|
||||||
|
},
|
||||||
|
migrations: [OptionsSync.migrations.removeUnused],
|
||||||
|
});
|
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Sessions</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Test</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||||
|
/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<title>Sessions</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Options</h1>
|
||||||
|
<section id="options">
|
||||||
|
<form>
|
||||||
|
<label
|
||||||
|
>API URL
|
||||||
|
<input name="apiUrl" type="text" />
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
>API Key
|
||||||
|
<input name="apiKey" type="text" />
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
>Encryption password
|
||||||
|
<input name="encryptionPassword" type="text" />
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script src="popup.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { optionsStorage } from './options-storage';
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
await optionsStorage.syncForm('#options form');
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2017",
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noImplicitAny": true
|
||||||
|
}
|
||||||
|
}
|
41
yarn.lock
41
yarn.lock
|
@ -960,6 +960,11 @@ detect-libc@^1.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||||
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
|
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
|
||||||
|
|
||||||
|
dom-form-serializer@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-form-serializer/-/dom-form-serializer-2.0.0.tgz#b1550efefa08ebca15547ee1b4c791bf7b45c748"
|
||||||
|
integrity sha512-HMrrc7gJIBj6sWmnJcO9DLZj8AsdFP60+pZSu0vMJxZhEP3GPfsNE9X1GC95nXZ0SZbS8FYDb/NHW/NArSmu0Q==
|
||||||
|
|
||||||
dom-serializer@^1.0.1:
|
dom-serializer@^1.0.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
|
||||||
|
@ -1185,6 +1190,11 @@ lmdb@2.5.2:
|
||||||
"@lmdb/lmdb-linux-x64" "2.5.2"
|
"@lmdb/lmdb-linux-x64" "2.5.2"
|
||||||
"@lmdb/lmdb-win32-x64" "2.5.2"
|
"@lmdb/lmdb-win32-x64" "2.5.2"
|
||||||
|
|
||||||
|
lz-string@^1.4.4:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
|
||||||
|
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
|
||||||
|
|
||||||
mdn-data@2.0.14:
|
mdn-data@2.0.14:
|
||||||
version "2.0.14"
|
version "2.0.14"
|
||||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
||||||
|
@ -1334,6 +1344,11 @@ posthtml@^0.16.4, posthtml@^0.16.5:
|
||||||
posthtml-parser "^0.11.0"
|
posthtml-parser "^0.11.0"
|
||||||
posthtml-render "^3.0.0"
|
posthtml-render "^3.0.0"
|
||||||
|
|
||||||
|
prettier@^2.7.1:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||||
|
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||||
|
|
||||||
react-error-overlay@6.0.9:
|
react-error-overlay@6.0.9:
|
||||||
version "6.0.9"
|
version "6.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||||
|
@ -1424,6 +1439,11 @@ terser@^5.2.0:
|
||||||
commander "^2.20.0"
|
commander "^2.20.0"
|
||||||
source-map-support "~0.5.20"
|
source-map-support "~0.5.20"
|
||||||
|
|
||||||
|
throttle-debounce@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933"
|
||||||
|
integrity sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==
|
||||||
|
|
||||||
timsort@^0.3.0:
|
timsort@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
|
@ -1467,6 +1487,27 @@ weak-lru-cache@^1.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
|
resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
|
||||||
integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
|
integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
|
||||||
|
|
||||||
|
webext-detect-page@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/webext-detect-page/-/webext-detect-page-4.0.1.tgz#124a6802c872fa0bc12549d99b2dd53d18af2ba7"
|
||||||
|
integrity sha512-Y9Skw6/Uj0dGwOIidc1XqZ3neEbmuuT4BlkL/J4JHAo6fVznHIZq6/MWDsPGOA/jnNowiSXtHHh4S/TOxbl6bQ==
|
||||||
|
|
||||||
|
webext-options-sync@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/webext-options-sync/-/webext-options-sync-4.0.0.tgz#d706aa6f1330c07fb061da541477aa68b8538593"
|
||||||
|
integrity sha512-00umVaF/jpd3cWyT/9OZJf1CM5Z3AtiIKT/4Fzuhwfe3pg2z84cGz/RwaIRiN60zz7A64ieEG1z9dKT8/cEICQ==
|
||||||
|
dependencies:
|
||||||
|
dom-form-serializer "^2.0.0"
|
||||||
|
lz-string "^1.4.4"
|
||||||
|
throttle-debounce "^5.0.0"
|
||||||
|
webext-detect-page "^4.0.1"
|
||||||
|
webext-polyfill-kinda "^0.10.0"
|
||||||
|
|
||||||
|
webext-polyfill-kinda@^0.10.0:
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/webext-polyfill-kinda/-/webext-polyfill-kinda-0.10.0.tgz#5eb154c581edae827f2832c090811d14dd074ea2"
|
||||||
|
integrity sha512-Yz5WTwig5byFfMXgagtfaJkVU+RrnVqtL1hmvA+GIbpRaGKU1DIrFYHMUUFkeyFqxRSuhbOdLKzteXxCd6VNzA==
|
||||||
|
|
||||||
xxhash-wasm@^0.4.2:
|
xxhash-wasm@^0.4.2:
|
||||||
version "0.4.2"
|
version "0.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79"
|
resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79"
|
||||||
|
|
Loading…
Reference in New Issue