migrate background related things from chrome

This commit is contained in:
DEV Sam Hayes
2025-02-07 22:26:34 +01:00
parent b20faf2359
commit 6c43a60810
7 changed files with 1002 additions and 19 deletions

View File

@@ -5,37 +5,26 @@
"version": "0.0.1",
"homepage_url": "https://getgooti.com",
"options_page": "options.html",
"permissions": [
"storage"
],
"permissions": ["storage"],
"action": {
"default_popup": "index.html",
"default_icon": "gooti-with-bg.png"
},
"background": {
"scripts": [
"background.js"
]
"scripts": ["background.js"]
},
"content_scripts": [
{
"run_at": "document_end",
"matches": [
"<all_urls>"
],
"js": [
"gooti-content-script.js"
]
"run_at": "document_start",
"matches": ["<all_urls>"],
"js": ["gooti-content-script.js"],
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": [
"gooti-extension.js"
],
"matches": [
"<all_urls>"
]
"resources": ["gooti-extension.js"],
"matches": ["<all_urls>"]
}
],
"browser_specific_settings": {

View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<head>
<title>Gooti</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="scripts.js"></script>
<style>
body {
background: var(--background);
height: 100vh;
width: 100vw;
color: #ffffff;
font-size: 16px;
}
.color-primary {
color: var(--primary);
}
.page {
height: 100%;
display: grid;
grid-template-rows: 1fr 60px;
grid-template-columns: 1fr;
overflow-y: hidden;
}
.card {
padding: var(--size);
background: var(--background-light);
border-radius: 8px;
color: #ffffff;
display: flex;
flex-direction: column;
}
.json {
white-space: pre;
overflow-y: auto;
font-size: 12px;
color: gray;
}
.text {
white-space: normal;
overflow-y: auto;
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<div class="page">
<div class="sam-flex-column" style="overflow-y: auto">
<div class="sam-text-header">
<span id="titleSpan" style="font-weight: 400 !important"></span>
</div>
<span
class="host-INSERT sam-align-self-center sam-text-muted"
style="font-weight: 500"
></span>
<!-- Card for getPublicKey -->
<div id="cardGetPublicKey" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">read your public key</b> <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card for getRelays -->
<div id="cardGetRelays" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">read your relays</b> <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card for signEvent -->
<div id="cardSignEvent" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">sign an event</b> (kind
<span id="kindSpan"></span>) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for signEvent -->
<div id="card2SignEvent" class="card sam-mt sam-ml sam-mr">
<div id="card2SignEvent_json" class="json"></div>
</div>
<!-- Card for nip04.encrypt -->
<div id="cardNip04Encrypt" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">encrypt a text</b> (NIP04) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for nip04.encrypt -->
<div id="card2Nip04Encrypt" class="card sam-mt sam-ml sam-mr">
<div id="card2Nip04Encrypt_text" class="text"></div>
</div>
<!-- Card for nip04.decrypt -->
<div id="cardNip04Decrypt" class="card sam-mt sam-ml sam-mr">
<span style="text-align: center">
<b><span class="host-INSERT color-primary"></span></b>
is requesting permission to<br />
<br />
<b class="color-primary">decrypt a text</b> (NIP04) <br />
<br />
<span>
for the selected identity
<span
style="font-weight: 500"
class="nick-INSERT color-primary"
></span>
</span>
</span>
</div>
<!-- Card2 for nip04.decrypt -->
<div id="card2Nip04Decrypt" class="card sam-mt sam-ml sam-mr">
<div id="card2Nip04Decrypt_text" class="text"></div>
</div>
</div>
<!------------->
<!-- ACTIONS -->
<!------------->
<div class="sam-footer-grid-2">
<div class="btn-group">
<button id="rejectButton" type="button" class="btn btn-secondary">
Reject
</button>
<button
type="button"
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button id="rejectJustOnceButton" class="dropdown-item">
just once
</button>
</li>
</ul>
</div>
<div class="btn-group">
<button id="approveButton" type="button" class="btn btn-primary">
Approve
</button>
<button
type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<button id="approveJustOnceButton" class="dropdown-item" href="#">
just once
</button>
</li>
</ul>
</div>
</div>
</div>
<script src="prompt.js"></script>
</body>
</html>

View File

@@ -0,0 +1,288 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
BrowserSessionData,
BrowserSyncData,
BrowserSyncFlow,
CryptoHelper,
GootiMetaData,
Identity_DECRYPTED,
Nip07Method,
Nip07MethodPolicy,
NostrHelper,
Permission_DECRYPTED,
Permission_ENCRYPTED,
} from '@common';
import { Event, EventTemplate, finalizeEvent, nip04 } from 'nostr-tools';
import { FirefoxMetaHandler } from './app/common/data/firefox-meta-handler';
import browser from 'webextension-polyfill';
export const debug = function (message: any) {
const dateString = new Date().toISOString();
console.log(`[Gooti - ${dateString}]: ${JSON.stringify(message)}`);
};
export type PromptResponse =
| 'reject'
| 'reject-once'
| 'approve'
| 'approve-once';
export interface PromptResponseMessage {
id: string;
response: PromptResponse;
}
export interface BackgroundRequestMessage {
method: Nip07Method;
params: any;
host: string;
}
export const getBrowserSessionData = async function (): Promise<
BrowserSessionData | undefined
> {
const browserSessionData = await browser.storage.session.get(null);
if (Object.keys(browserSessionData).length === 0) {
return undefined;
}
return browserSessionData as unknown as BrowserSessionData;
};
export const getBrowserSyncData = async function (): Promise<
BrowserSyncData | undefined
> {
const gootiMetaHandler = new FirefoxMetaHandler();
const gootiMetaData =
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
let browserSyncData: BrowserSyncData | undefined;
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
browserSyncData = (await browser.storage.local.get(
null
)) as unknown as BrowserSyncData;
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
browserSyncData = (await browser.storage.sync.get(
null
)) as unknown as BrowserSyncData;
}
return browserSyncData;
};
export const savePermissionsToBrowserSyncStorage = async function (
permissions: Permission_ENCRYPTED[]
): Promise<void> {
const gootiMetaHandler = new FirefoxMetaHandler();
const gootiMetaData =
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
await browser.storage.local.set({ permissions });
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
await browser.storage.sync.set({ permissions });
}
};
export const checkPermissions = function (
browserSessionData: BrowserSessionData,
identity: Identity_DECRYPTED,
host: string,
method: Nip07Method,
params: any
): boolean | undefined {
const permissions = browserSessionData.permissions.filter(
(x) =>
x.identityId === identity.id && x.host === host && x.method === method
);
if (permissions.length === 0) {
return undefined;
}
if (method === 'getPublicKey') {
// No evaluation of params required.
return permissions.every((x) => x.methodPolicy === 'allow');
}
if (method === 'getRelays') {
// No evaluation of params required.
return permissions.every((x) => x.methodPolicy === 'allow');
}
if (method === 'signEvent') {
// Evaluate params.
const eventTemplate = params as EventTemplate;
if (
permissions.find(
(x) => x.methodPolicy === 'allow' && typeof x.kind === 'undefined'
)
) {
return true;
}
if (
permissions.some(
(x) => x.methodPolicy === 'allow' && x.kind === eventTemplate.kind
)
) {
return true;
}
if (
permissions.some(
(x) => x.methodPolicy === 'deny' && x.kind === eventTemplate.kind
)
) {
return false;
}
return undefined;
}
if (method === 'nip04.encrypt') {
// No evaluation of params required.
return permissions.every((x) => x.methodPolicy === 'allow');
}
if (method === 'nip04.decrypt') {
// No evaluation of params required.
return permissions.every((x) => x.methodPolicy === 'allow');
}
return undefined;
};
export const storePermission = async function (
browserSessionData: BrowserSessionData,
identity: Identity_DECRYPTED,
host: string,
method: Nip07Method,
methodPolicy: Nip07MethodPolicy,
kind?: number
) {
const browserSyncData = await getBrowserSyncData();
if (!browserSyncData) {
throw new Error(`Could not retrieve sync data`);
}
const permission: Permission_DECRYPTED = {
id: crypto.randomUUID(),
identityId: identity.id,
host,
method,
methodPolicy,
kind,
};
// Store session data
await browser.storage.session.set({
permissions: [...browserSessionData.permissions, permission],
});
// Encrypt permission to store in sync storage (depending on sync flow).
const encryptedPermission = await encryptPermission(
permission,
browserSessionData.iv,
browserSessionData.vaultPassword as string
);
await savePermissionsToBrowserSyncStorage([
...browserSyncData.permissions,
encryptedPermission,
]);
};
export const getPosition = async function (width: number, height: number) {
let left = 0;
let top = 0;
try {
const lastFocused = await browser.windows.getLastFocused();
if (
lastFocused &&
lastFocused.top !== undefined &&
lastFocused.left !== undefined &&
lastFocused.width !== undefined &&
lastFocused.height !== undefined
) {
// Position window in the center of the lastFocused window
top = Math.round(lastFocused.top + (lastFocused.height - height) / 2);
left = Math.round(lastFocused.left + (lastFocused.width - width) / 2);
} else {
console.error('Last focused window properties are undefined.');
}
} catch (error) {
console.error('Error getting window position:', error);
}
return {
top,
left,
};
};
export const signEvent = function (
eventTemplate: EventTemplate,
privkey: string
): Event {
return finalizeEvent(eventTemplate, NostrHelper.hex2bytes(privkey));
};
export const nip04Encrypt = async function (
privkey: string,
peerPubkey: string,
plaintext: string
): Promise<string> {
return await nip04.encrypt(
NostrHelper.hex2bytes(privkey),
peerPubkey,
plaintext
);
};
export const nip04Decrypt = async function (
privkey: string,
peerPubkey: string,
ciphertext: string
): Promise<string> {
return await nip04.decrypt(
NostrHelper.hex2bytes(privkey),
peerPubkey,
ciphertext
);
};
const encryptPermission = async function (
permission: Permission_DECRYPTED,
iv: string,
password: string
): Promise<Permission_ENCRYPTED> {
const encryptedPermission: Permission_ENCRYPTED = {
id: await encrypt(permission.id, iv, password),
identityId: await encrypt(permission.identityId, iv, password),
host: await encrypt(permission.host, iv, password),
method: await encrypt(permission.method, iv, password),
methodPolicy: await encrypt(permission.methodPolicy, iv, password),
};
if (typeof permission.kind !== 'undefined') {
encryptedPermission.kind = await encrypt(
permission.kind.toString(),
iv,
password
);
}
return encryptedPermission;
};
const encrypt = async function (
value: string,
iv: string,
password: string
): Promise<string> {
return await CryptoHelper.encrypt(value, iv, password);
};

