From a08923eea47366afd6a5d5c692ca7503bfabab14 Mon Sep 17 00:00:00 2001 From: Wrongthink Date: Mon, 11 Dec 2023 23:09:57 -0500 Subject: [PATCH] Rebase to xiMatrix 0.9.0 --- README.md | 2 +- manifest.json | 9 +- src/bg.js | 334 +++++++++++++++++++++++----------------------- src/content.js | 7 +- src/popup.css | 27 ++-- src/popup.html | 19 ++- src/popup.js | 84 ++++++------ src/settings.css | 6 + src/settings.html | 9 +- src/settings.js | 28 ++-- src/shared.js | 8 +- src/storage.js | 56 ++++++++ 12 files changed, 327 insertions(+), 262 deletions(-) create mode 100644 src/storage.js diff --git a/README.md b/README.md index 832f28a..a6e85db 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ He has clearly stated in his project that it is a personal tool and will not lik - Open to being extended - Separate image and media columns - Reload page button in-panel -- Dark theme! +- ~~Dark theme!~~ *(Mainlined, mostly)* - Dialog clarifications - More to come diff --git a/manifest.json b/manifest.json index 189b4a3..28c4d96 100644 --- a/manifest.json +++ b/manifest.json @@ -2,9 +2,9 @@ "manifest_version": 2, "name": "paraMatrix", "author": "Wrongthink", - "homepage_url": "https://gitler.moe/Wrongthink/paraMatrix", + "homepage_url": "https://gitler.moe/wrongthink/paramatrix", "description": "block requests based on domain and type", - "version": "🐢.🐎", + "version": "🐢.🦍", "browser_action": { "default_title": "paraMatrix", "default_popup": "src/popup.html", @@ -14,8 +14,9 @@ "48": "icon.svg" }, "background": { - "scripts": ["src/shared.js", "src/bg.js"], - "persistent": false + "scripts": ["src/bg.js"], + "persistent": false, + "type": "module" }, "content_scripts": [{ "js": ["src/content.js"], diff --git a/src/bg.js b/src/bg.js index 76e00cc..8808720 100644 --- a/src/bg.js +++ b/src/bg.js @@ -1,101 +1,90 @@ -/* global browser shared */ +/* global browser */ -var lock = Promise.resolve(); +import * as shared from './shared.js'; +import * as storage from './storage.js'; -var STORAGE_DEFAULTS = { - 'rules': {}, - 'savedRules': {}, - 'requests': {}, - 'recording': true, +var glob = function(s, pattern) { + var p = pattern.split('*'); + return s.startsWith(p[0]) && s.endsWith(p.at(-1)); }; -var getHostname = function(url) { +var getHostname = function(url, patterns) { var u = new URL(url); + + for (var pattern of patterns) { + if (glob(u.hostname, pattern)) { + return pattern; + } + } + return u.hostname; }; -var storageGet = function(key) { - return browser.storage.local.get(key).then(data => { - return data[key] ?? STORAGE_DEFAULTS[key]; - }); -}; - -var _storageChange = function(key, fn) { - return storageGet(key).then(oldValue => { - var data = {}; - data[key] = fn(oldValue); - return browser.storage.local.set(data); - }); -}; - -var storageChange = function(key, fn) { - lock = lock.then(() => _storageChange(key, fn)); - return lock; -}; - -var setRule = function(context, hostname, type, rule) { - return storageGet('savedRules').then(savedRules => { - return storageChange('rules', rules => { - if (hostname === 'first-party') { - context = '*'; - } - if (!rules[context]) { - rules[context] = savedRules[context] || {}; - } - if (!rules[context][hostname]) { - rules[context][hostname] = {}; - } - if (rule) { - rules[context][hostname][type] = rule; - } else { - delete rules[context][hostname][type]; - if (Object.keys(rules[context][hostname]).length === 0) { - delete rules[context][hostname]; - } - if (Object.keys(rules[context]).length === 0 && !savedRules[context]) { - delete rules[context]; - } - } - return rules; - }); - }); -}; - -var getRules = function(context) { - return Promise.all([ - storageGet('rules'), - storageGet('savedRules'), - ]).then(([rules, savedRules]) => { - var restricted = {}; - restricted['*'] = rules['*'] || savedRules['*'] || {}; - restricted[context] = rules[context] || savedRules[context] || {}; - restricted.dirty = !!rules[context]; - return restricted; - }); -}; - -var pushRequest = function(tabId, hostname, type) { - return storageGet('recording').then(recording => { - if (recording) { - return storageChange('requests', requests => { - if (!requests[tabId]) { - requests[tabId] = {}; - } - if (!requests[tabId][hostname]) { - requests[tabId][hostname] = {}; - } - if (!requests[tabId][hostname][type]) { - requests[tabId][hostname][type] = 0; - } - requests[tabId][hostname][type] += 1; - return requests; - }); +var setRule = async function(context, hostname, type, rule) { + var savedRules = await storage.get('savedRules'); + await storage.change('rules', rules => { + if (hostname === 'first-party') { + context = '*'; } + if (!rules[context]) { + rules[context] = savedRules[context] || {}; + } + if (!rules[context][hostname]) { + rules[context][hostname] = {}; + } + if (rule) { + rules[context][hostname][type] = rule; + } else { + delete rules[context][hostname][type]; + if (Object.keys(rules[context][hostname]).length === 0) { + delete rules[context][hostname]; + } + if (Object.keys(rules[context]).length === 0 && !savedRules[context]) { + delete rules[context]; + } + } + return rules; }); }; -var clearRequests = function(tabId) { - return storageChange('requests', requests => { +var getPatterns = async function() { + var savedRules = await storage.get('savedRules'); + return savedRules._patterns || []; +}; + +var getRules = async function(context) { + var [rules, savedRules] = await Promise.all([ + storage.get('rules'), + storage.get('savedRules'), + ]); + var restricted = {}; + restricted['*'] = rules['*'] || savedRules['*'] || {}; + restricted[context] = rules[context] || savedRules[context] || {}; + restricted.dirty = !!rules[context]; + return restricted; +}; + +var pushRequest = async function(tabId, hostname, type) { + var recording = await storage.get('recording'); + if (recording) { + await storage.change('requests', requests => { + if (!requests[tabId]) { + requests[tabId] = {}; + } + if (!requests[tabId][hostname]) { + requests[tabId][hostname] = {}; + } + if (!requests[tabId][hostname][type]) { + requests[tabId][hostname][type] = 0; + } + requests[tabId][hostname][type] += 1; + return requests; + }); + } +}; + +var clearRequests = async function(tabId) { + await storage.change('requests', requests => { if (requests[tabId]) { delete requests[tabId]; } @@ -103,138 +92,145 @@ var clearRequests = function(tabId) { }); }; -var getCurrentTab = function() { - return browser.tabs.query({ +var getCurrentTab = async function() { + var tabs = await browser.tabs.query({ active: true, currentWindow: true, - }).then(tabs => tabs[0]); + }); + return tabs[0]; }; -browser.runtime.onMessage.addListener((msg, sender) => { +browser.runtime.onMessage.addListener(async (msg, sender) => { if (msg.type === 'get') { - return getCurrentTab().then(tab => { - var context = getHostname(tab.url); - return Promise.all([ - getRules(context), - storageGet('requests'), - storageGet('recording'), - ]).then(([rules, requests, recording]) => { - return { - context: context, - rules: rules, - requests: requests[tab.id] || {}, - recording: recording, - }; - }); - }); + const [tab, patterns] = await Promise.all([ + getCurrentTab(), + getPatterns(), + ]); + const context = getHostname(tab.url, patterns); + const [rules, requests, recording] = await Promise.all([ + getRules(context), + storage.get('requests'), + storage.get('recording'), + ]); + return { + context: context, + rules: rules, + requests: requests[tab.id] || {}, + recording: recording, + }; } else if (msg.type === 'setRule') { - return setRule( + await setRule( msg.data.context, msg.data.hostname, msg.data.type, msg.data.value, - ).then(() => getRules(msg.data.context)); + ); + return await getRules(msg.data.context); } else if (msg.type === 'commit') { - var r; - return storageChange('rules', rules => { + let r; + await storage.change('rules', rules => { r = rules[msg.data]; delete rules[msg.data]; return rules; - }).then(() => storageChange('savedRules', savedRules => { + }); + await storage.change('savedRules', savedRules => { if (Object.keys(r).length === 0) { delete savedRules[msg.data]; } else { savedRules[msg.data] = r; } return savedRules; - })); + }); } else if (msg.type === 'reset') { - return storageChange('rules', rules => { + await storage.change('rules', rules => { delete rules[msg.data]; return rules; }); } else if (msg.type === 'securitypolicyviolation') { - return pushRequest(sender.tab.id, 'inline', msg.data); + await pushRequest(sender.tab.id, 'inline', msg.data); } else if (msg.type === 'toggleRecording') { - return storageChange('recording', recording => !recording); + await storage.change('recording', recording => !recording); } }); browser.tabs.onRemoved.addListener(clearRequests); -browser.webNavigation.onBeforeNavigate.addListener(details => { +browser.webNavigation.onBeforeNavigate.addListener(async details => { if (details.frameId === 0) { - return clearRequests(details.tabId); + await clearRequests(details.tabId); } }); -browser.webRequest.onBeforeSendHeaders.addListener(details => { - var context = getHostname(details.documentUrl || details.url); - if (details.frameAncestors.length) { +browser.webRequest.onBeforeSendHeaders.addListener(async details => { + var patterns = await getPatterns(); + var context = getHostname(details.documentUrl || details.url, patterns); + if (details.frameAncestors && details.frameAncestors.length) { var last = details.frameAncestors.length - 1; - context = getHostname(details.frameAncestors[last].url); + context = getHostname(details.frameAncestors[last].url, patterns); } - var hostname = getHostname(details.url); + var hostname = getHostname(details.url, patterns); var type = shared.TYPE_MAP[details.type] || 'other'; - let isCookie = h => h.name.toLowerCase() === 'cookie'; - var cookiePromise = Promise.resolve(); - if (details.requestHeaders.some(isCookie)) { - cookiePromise = pushRequest(details.tabId, hostname, 'cookie'); + var promises = [ + getRules(context), + ]; + + if (details.type !== 'main_frame') { + promises.push(pushRequest(details.tabId, hostname, type)); } - return Promise.all([ - pushRequest(details.tabId, hostname, type), - cookiePromise, - getRules(context), - ]).then(([_, _2, rules]) => { - if ( - details.type !== 'main_frame' - && !shared.shouldAllow(rules, context, hostname, type) - ) { - if (details.type === 'sub_frame') { - // this can in turn be blocked by a local CSP - return {redirectUrl: 'data:,' + encodeURIComponent(details.url)}; - } else { - return {cancel: true}; - } - } + var isCookie = h => h.name.toLowerCase() === 'cookie'; + if (details.requestHeaders.some(isCookie)) { + promises.push(pushRequest(details.tabId, hostname, 'cookie')); + } - if (shared.shouldAllow(rules, context, hostname, 'cookie')) { - return {requestHeaders: details.requestHeaders}; + var [rules, ..._rest] = await Promise.all(promises); + if ( + details.type !== 'main_frame' + && !shared.shouldAllow(rules, context, hostname, type) + ) { + if (details.type === 'sub_frame') { + // this can in turn be blocked by a local CSP + return {redirectUrl: 'data:,' + encodeURIComponent(details.url)}; } else { - var filtered = details.requestHeaders.filter(h => !isCookie(h)); - return {requestHeaders: filtered}; + return {cancel: true}; } - }); + } + + if (shared.shouldAllow(rules, context, hostname, 'cookie')) { + return {requestHeaders: details.requestHeaders}; + } else { + var filtered = details.requestHeaders.filter(h => !isCookie(h)); + return {requestHeaders: filtered}; + } }, {urls: ['']}, ['blocking', 'requestHeaders']); -browser.webRequest.onHeadersReceived.addListener(details => { - var context = getHostname(details.url); - return Promise.all([ +browser.webRequest.onHeadersReceived.addListener(async details => { + var patterns = await getPatterns(); + var context = getHostname(details.url, patterns); + var [rules, recording] = await Promise.all([ getRules(context), - storageGet('recording'), - ]).then(([rules, recording]) => { - var csp = (type, value) => { - var name = 'Content-Security-Policy'; - if (shared.shouldAllow(rules, context, 'inline', type)) { - if (recording) { - name = 'Content-Security-Policy-Report-Only'; - } else { - return; - } + storage.get('recording'), + ]); + var csp = (type, value) => { + var name = 'Content-Security-Policy'; + if (shared.shouldAllow(rules, context, 'inline', type)) { + if (recording) { + name = 'Content-Security-Policy-Report-Only'; + } else { + return; } - details.responseHeaders.push({ - name: name, - value: value, - }); - }; + } + details.responseHeaders.push({ + name: name, + value: value, + }); + }; - csp('css', "style-src 'self' *"); - csp('script', "script-src 'self' *"); - csp('media', "img-src 'self' *"); + csp('css', "style-src 'self' *"); + csp('script', "script-src 'self' *"); + csp('media', "img-src 'self' *"); - return {responseHeaders: details.responseHeaders}; - }); + return {responseHeaders: details.responseHeaders}; }, { urls: [''], types: ['main_frame'], diff --git a/src/content.js b/src/content.js index f605ef2..65d2c9a 100644 --- a/src/content.js +++ b/src/content.js @@ -2,12 +2,17 @@ const TYPE_MAP = { 'style-src': 'css', + 'style-src-elem': 'css', + 'style-src-attr': 'css', 'script-src': 'script', + 'script-src-elem': 'script', + 'script-src-attr': 'script', 'img-src': 'media', + 'media-src': 'media', }; document.addEventListener('securitypolicyviolation', event => { - var type = TYPE_MAP[event.violatedDirective]; + var type = TYPE_MAP[event.effectiveDirective]; if (type) { browser.runtime.sendMessage({ type: 'securitypolicyviolation', diff --git a/src/popup.css b/src/popup.css index a52ff8d..82ecd80 100644 --- a/src/popup.css +++ b/src/popup.css @@ -1,10 +1,10 @@ :root { - --blue-light: #5e69f0; - --blue-dark: #2425ac; - --orange-light: #3f372f; - --dark-grey: #1b1b1b; - --text-light: #838383; - --text-dark: #fff; + --blue-light: #5e69f0; + --blue-dark: #2425ac; + --orange-light: #3f372f; + --dark-grey: #1b1b1b; + --text-light: #838383; + --text-dark: #fff; } .toolbar { @@ -17,13 +17,14 @@ table { background: var(--orange-light); + color: var(--text-on-light); border-spacing: 0; margin-block-end: 0.2em; } th, td { position: relative; border: 1px solid #1f2042; - min-inline-size: 3.4em; + min-inline-size: 2.4em; line-height: 1.8; text-align: center; font-weight: normal; @@ -52,9 +53,6 @@ table input:focus-visible { .inherit-allow { background: var(--blue-light); } -.inherit-allow ~ span { - color: var(--text-dark); -} table input:checked { background: var(--blue-dark); } @@ -62,8 +60,13 @@ table input ~ span { pointer-events: none; position: relative; z-index: 1; - color: var(--text-light); } table input:checked ~ span { - color: var(--text-dark); + color: var(--text-on-dark); +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + } } diff --git a/src/popup.html b/src/popup.html index 91f2c61..bdfd767 100644 --- a/src/popup.html +++ b/src/popup.html @@ -4,7 +4,7 @@ - +
Help -

Columns represent different types of requests. Rows represent domains. Numbers (if recording is enabled) show how many requests of a given type the current tab tries to make to the given domain. Grey cells are blocked, blue cells are allowed. Light blue cells inherit rules indirectly because they represent a sub-domain of an allowed domain. Black cells are disabled.

-

Everything is blocked by default. Click on a cell to allow it, then click reload page to load those assets. Click commit to save the configuration for the current site. Click on the domain or type to allow an entire row or column. Master rows are available for:

-
    -
  • inline: Controls <script>-elements that are directly embedded in the HTML code. Categories that cannot be inline are disabled.
  • -
  • first-party: Sets global defaults for requests to the same domain as the page itself.
  • -
  • sub-domains: If a domain is allowed, all of its subdomains inhereit that rule.
  • -
+

Columns represent different types of requests. Rows represent domains. Numbers (if recording is enabled) show how many requests of a given type the current tab tries to make to the given domain. Grey cells are blocked, blue cells are allowed. Light blue cells inherit rules indirectly because they represent a sub-domain of an allowed domain. Black cells are disabled.

+

Everything is blocked by default. Click on a cell to allow it, then click reload page to load those assets. Click commit to save the configuration for the current site. Click on the domain or type to allow an entire row or column. Master rows are available for:

+
    +
  • inline: Controls <script>-elements that are directly embedded in the HTML code. Categories that cannot be inline are disabled.
  • +
  • first-party: Sets global defaults for requests to the same domain as the page itself.
  • +
  • sub-domains: If a domain is allowed, all of its subdomains inhereit that rule.
  • +
- - + diff --git a/src/popup.js b/src/popup.js index 85cb78f..766a0aa 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,17 +1,19 @@ -/* global browser shared */ +/* global browser */ + +import * as shared from './shared.js'; var context; var requests; var rules; var table = document.querySelector('table'); -var recording = document.querySelector('[name="recording"]') +var recording = document.querySelector('[name="recording"]'); var commitButton = document.querySelector('[name="commit"]'); var resetButton = document.querySelector('[name="reset"]'); var reloadButton = document.querySelector('[name="reload"]'); -var sendMessage = function(type, data) { - return browser.runtime.sendMessage({type: type, data: data}); +var sendMessage = async function(type, data) { + return await browser.runtime.sendMessage({type: type, data: data}); }; var getHostnames = function() { @@ -76,19 +78,18 @@ var createCheckbox = function(hostname, type) { var c = (hostname === 'first-party') ? '*' : context; input.checked = (rules[c][hostname] || {})[type]; - input.onchange = () => { - sendMessage('setRule', { + input.onchange = async () => { + var newRules = await sendMessage('setRule', { context: context, hostname: hostname, type: type, value: input.checked, - }).then(newRules => { - rules = newRules; - commitButton.disabled = !rules.dirty; - resetButton.disabled = !rules.dirty; - reloadButton.disabled = !rules.dirty; - updateInherit(type); }); + rules = newRules; + commitButton.disabled = !rules.dirty; + resetButton.disabled = !rules.dirty; + reloadButton.disabled = !rules.dirty; + updateInherit(type); }; return input; @@ -135,54 +136,53 @@ var createRow = function(hostname) { return tr; }; -var loadContext = function() { - return sendMessage('get').then(data => { - context = data.context; - requests = data.requests; - rules = data.rules; - recording.checked = data.recording; - commitButton.disabled = !rules.dirty; - resetButton.disabled = !rules.dirty; +var loadContext = async function() { + var data = await sendMessage('get'); + context = data.context; + requests = data.requests; + rules = data.rules; + recording.checked = data.recording; + commitButton.disabled = !rules.dirty; + resetButton.disabled = !rules.dirty; - table.innerHTML = ''; - table.append(createHeader()); - table.append(createRow('inline')); - table.append(createRow('first-party')); + table.innerHTML = ''; + table.append(createHeader()); + table.append(createRow('inline')); + table.append(createRow('first-party')); - for (const hostname of getHostnames()) { - table.append(createRow(hostname)); - } + for (const hostname of getHostnames()) { + table.append(createRow(hostname)); + } - updateInherit('*'); - }); + updateInherit('*'); }; browser.webNavigation.onBeforeNavigate.addListener(window.close); -document.querySelector('[name="settings"]').addEventListener('click', event => { +document.querySelector('[name="settings"]').addEventListener('click', () => { browser.runtime.openOptionsPage(); }); -document.addEventListener('DOMContentLoaded', () => { - loadContext(); +document.addEventListener('DOMContentLoaded', async () => { + await loadContext(); }); -recording.addEventListener('change', event => { - sendMessage('toggleRecording'); +recording.addEventListener('change', async () => { + await sendMessage('toggleRecording'); }); reloadButton.addEventListener('click', event => { browser.tabs.reload({bypassCache: true}); }); -commitButton.addEventListener('click', event => { - sendMessage('commit', context).then(() => { - commitButton.disabled = true; - resetButton.disabled = true; - reloadButton.disabled= true; - }); +commitButton.addEventListener('click', async () => { + await sendMessage('commit', context); + commitButton.disabled = true; + resetButton.disabled= true; + reloadButton.disabled = true; }); -resetButton.addEventListener('click', event => { - sendMessage('reset', context).then(loadContext); +resetButton.addEventListener('click', async () => { + await sendMessage('reset', context); + await loadContext(); }); diff --git a/src/settings.css b/src/settings.css index f243bc7..8caf76f 100644 --- a/src/settings.css +++ b/src/settings.css @@ -33,3 +33,9 @@ button { justify-self: end; grid-column: 1 / 3; } + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + } +} diff --git a/src/settings.html b/src/settings.html index d41a1af..e0db7d4 100644 --- a/src/settings.html +++ b/src/settings.html @@ -5,19 +5,18 @@ paraMatrix — edit rules - +
- - + diff --git a/src/settings.js b/src/settings.js index 644d5bd..4e31aa4 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,22 +1,24 @@ +/* global browser */ + var form = document.querySelector('form'); var textarea1 = document.querySelector('textarea.rules'); var textarea2 = document.querySelector('textarea.savedRules'); browser.storage.local.get(['rules', 'savedRules']).then(data => { - var rules = data.rules || {}; - var savedRules = data.savedRules || {}; - textarea1.value = JSON.stringify(rules, null, 2) - textarea2.value = JSON.stringify(savedRules, null, 2) + var rules = data.rules || {}; + var savedRules = data.savedRules || {}; + textarea1.value = JSON.stringify(rules, null, 2); + textarea2.value = JSON.stringify(savedRules, null, 2); }); form.addEventListener('submit', event => { - event.preventDefault(); - var rules = JSON.parse(textarea1.value); - var savedRules = JSON.parse(textarea2.value); - browser.storage.local.set({ - 'rules': rules, - 'savedRules': savedRules, - }).then(() => { - location.reload(); - }); + event.preventDefault(); + var rules = JSON.parse(textarea1.value); + var savedRules = JSON.parse(textarea2.value); + browser.storage.local.set({ + 'rules': rules, + 'savedRules': savedRules, + }).then(() => { + location.reload(); + }); }); diff --git a/src/shared.js b/src/shared.js index 55249e4..f28bb4e 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1,7 +1,5 @@ -var shared = {}; - -shared.TYPES = ['cookie', 'font', 'css', 'image', 'media', 'script', 'xhr', 'frame', 'other']; -shared.TYPE_MAP = { +export const TYPES = ['cookie', 'font', 'css', 'image', 'media', 'script', 'xhr', 'frame', 'other']; +export const TYPE_MAP = { 'stylesheet': 'css', 'font': 'font', 'image': 'image', @@ -14,7 +12,7 @@ shared.TYPE_MAP = { 'sub_frame': 'frame', }; -shared.shouldAllow = function(rules, context, hostname, type) { +export var shouldAllow = function(rules, context, hostname, type) { var hostnames = ['*', hostname]; var parts = hostname.split('.'); while (parts.length > 2) { diff --git a/src/storage.js b/src/storage.js new file mode 100644 index 0000000..04ecea2 --- /dev/null +++ b/src/storage.js @@ -0,0 +1,56 @@ +/* global browser */ + +var STORAGE_DEFAULTS = { + 'rules': {}, + 'savedRules': {}, + 'requests': {}, + 'recording': true, +}; +var STORAGE_AREAS = { + 'rules': browser.storage.local, + 'savedRules': browser.storage.local, + 'requests': browser.storage.session, + 'recording': browser.storage.local, +}; + +var lock = Promise.resolve(); +var cache = {}; + +var _get = async function(key) { + var data = await STORAGE_AREAS[key].get(key); + return data[key] ?? STORAGE_DEFAULTS[key]; +}; + +export var get = function(key) { + if (!cache[key]) { + cache[key] = _get(key); + } + return cache[key]; +}; + +var _change = async function(key, fn) { + var oldValue = await get(key); + var data = {}; + data[key] = fn(oldValue); + delete cache[key]; + await STORAGE_AREAS[key].set(data); +}; + +export var change = async function(key, fn) { + lock = lock.then(() => _change(key, fn)); + await lock; +}; + +var invalidateCache = function(changes) { + for (var key in changes) { + delete cache[key]; + } +}; + +browser.storage.local.onChanged.addListener(invalidateCache); + +// migrations +browser.runtime.onInstalled.addListener(() => { + // 0.8.0: store requests to session storage + lock = lock.then(() => browser.storage.local.remove('requests')); +});