Transition from manifest v2 to v3 for chrome (#1367)

* Update manifest.json

* replace `extension.getUrl` to `runtime.getUrl`

* Update localStorage to async chrome.storage.local

* Change action to browserAction

* fix bug

* revert async changes

* revert test change

* Fix localStorage breaking change

* .extension -> .runtime

* fix undefined behaviour

* fixed tests

* lint

* Up some callbacks to async

* Change where extractMetadata is called

* fix  in mock

* Update background field to take in type

* Change background.js to service_worker.js

* Update id and request origin for service worker

* updated chrome mock

* 13 tests passing

* Fix async issue

* switch naming back to original

* linting

* Fix linting in base

* remove timeOut

* added permission to alarms in manifest

* fixed alarms uncaught errors

* Cleanup addr for tests

---------

Co-authored-by: bee344 <alberto.penayo@parity.io>
This commit is contained in:
Tarik Gul
2024-07-07 22:32:42 -04:00
committed by GitHub
co-authored by bee344
parent 5d36b76a16
commit b6d42c28b5
15 changed files with 236 additions and 104 deletions
@@ -7,7 +7,7 @@
import '@polkadot/extension-mocks/chrome';
import type { AuthUrls, ResponseSigning } from '@polkadot/extension-base/background/types';
import type { ResponseSigning } from '@polkadot/extension-base/background/types';
import type { MetadataDef } from '@polkadot/extension-inject/types';
import type { KeyringPair } from '@polkadot/keyring/types';
import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
@@ -29,24 +29,15 @@ describe('Extension', () => {
let tabs: Tabs;
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const password = 'passw0rd';
const address = '5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX';
async function createExtension (): Promise<Extension> {
try {
await cryptoWaitReady();
keyring.loadAll({ store: new AccountsStore() });
const authUrls: AuthUrls = {};
authUrls['localhost:3000'] = {
authorizedAccounts: [address],
count: 0,
id: '11',
origin: 'example.com',
url: 'http://localhost:3000'
};
localStorage.setItem('authUrls', JSON.stringify(authUrls));
state = new State();
await state.init();
tabs = new Tabs(state);
return new Extension(state);
@@ -313,7 +304,7 @@ describe('Extension', () => {
userExtensions
};
state.saveMetadata(meta);
await state.saveMetadata(meta);
const payload: SignerPayloadJSON = {
address,
@@ -388,7 +379,7 @@ describe('Extension', () => {
userExtensions
};
state.saveMetadata(meta);
await state.saveMetadata(meta);
const registry = new TypeRegistry();
@@ -444,7 +435,7 @@ describe('Extension', () => {
userExtensions
};
state.saveMetadata(meta);
await state.saveMetadata(meta);
const payload = {
address,
@@ -116,7 +116,7 @@ export default class Extension {
};
}
private accountsForget ({ address }: RequestAccountForget): boolean {
private async accountsForget ({ address }: RequestAccountForget): Promise<boolean> {
const authorizedAccountsDiff: AuthorizedAccountsDiff = [];
// cycle through authUrls and prepare the array of diff
@@ -128,12 +128,12 @@ export default class Extension {
}
});
this.#state.updateAuthorizedAccounts(authorizedAccountsDiff);
await this.#state.updateAuthorizedAccounts(authorizedAccountsDiff);
// cycle through default account selection for auth and remove any occurrence of the account
const newDefaultAuthAccounts = this.#state.defaultAuthAccountSelection.filter((defaultSelectionAddress) => defaultSelectionAddress !== address);
this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts);
await this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts);
keyring.forgetAccount(address);
@@ -212,8 +212,8 @@ export default class Extension {
return true;
}
private authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): void {
return this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]);
private async authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): Promise<void> {
return await this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]);
}
private getAuthList (): ResponseAuthorizeList {
@@ -235,14 +235,14 @@ export default class Extension {
return true;
}
private metadataApprove ({ id }: RequestMetadataApprove): boolean {
private async metadataApprove ({ id }: RequestMetadataApprove): Promise<boolean> {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { request, resolve } = queued;
this.#state.saveMetadata(request);
await this.#state.saveMetadata(request);
resolve(true);
@@ -467,7 +467,7 @@ export default class Extension {
}
private windowOpen (path: AllowedPath): boolean {
const url = `${chrome.extension.getURL('index.html')}#${path}`;
const url = `${chrome.runtime.getURL('index.html')}#${path}`;
if (!ALLOWED_PATH.includes(path)) {
console.error('Not allowed to open the url:', url);
@@ -518,8 +518,10 @@ export default class Extension {
return true;
}
private removeAuthorization (url: string): ResponseAuthorizeList {
return { list: this.#state.removeAuthorization(url) };
private async removeAuthorization (url: string): Promise<ResponseAuthorizeList> {
const remAuth = await this.#state.removeAuthorization(url);
return { list: remAuth };
}
private deleteAuthRequest (requestId: string): void {
@@ -593,7 +595,7 @@ export default class Extension {
return this.accountsValidate(request as RequestAccountValidate);
case 'pri(metadata.approve)':
return this.metadataApprove(request as RequestMetadataApprove);
return await this.metadataApprove(request as RequestMetadataApprove);
case 'pri(metadata.get)':
return this.metadataGet(request as string);
@@ -59,7 +59,7 @@ interface SignRequest extends Resolver<ResponseSigning> {
url: string;
}
const NOTIFICATION_URL = chrome.extension.getURL('notification.html');
const NOTIFICATION_URL = chrome.runtime.getURL('notification.html');
const POPUP_WINDOW_OPTS: chrome.windows.CreateData = {
focused: true,
@@ -86,8 +86,8 @@ export enum NotificationOptions {
const AUTH_URLS_KEY = 'authUrls';
const DEFAULT_AUTH_ACCOUNTS = 'defaultAuthAccounts';
function extractMetadata (store: MetadataStore): void {
store.allMap((map): void => {
async function extractMetadata (store: MetadataStore): Promise<void> {
await store.allMap(async (map): Promise<void> => {
const knownEntries = Object.entries(knownGenesis);
const defs: Record<string, { def: MetadataDef, index: number, key: string }> = {};
const removals: string[] = [];
@@ -117,13 +117,16 @@ function extractMetadata (store: MetadataStore): void {
}
});
removals.forEach((key) => store.remove(key));
for (const key of removals) {
await store.remove(key);
}
Object.values(defs).forEach(({ def }) => addMetadata(def));
});
}
export default class State {
readonly #authUrls: AuthUrls = {};
#authUrls: AuthUrls = {};
readonly #authRequests: Record<string, AuthRequest> = {};
@@ -155,17 +158,20 @@ export default class State {
constructor (providers: Providers = {}) {
this.#providers = providers;
}
extractMetadata(this.#metaStore);
public async init () {
await extractMetadata(this.#metaStore);
// retrieve previously set authorizations
const authString = localStorage.getItem(AUTH_URLS_KEY) || '{}';
const storageAuthUrls: Record<string, string> = await chrome.storage.local.get(AUTH_URLS_KEY);
const authString = storageAuthUrls?.[AUTH_URLS_KEY] || '{}';
const previousAuth = JSON.parse(authString) as AuthUrls;
this.#authUrls = previousAuth;
// retrieve previously set default auth accounts
const defaultAuthString = localStorage.getItem(DEFAULT_AUTH_ACCOUNTS) || '[]';
const storageDefaultAuthAccounts: Record<string, string> = await chrome.storage.local.get(DEFAULT_AUTH_ACCOUNTS);
const defaultAuthString: string = storageDefaultAuthAccounts?.[DEFAULT_AUTH_ACCOUNTS] || '[]';
const previousDefaultAuth = JSON.parse(defaultAuthString) as string[];
this.defaultAuthAccountSelection = previousDefaultAuth;
@@ -209,6 +215,10 @@ export default class State {
return this.#authUrls;
}
private set authUrls (urls: AuthUrls) {
this.#authUrls = urls;
}
private popupClose (): void {
this.#windows.forEach((id: number) =>
withErrorLog(() => chrome.windows.remove(id))
@@ -230,7 +240,7 @@ export default class State {
}
private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void): Resolver<AuthResponse> => {
const complete = (authorizedAccounts: string[] = []) => {
const complete = async (authorizedAccounts: string[] = []) => {
const { idStr, request: { origin }, url } = this.#authRequests[id];
this.#authUrls[this.stripUrl(url)] = {
@@ -241,19 +251,21 @@ export default class State {
url
};
this.saveCurrentAuthList();
this.updateDefaultAuthAccounts(authorizedAccounts);
await this.saveCurrentAuthList();
await this.updateDefaultAuthAccounts(authorizedAccounts);
delete this.#authRequests[id];
this.updateIconAuth(true);
};
return {
reject: (error: Error): void => {
complete();
// eslint-disable-next-line @typescript-eslint/no-misused-promises
reject: async (error: Error): Promise<void> => {
await complete();
reject(error);
},
resolve: ({ authorizedAccounts, result }: AuthResponse): void => {
complete(authorizedAccounts);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
resolve: async ({ authorizedAccounts, result }: AuthResponse): Promise<void> => {
await complete(authorizedAccounts);
resolve({ authorizedAccounts, result });
}
};
@@ -289,17 +301,17 @@ export default class State {
this.updateIconAuth(true);
}
private saveCurrentAuthList () {
localStorage.setItem(AUTH_URLS_KEY, JSON.stringify(this.#authUrls));
private async saveCurrentAuthList () {
await chrome.storage.local.set({ [AUTH_URLS_KEY]: JSON.stringify(this.#authUrls) });
}
private saveDefaultAuthAccounts () {
localStorage.setItem(DEFAULT_AUTH_ACCOUNTS, JSON.stringify(this.defaultAuthAccountSelection));
private async saveDefaultAuthAccounts () {
await chrome.storage.local.set({ [DEFAULT_AUTH_ACCOUNTS]: JSON.stringify(this.defaultAuthAccountSelection) });
}
public updateDefaultAuthAccounts (newList: string[]) {
public async updateDefaultAuthAccounts (newList: string[]) {
this.defaultAuthAccountSelection = newList;
this.saveDefaultAuthAccounts();
await this.saveDefaultAuthAccounts();
}
private metaComplete = (id: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver<boolean> => {
@@ -358,20 +370,20 @@ export default class State {
: (signCount ? `${signCount}` : '')
);
withErrorLog(() => chrome.browserAction.setBadgeText({ text }));
withErrorLog(() => chrome.action.setBadgeText({ text }));
if (shouldClose && text === '') {
this.popupClose();
}
}
public removeAuthorization (url: string): AuthUrls {
public async removeAuthorization (url: string): Promise<AuthUrls> {
const entry = this.#authUrls[url];
assert(entry, `The source ${url} is not known`);
delete this.#authUrls[url];
this.saveCurrentAuthList();
await this.saveCurrentAuthList();
return this.#authUrls;
}
@@ -391,12 +403,12 @@ export default class State {
this.updateIcon(shouldClose);
}
public updateAuthorizedAccounts (authorizedAccountDiff: AuthorizedAccountsDiff): void {
public async updateAuthorizedAccounts (authorizedAccountDiff: AuthorizedAccountsDiff): Promise<void> {
authorizedAccountDiff.forEach(([url, authorizedAccountDiff]) => {
this.#authUrls[url].authorizedAccounts = authorizedAccountDiff;
});
this.saveCurrentAuthList();
await this.saveCurrentAuthList();
}
public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise<AuthResponse> {
@@ -539,8 +551,8 @@ export default class State {
return provider.unsubscribe(request.type, request.method, request.subscriptionId);
}
public saveMetadata (meta: MetadataDef): void {
this.#metaStore.set(meta.genesisHash, meta);
public async saveMetadata (meta: MetadataDef): Promise<void> {
await this.#metaStore.set(meta.genesisHash, meta);
addMetadata(meta);
}
@@ -188,7 +188,7 @@ export default class Tabs {
private redirectPhishingLanding (phishingWebsite: string): void {
const nonFragment = phishingWebsite.split('#')[0];
const encodedWebsite = encodeURIComponent(nonFragment);
const url = `${chrome.extension.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`;
const url = `${chrome.runtime.getURL('index.html')}#${PHISHING_PAGE_REDIRECT}/${encodedWebsite}`;
chrome.tabs.query({ url: nonFragment }, (tabs) => {
tabs
@@ -15,6 +15,8 @@ import Tabs from './Tabs.js';
export { withErrorLog } from './helpers.js';
const state = new State();
await state.init();
const extension = new Extension(state);
const tabs = new Tabs(state);
@@ -15,7 +15,7 @@ export default class AccountsStore extends BaseStore<KeyringJson> implements Key
);
}
public override set (key: string, value: KeyringJson, update?: () => void): void {
public override async set (key: string, value: KeyringJson, update?: () => void): Promise<void> {
// shortcut, don't save testing accounts in extension storage
if (key.startsWith('account:') && value.meta && value.meta.isTesting) {
update && update();
@@ -23,6 +23,6 @@ export default class AccountsStore extends BaseStore<KeyringJson> implements Key
return;
}
super.set(key, value, update);
await super.set(key, value, update);
}
}
+31 -17
View File
@@ -20,16 +20,19 @@ export default abstract class BaseStore <T> {
this.#prefix = prefix ? `${prefix}:` : '';
}
public all (update: (key: string, value: T) => void): void {
this.allMap((map): void => {
Object.entries(map).forEach(([key, value]): void => {
update(key, value);
});
public async all (update: (key: string, value: T) => void): Promise<void> {
await this.allMap(async (map): Promise<void> => {
const entries = Object.entries(map);
for (const [key, value] of entries) {
// eslint-disable-next-line @typescript-eslint/await-thenable
await update(key, value);
}
});
}
public allMap (update: (value: Record<string, T>) => void): void {
chrome.storage.local.get(null, (result: StoreValue): void => {
public async allMap (update: (value: Record<string, T>) => Promise<void>): Promise<void> {
await chrome.storage.local.get(null).then(async (result: StoreValue) => {
lastError('all');
const entries = Object.entries(result);
@@ -43,37 +46,48 @@ export default abstract class BaseStore <T> {
}
}
update(map);
await update(map);
}).catch(({ message }: Error) => {
console.error(`BaseStore error within allMap: ${message}`);
});
}
public get (key: string, update: (value: T) => void): void {
public async get (key: string, update: (value: T) => void): Promise<void> {
const prefixedKey = `${this.#prefix}${key}`;
chrome.storage.local.get([prefixedKey], (result: StoreValue): void => {
await chrome.storage.local.get([prefixedKey]).then(async (result: StoreValue) => {
lastError('get');
update(result[prefixedKey] as T);
// eslint-disable-next-line @typescript-eslint/await-thenable
await update(result[prefixedKey] as T);
}).catch(({ message }: Error) => {
console.error(`BaseStore error within get: ${message}`);
});
}
public remove (key: string, update?: () => void): void {
public async remove (key: string, update?: () => void): Promise<void> {
const prefixedKey = `${this.#prefix}${key}`;
chrome.storage.local.remove(prefixedKey, (): void => {
await chrome.storage.local.remove(prefixedKey).then(async () => {
lastError('remove');
update && update();
// eslint-disable-next-line @typescript-eslint/await-thenable
update && await update();
}).catch(({ message }: Error) => {
console.error(`BaseStore error within remove: ${message}`);
});
}
public set (key: string, value: T, update?: () => void): void {
public async set (key: string, value: T, update?: () => void): Promise<void> {
const prefixedKey = `${this.#prefix}${key}`;
chrome.storage.local.set({ [prefixedKey]: value }, (): void => {
await chrome.storage.local.set({ [prefixedKey]: value }).then(async () => {
lastError('set');
update && update();
// eslint-disable-next-line @typescript-eslint/await-thenable
update && await update();
}).catch(({ message }: Error) => {
console.error(`BaseStore error within set: ${message}`);
});
}
}
+31 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
/* eslint-disable @typescript-eslint/no-explicit-any */
import chrome from 'sinon-chrome';
import chrome from './chromeWrapper';
class MessagingFake {
private listeners: ((...params: unknown[]) => unknown)[] = [];
@@ -28,6 +28,36 @@ const messagingFake = new MessagingFake();
chrome.runtime.connect.returns(messagingFake);
chrome.storage.local.get.returns(
new Promise<Record<string, any>>((resolve, reject) => {
try {
const result = {
authUrls: JSON.stringify({
'localhost:3000': {
authorizedAccounts: ['5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX'],
count: 0,
id: '11',
origin: 'example.com',
url: 'http://localhost:3000'
}
})
};
resolve(result);
} catch (error) {
reject(error);
}
}));
chrome.storage.local.set.returns(
new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
(window as any).chrome = (globalThis as any).chrome = chrome;
@@ -0,0 +1,24 @@
// Copyright 2019-2024 @polkadot/extension-mocks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import sinonChrome from 'sinon-chrome';
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ChromeWrapper {
export interface IAction {
setBadgeText: (content: object) => Promise<void>;
}
export const action: IAction = {
setBadgeText: (_: object) => {
return new Promise<void>((resolve, _reject) => {
resolve();
});
}
};
}
const extendedSinonChrome = {
...sinonChrome,
action: ChromeWrapper.action
};
export default extendedSinonChrome;
@@ -1,6 +1,8 @@
// Copyright 2019-2024 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
/* global chrome */
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { PASSWORD_EXPIRY_MIN } from '@polkadot/extension-base/defaults';
@@ -30,25 +32,46 @@ function SignArea ({ buttonText, className, error, isExternal, isFirst, setError
const { t } = useTranslation();
useEffect(() => {
setIsLocked(null);
let timeout: ReturnType<typeof setTimeout>;
const lockSigner = async () => {
setIsLocked(null);
try {
const { isLocked, remainingTime } = await isSignLocked(signId);
!isExternal && isSignLocked(signId)
.then(({ isLocked, remainingTime }) => {
setIsLocked(isLocked);
timeout = setTimeout(() => {
setIsLocked(true);
}, remainingTime);
// if the account was unlocked check the remember me
// automatically to prolong the unlock period
!isLocked && setSavePass(true);
})
.catch((error: Error) => console.error(error));
await chrome.alarms.create('SIGNER_TIMEOUT', { delayInMinutes: remainingTime / 60000 });
return () => {
!!timeout && clearTimeout(timeout);
if (!isLocked) {
setSavePass(true);
}
} catch (error) {
console.error('Error locking signer:', error);
}
};
const resetAlarm = async () => {
return new Promise((resolve, reject) => {
chrome.alarms.clear('SIGNER_TIMEOUT', (cleared) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(cleared);
}
});
});
};
const executeLockSigner = async () => {
if (!isExternal) {
await lockSigner();
await resetAlarm();
}
};
executeLockSigner().then(async () => {
return await executeLockSigner();
}).catch((error) => console.error('Error clearing the alarm: ', error));
}, [isExternal, signId]);
const _onSign = useCallback(
@@ -80,7 +103,7 @@ function SignArea ({ buttonText, className, error, isExternal, isFirst, setError
const RememberPasswordCheckbox = () => (
<Checkbox
checked={savePass}
label={ isLocked
label={isLocked
? t(
'Remember my password for the next {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
@@ -130,7 +153,7 @@ function SignArea ({ buttonText, className, error, isExternal, isFirst, setError
);
}
export default styled(SignArea)<Props>`
export default styled(SignArea) <Props>`
flex-direction: column;
padding: 6px 24px;
@@ -79,6 +79,8 @@ export default function Popup (): React.ReactElement {
const [settingsCtx, setSettingsCtx] = useState<SettingsStruct>(startSettings);
const history = useHistory();
console.log('WINDOW; ', window);
const _onAction = useCallback(
(to?: string): void => {
setWelcomeDone(window.localStorage.getItem('welcome_read') === 'ok');
@@ -1,7 +1,9 @@
// Copyright 2019-2024 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useState } from 'react';
/* global chrome */
import React, { useCallback, useEffect, useState } from 'react';
import { ToastContext } from '../index.js';
import Toast from './Toast.js';
@@ -11,18 +13,37 @@ interface ToastProviderProps {
}
const TOAST_TIMEOUT = 1500;
const ALARM_NAME = 'toastAlarm';
const ToastProvider = ({ children }: ToastProviderProps): React.ReactElement<ToastProviderProps> => {
const [content, setContent] = useState('');
const [visible, setVisible] = useState(false);
const show = useCallback((message: string): () => void => {
const timerId = setTimeout(() => setVisible(false), TOAST_TIMEOUT);
useEffect(() => {
const createAlarm = async () => {
try {
await chrome.alarms.create(ALARM_NAME, { delayInMinutes: TOAST_TIMEOUT / 60000 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
setVisible(false);
}
});
} catch (error) {
console.error('Error creating the alarm:', error);
throw error;
}
};
createAlarm().then(async () => {
return await chrome.alarms.clear(ALARM_NAME);
}).catch((error) => console.error('Error clearing the alarm: ', error));
}, []);
const show = useCallback((message: string): (() => void) => {
setContent(message);
setVisible(true);
return (): void => clearTimeout(timerId);
return (): void => setVisible(false);
}, []);
return (
+18 -7
View File
@@ -4,13 +4,13 @@
"homepage_url": "https://github.com/polkadot-js/extension",
"name": "polkadot{.js} extension",
"short_name": "polkadot{.js}",
"manifest_version": 2,
"permissions": ["storage", "tabs"],
"manifest_version": 3,
"permissions": ["storage", "tabs", "alarms"],
"background": {
"scripts": ["background.js"],
"persistent": true
"service_worker": "background.js",
"type": "module"
},
"browser_action": {
"action": {
"default_title": "polkadot{.js}",
"default_popup": "index.html"
},
@@ -26,6 +26,17 @@
"64": "images/icon-64.png",
"128": "images/icon-128.png"
},
"web_accessible_resources": ["page.js"],
"content_security_policy": "script-src 'self' blob: 'unsafe-eval' 'wasm-eval'; object-src 'self'"
"web_accessible_resources": [
{
"resources": [
"page.js"
],
"matches": [
"*://*/*"
]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ import { assert } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
// setup the notification (same a FF default background, white text)
withErrorLog(() => chrome.browserAction.setBadgeBackgroundColor({ color: '#d90000' }));
withErrorLog(() => chrome.action.setBadgeBackgroundColor({ color: '#d90000' }));
// listen to all messages and handle appropriately
chrome.runtime.onConnect.addListener((port): void => {
+1 -1
View File
@@ -27,7 +27,7 @@ window.addEventListener('message', ({ data, source }: Message): void => {
// inject our data injector
const script = document.createElement('script');
script.src = chrome.extension.getURL('page.js');
script.src = chrome.runtime.getURL('page.js');
script.onload = (): void => {
// remove the injecting tag when loaded