View File

@@ -0,0 +1,148 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NostrHelper } from '@common';
import {
BackgroundRequestMessage,
checkPermissions,
debug,
getBrowserSessionData,
getPosition,
nip04Decrypt,
nip04Encrypt,
PromptResponse,
PromptResponseMessage,
signEvent,
storePermission,
} from './background-common';
import browser from 'webextension-polyfill';
import { Buffer } from 'buffer';
type Relays = Record<string, { read: boolean; write: boolean }>;
const openPrompts = new Map<
string,
{
resolve: (response: PromptResponse) => void;
reject: (reason?: any) => void;
}
>();
browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
debug('Message received');
const request = message as BackgroundRequestMessage | PromptResponseMessage;
debug(request);
if ((request as PromptResponseMessage)?.id) {
// Handle prompt response
const promptResponse = request as PromptResponseMessage;
const openPrompt = openPrompts.get(promptResponse.id);
if (!openPrompt) {
throw new Error(
'Prompt response could not be matched to any previous request.'
);
}
openPrompt.resolve(promptResponse.response);
openPrompts.delete(promptResponse.id);
return;
}
const browserSessionData = await getBrowserSessionData();
if (!browserSessionData) {
throw new Error('Gooti vault not unlocked by the user.');
}
const currentIdentity = browserSessionData.identities.find(
(x) => x.id === browserSessionData.selectedIdentityId
);
if (!currentIdentity) {
throw new Error('No Nostr identity available at endpoint.');
}
const req = request as BackgroundRequestMessage;
const permissionState = checkPermissions(
browserSessionData,
currentIdentity,
req.host,
req.method,
req.params
);
if (permissionState === false) {
throw new Error('Permission denied');
}
if (permissionState === undefined) {
// Ask user for permission.
const width = 375;
const height = 600;
const { top, left } = await getPosition(width, height);
const base64Event = Buffer.from(
JSON.stringify(req.params ?? {}, undefined, 2)
).toString('base64');
const response = await new Promise<PromptResponse>((resolve, reject) => {
const id = crypto.randomUUID();
openPrompts.set(id, { resolve, reject });
browser.windows.create({
type: 'popup',
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
height,
width,
top,
left,
});
});
debug(response);
if (response === 'approve' || response === 'reject') {
await storePermission(
browserSessionData,
currentIdentity,
req.host,
req.method,
response === 'approve' ? 'allow' : 'deny',
req.params?.kind
);
}
if (['reject', 'reject-once'].includes(response)) {
throw new Error('Permission denied');
}
} else {
debug('Request allowed (via saved permission).');
}
const relays: Relays = {};
switch (req.method) {
case 'getPublicKey':
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
case 'signEvent':
return signEvent(req.params, currentIdentity.privkey);
case 'getRelays':
browserSessionData.relays.forEach((x) => {
relays[x.url] = { read: x.read, write: x.write };
});
return relays;
case 'nip04.encrypt':
return await nip04Encrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.plaintext
);
case 'nip04.decrypt':
return await nip04Decrypt(
currentIdentity.privkey,
req.params.peerPubkey,
req.params.ciphertext
);
default:
throw new Error(`Not supported request method '${req.method}'.`);
}
});

View File

@@ -0,0 +1,42 @@
import browser from 'webextension-polyfill';
import { BackgroundRequestMessage } from './background-common';
// Inject the script that will provide window.nostr
// The script needs to run before any other scripts from the real
// page run (and maybe check for window.nostr).
const script = document.createElement('script');
script.setAttribute('async', 'false');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', browser.runtime.getURL('gooti-extension.js'));
(document.head || document.documentElement).appendChild(script);
// listen for messages from that script
window.addEventListener('message', async (message) => {
// We will also receive our own messages, that we sent.
// We have to ignore them (they will not have a params field).
if (message.source !== window) return;
if (!message.data) return;
if (!message.data.params) return;
if (message.data.ext !== 'gooti') return;
// pass on to background
let response;
try {
const request: BackgroundRequestMessage = {
method: message.data.method,
params: message.data.params,
host: location.host,
};
response = await browser.runtime.sendMessage(request);
} catch (error) {
response = { error };
}
// return response
window.postMessage(
{ id: message.data.id, ext: 'gooti', response },
message.origin
);
});

View File

@@ -0,0 +1,128 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Event, EventTemplate } from 'nostr-tools';
import { Nip07Method } from '@common';
type Relays = Record<string, { read: boolean; write: boolean }>;
class Messenger {
#requests = new Map<
string,
{
resolve: (value: unknown) => void;
reject: (reason: any) => void;
}
>();
constructor() {
window.addEventListener('message', this.#handleCallResponse.bind(this));
}
async request(method: Nip07Method, params: any): Promise<any> {
const id = crypto.randomUUID();
return new Promise((resolve, reject) => {
this.#requests.set(id, { resolve, reject });
window.postMessage(
{
id,
ext: 'gooti',
method,
params,
},
'*'
);
});
}
#handleCallResponse(message: MessageEvent) {
// We also will receive our own messages, that we sent.
// We have to ignore them (they will not have a response field).
if (
!message.data ||
message.data.response === null ||
message.data.response === undefined ||
message.data.ext !== 'gooti' ||
!this.#requests.has(message.data.id)
) {
return;
}
if (message.data.response.error) {
this.#requests.get(message.data.id)?.reject(message.data.response.error);
} else {
this.#requests.get(message.data.id)?.resolve(message.data.response);
}
this.#requests.delete(message.data.id);
}
}
const nostr = {
messenger: new Messenger(),
async getPublicKey(): Promise<string> {
debug('getPublicKey received');
const pubkey = await this.messenger.request('getPublicKey', {});
debug(`getPublicKey response:`);
debug(pubkey);
return pubkey;
},
async signEvent(event: EventTemplate): Promise<Event> {
debug('signEvent received');
const signedEvent = await this.messenger.request('signEvent', event);
debug('signEvent response:');
debug(signedEvent);
return signedEvent;
},
async getRelays(): Promise<Relays> {
debug('getRelays received');
const relays = (await this.messenger.request('getRelays', {})) as Relays;
debug('getRelays response:');
debug(relays);
return relays;
},
nip04: {
that: this,
async encrypt(peerPubkey: string, plaintext: string): Promise<string> {
debug('nip04.encrypt received');
const ciphertext = (await nostr.messenger.request('nip04.encrypt', {
peerPubkey,
plaintext,
})) as string;
debug('nip04.encrypt response:');
debug(ciphertext);
return ciphertext;
},
async decrypt(peerPubkey: string, ciphertext: string): Promise<string> {
debug('nip04.decrypt received');
const plaintext = (await nostr.messenger.request('nip04.decrypt', {
peerPubkey,
ciphertext,
})) as string;
debug('nip04.decrypt response:');
debug(plaintext);
return plaintext;
},
},
// nip44: {
// async encrypt(peer, plaintext) {
// return window.nostr._call('nip44.encrypt', { peer, plaintext });
// },
// async decrypt(peer, ciphertext) {
// return window.nostr._call('nip44.decrypt', { peer, ciphertext });
// },
// },
};
window.nostr = nostr as any;
const debug = function (value: any) {
console.log(JSON.stringify(value));
};

View File

@@ -0,0 +1,167 @@
import browser from 'webextension-polyfill';
import { Buffer } from 'buffer';
import { Nip07Method } from '@common';
import { PromptResponse, PromptResponseMessage } from './background-common';
const params = new URLSearchParams(location.search);
const id = params.get('id') as string;
const method = params.get('method') as Nip07Method;
const host = params.get('host') as string;
const nick = params.get('nick') as string;
const event = Buffer.from(params.get('event') as string, 'base64').toString();
let title = '';
switch (method) {
case 'getPublicKey':
title = 'Get Public Key';
break;
case 'signEvent':
title = 'Sign Event';
break;
case 'nip04.encrypt':
title = 'Encrypt';
break;
case 'nip04.decrypt':
title = 'Decrypt';
break;
case 'getRelays':
title = 'Get Relays';
break;
default:
break;
}
const titleSpanElement = document.getElementById('titleSpan');
if (titleSpanElement) {
titleSpanElement.innerText = title;
}
Array.from(document.getElementsByClassName('nick-INSERT')).forEach(
(element) => {
(element as HTMLElement).innerText = nick;
}
);
Array.from(document.getElementsByClassName('host-INSERT')).forEach(
(element) => {
(element as HTMLElement).innerText = host;
}
);
const kindSpanElement = document.getElementById('kindSpan');
if (kindSpanElement) {
kindSpanElement.innerText = JSON.parse(event).kind;
}
const cardGetPublicKeyElement = document.getElementById('cardGetPublicKey');
if (cardGetPublicKeyElement) {
if (method === 'getPublicKey') {
// Do nothing.
} else {
cardGetPublicKeyElement.style.display = 'none';
}
}
const cardGetRelaysElement = document.getElementById('cardGetRelays');
if (cardGetRelaysElement) {
if (method === 'getRelays') {
// Do nothing.
} else {
cardGetRelaysElement.style.display = 'none';
}
}
const cardSignEventElement = document.getElementById('cardSignEvent');
const card2SignEventElement = document.getElementById('card2SignEvent');
if (cardSignEventElement && card2SignEventElement) {
if (method === 'signEvent') {
const card2SignEvent_jsonElement = document.getElementById(
'card2SignEvent_json'
);
if (card2SignEvent_jsonElement) {
card2SignEvent_jsonElement.innerText = event;
}
} else {
cardSignEventElement.style.display = 'none';
card2SignEventElement.style.display = 'none';
}
}
const cardNip04EncryptElement = document.getElementById('cardNip04Encrypt');
const card2Nip04EncryptElement = document.getElementById('card2Nip04Encrypt');
if (cardNip04EncryptElement && card2Nip04EncryptElement) {
if (method === 'nip04.encrypt') {
const card2Nip04Encrypt_textElement = document.getElementById(
'card2Nip04Encrypt_text'
);
if (card2Nip04Encrypt_textElement) {
const eventObject: { peerPubkey: string; plaintext: string } =
JSON.parse(event);
card2Nip04Encrypt_textElement.innerText = eventObject.plaintext;
}
} else {
cardNip04EncryptElement.style.display = 'none';
card2Nip04EncryptElement.style.display = 'none';
}
}
const cardNip04DecryptElement = document.getElementById('cardNip04Decrypt');
const card2Nip04DecryptElement = document.getElementById('card2Nip04Decrypt');
if (cardNip04DecryptElement && card2Nip04DecryptElement) {
if (method === 'nip04.decrypt') {
const card2Nip04Decrypt_textElement = document.getElementById(
'card2Nip04Decrypt_text'
);
if (card2Nip04Decrypt_textElement) {
const eventObject: { peerPubkey: string; ciphertext: string } =
JSON.parse(event);
card2Nip04Decrypt_textElement.innerText = eventObject.ciphertext;
}
} else {
cardNip04DecryptElement.style.display = 'none';
card2Nip04DecryptElement.style.display = 'none';
}
}
//
// Functions
//
function deliver(response: PromptResponse) {
const message: PromptResponseMessage = {
id,
response,
};
browser.runtime.sendMessage(message);
window.close();
}
document.addEventListener('DOMContentLoaded', function () {
const rejectJustOnceButton = document.getElementById('rejectJustOnceButton');
rejectJustOnceButton?.addEventListener('click', () => {
deliver('reject-once');
});
const rejectButton = document.getElementById('rejectButton');
rejectButton?.addEventListener('click', () => {
deliver('reject');
});
const approveJustOnceButton = document.getElementById(
'approveJustOnceButton'
);
approveJustOnceButton?.addEventListener('click', () => {
deliver('approve-once');
});
const approveButton = document.getElementById('approveButton');
approveButton?.addEventListener('click', () => {
deliver('approve');
});
});