mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-13 00:58:20 -05:00
Compare commits
9 Commits
requestbui
...
contextMen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
727e147690 | ||
|
|
2db735e04f | ||
|
|
f6e582016b | ||
|
|
21ebcd1a11 | ||
|
|
5063d6cb45 | ||
|
|
3d5e3ce4ac | ||
|
|
d47cf0d8ea | ||
|
|
25f35d0051 | ||
|
|
5ccdd9b06a |
6180
package-lock.json
generated
6180
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.1.0.6",
|
||||
"version": "0.1.0.700",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -38,8 +38,8 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.6.2",
|
||||
"tlsn-jsV5.3": "npm:tlsn-js@0.1.0-alpha.5.3"
|
||||
"tlsn-js": "0.1.0-alpha.7.1",
|
||||
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
You can find example plugins at https://github.com/tlsnotary/tlsn-plugin-boilerplate/tree/main/examples
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -72,11 +72,11 @@ dependencies:
|
||||
specifier: ^3.3.3
|
||||
version: 3.4.3
|
||||
tlsn-js:
|
||||
specifier: 0.1.0-alpha.6.2
|
||||
version: 0.1.0-alpha.6.2
|
||||
tlsn-jsV5.3:
|
||||
specifier: npm:tlsn-js@0.1.0-alpha.5.3
|
||||
version: /tlsn-js@0.1.0-alpha.5.3
|
||||
specifier: 0.1.0-alpha.7.1
|
||||
version: 0.1.0-alpha.7.1
|
||||
tlsn-js-v5:
|
||||
specifier: npm:tlsn-js@0.1.0-alpha.5.4
|
||||
version: /tlsn-js@0.1.0-alpha.5.4
|
||||
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
@@ -7974,16 +7974,22 @@ packages:
|
||||
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
|
||||
dev: true
|
||||
|
||||
/tlsn-js@0.1.0-alpha.5.3:
|
||||
resolution: {integrity: sha512-eTSCQ6MaH8mN2oCfLsQDe/mdfTlIq74MbKVesV6M01C2SRE0FJcVlTWMsnT3L+wOpvOlvsiBeCWV7T9m/YMzew==}
|
||||
/tlsn-js@0.1.0-alpha.5.4:
|
||||
resolution: {integrity: sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==}
|
||||
engines: {node: '>= 16.20.2'}
|
||||
dependencies:
|
||||
comlink: 4.4.1
|
||||
dev: false
|
||||
|
||||
/tlsn-js@0.1.0-alpha.6.2:
|
||||
resolution: {integrity: sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw==}
|
||||
/tlsn-js@0.1.0-alpha.7.1:
|
||||
resolution: {integrity: sha512-EWdRp1VQBfdre8jehJgmDjtDvt01ZL1JWbcscctnFTLIIwMYS7IBxU07UYG0NMZFXeTE8PlrUDEVwEl1+vla+g==}
|
||||
engines: {node: '>= 16.20.2'}
|
||||
dependencies:
|
||||
tlsn-wasm: 0.1.0-alpha.7.2
|
||||
dev: false
|
||||
|
||||
/tlsn-wasm@0.1.0-alpha.7.2:
|
||||
resolution: {integrity: sha512-NzrDfOxmFtMHDb4lmMsx6RaS6F+IVXEHxK0zow8jpnx+NryuJ+qnp4380Lq0uj61w/Yuq+yzOhzFe6Bpeo59dA==}
|
||||
dev: false
|
||||
|
||||
/tmp@0.0.33:
|
||||
|
||||
BIN
src/assets/plugins/discord_dm.wasm
Normal file
BIN
src/assets/plugins/discord_dm.wasm
Normal file
Binary file not shown.
BIN
src/assets/plugins/twitter_profile.wasm
Normal file
BIN
src/assets/plugins/twitter_profile.wasm
Normal file
Binary file not shown.
115
src/components/Menu/index.tsx
Normal file
115
src/components/Menu/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../Icon';
|
||||
import browser from 'webextension-polyfill';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import PluginUploadInfo from '../PluginInfo';
|
||||
|
||||
export function MenuIcon(): ReactElement {
|
||||
const [opened, setOpen] = useState(false);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setOpen(!opened);
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{opened && (
|
||||
<>
|
||||
<div
|
||||
className="fixed top-0 left-0 w-screen h-screen z-10"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
<Menu opened={opened} setOpen={setOpen} />
|
||||
</>
|
||||
)}
|
||||
<Icon
|
||||
fa="fa-solid fa-bars"
|
||||
className="text-slate-500 hover:text-slate-700 active:text-slate-900 cursor-pointer z-20"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Menu(props: {
|
||||
opened: boolean;
|
||||
setOpen: (opened: boolean) => void;
|
||||
}): ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const openExtensionInPage = () => {
|
||||
props.setOpen(false);
|
||||
browser.tabs.create({
|
||||
url: `chrome-extension://${chrome.runtime.id}/popup.html`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-[100%] right-0 rounded-md z-20">
|
||||
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
|
||||
<MenuRow
|
||||
fa="fa-solid fa-plus"
|
||||
className="relative"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
}}
|
||||
>
|
||||
<PluginUploadInfo />
|
||||
<span>Install Plugin</span>
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
fa="fa-solid fa-toolbox"
|
||||
className="border-b border-slate-300"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
navigate('/plugins');
|
||||
}}
|
||||
>
|
||||
Plugins
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
className="lg:hidden"
|
||||
fa="fa-solid fa-up-right-and-down-left-from-center"
|
||||
onClick={openExtensionInPage}
|
||||
>
|
||||
Expand
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
fa="fa-solid fa-gear"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
navigate('/options');
|
||||
}}
|
||||
>
|
||||
Options
|
||||
</MenuRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuRow(props: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
onClick?: MouseEventHandler;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center py-3 px-4 gap-2 hover:bg-slate-300 cursor-pointer text-slate-800 hover:text-slate-900 font-semibold',
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon size={0.875} fa={props.fa} />
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '../../utils/plugins';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import classNames from 'classnames';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
|
||||
export default function PluginUploadInfo(): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
@@ -122,7 +123,7 @@ export function PluginInfoModal(props: {
|
||||
<ModalHeader className="w-full p-2 border-gray-200 text-gray-500">
|
||||
{header || (
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<img className="h-5" src={logo} alt="logo" />
|
||||
<img className="h-5" src={logo || DefaultPluginIcon} alt="logo" />
|
||||
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -132,7 +133,7 @@ export function PluginInfoModal(props: {
|
||||
<>
|
||||
<img
|
||||
className="w-12 h-12"
|
||||
src={pluginContent.icon}
|
||||
src={pluginContent.icon || DefaultPluginIcon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-center">
|
||||
|
||||
@@ -5,12 +5,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
fetchPluginHashes,
|
||||
removePlugin,
|
||||
fetchPluginConfigByHash,
|
||||
runPlugin,
|
||||
} from '../../utils/rpc';
|
||||
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
|
||||
import { usePluginHashes } from '../../reducers/plugins';
|
||||
import { PluginConfig } from '../../utils/misc';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
@@ -169,7 +164,7 @@ export function Plugin(props: {
|
||||
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
|
||||
<img
|
||||
className="w-12 h-12 mb-2"
|
||||
src={config.icon}
|
||||
src={config.icon || DefaultPluginIcon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-blue-600 font-semibold">
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
getMaxSent,
|
||||
} from '../../utils/storage';
|
||||
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
|
||||
import { urlify } from '../../utils/misc';
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { BackgroundActiontype, RequestLog } from '../../entries/Background/rpc';
|
||||
import { useNavigate } from 'react-router';
|
||||
import Fuse from 'fuse.js';
|
||||
import Icon from '../Icon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setRequests } from '../../reducers/requests';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
requests: RequestLog[];
|
||||
shouldFix?: boolean;
|
||||
};
|
||||
|
||||
export default function RequestTable(props: Props): ReactElement {
|
||||
@@ -47,7 +55,14 @@ export default function RequestTable(props: Props): ReactElement {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<div className="flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2',
|
||||
{
|
||||
'fixed top-[4.5rem] w-full shadow': props.shouldFix,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className="input w-full"
|
||||
type="text"
|
||||
@@ -61,7 +76,7 @@ export default function RequestTable(props: Props): ReactElement {
|
||||
onClick={reset}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto h-0">
|
||||
<div className="flex-grow">
|
||||
<table className="border border-slate-300 border-collapse table-fixed w-full">
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
|
||||
@@ -28,6 +28,12 @@ const cookiesDb = db.sublevel<string, boolean>('cookies', {
|
||||
const headersDb = db.sublevel<string, boolean>('headers', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const appDb = db.sublevel<string, any>('app', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
enum AppDatabaseKey {
|
||||
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
|
||||
}
|
||||
|
||||
export async function addNotaryRequest(
|
||||
now = Date.now(),
|
||||
@@ -372,3 +378,19 @@ export async function getHeadersByHost(host: string) {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function getDefaultPluginsInstalled(): Promise<boolean> {
|
||||
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
|
||||
}
|
||||
|
||||
export async function setDefaultPluginsInstalled(installed = false) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAppState() {
|
||||
return {
|
||||
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
|
||||
import { deleteCacheByTabId } from './cache';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { getAppState, setDefaultPluginsInstalled } from './db';
|
||||
import { installPlugin } from './plugins/utils';
|
||||
|
||||
(async () => {
|
||||
browser.webRequest.onSendHeaders.addListener(
|
||||
@@ -31,6 +33,19 @@ import browser from 'webextension-polyfill';
|
||||
deleteCacheByTabId(tabId);
|
||||
});
|
||||
|
||||
const { defaultPluginsInstalled } = await getAppState();
|
||||
|
||||
if (!defaultPluginsInstalled) {
|
||||
try {
|
||||
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
|
||||
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
|
||||
await installPlugin(twitterProfileUrl);
|
||||
await installPlugin(discordDmUrl);
|
||||
} finally {
|
||||
await setDefaultPluginsInstalled(true);
|
||||
}
|
||||
}
|
||||
|
||||
const { initRPC } = await import('./rpc');
|
||||
await createOffscreenDocument();
|
||||
initRPC();
|
||||
|
||||
29
src/entries/Background/plugins/utils.ts
Normal file
29
src/entries/Background/plugins/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
|
||||
import { getPluginConfig } from '../../../utils/misc';
|
||||
|
||||
export async function installPlugin(
|
||||
urlOrBuffer: ArrayBuffer | string,
|
||||
origin = '',
|
||||
filePath = '',
|
||||
metadata: {[key: string]: string} = {},
|
||||
) {
|
||||
let arrayBuffer;
|
||||
|
||||
if (typeof urlOrBuffer === 'string') {
|
||||
const resp = await fetch(urlOrBuffer);
|
||||
arrayBuffer = await resp.arrayBuffer();
|
||||
} else {
|
||||
arrayBuffer = urlOrBuffer;
|
||||
}
|
||||
|
||||
const config = await getPluginConfig(arrayBuffer);
|
||||
const hex = Buffer.from(arrayBuffer).toString('hex');
|
||||
const hash = await addPlugin(hex);
|
||||
await addPluginConfig(hash!, config);
|
||||
await addPluginMetadata(hash!, {
|
||||
...metadata,
|
||||
origin,
|
||||
filePath,
|
||||
});
|
||||
return hash;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { clearCache, getCacheByTabId } from './cache';
|
||||
import { addRequestHistory } from '../../reducers/history';
|
||||
import { addRequestHistory, setRequests } from '../../reducers/history';
|
||||
import {
|
||||
addNotaryRequest,
|
||||
addNotaryRequestProofs,
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
getPlugins,
|
||||
getCookiesByHost,
|
||||
getHeadersByHost,
|
||||
getAppState,
|
||||
setDefaultPluginsInstalled,
|
||||
} from './db';
|
||||
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
|
||||
import {
|
||||
@@ -85,6 +87,8 @@ export enum BackgroundActiontype {
|
||||
run_plugin_request = 'run_plugin_request',
|
||||
run_plugin_response = 'run_plugin_response',
|
||||
get_logging_level = 'get_logging_level',
|
||||
get_app_state = 'get_app_state',
|
||||
set_default_plugins_installed = 'set_default_plugins_installed',
|
||||
}
|
||||
|
||||
export type BackgroundAction = {
|
||||
@@ -115,7 +119,6 @@ export type RequestHistory = {
|
||||
method: string;
|
||||
headers: { [key: string]: string };
|
||||
body?: string;
|
||||
maxTranscriptSize: number;
|
||||
maxSentData: number;
|
||||
maxRecvData: number;
|
||||
notaryUrl: string;
|
||||
@@ -192,6 +195,12 @@ export const initRPC = () => {
|
||||
case BackgroundActiontype.get_logging_level:
|
||||
getLoggingFilter().then(sendResponse);
|
||||
return true;
|
||||
case BackgroundActiontype.get_app_state:
|
||||
getAppState().then(sendResponse);
|
||||
return true;
|
||||
case BackgroundActiontype.set_default_plugins_installed:
|
||||
setDefaultPluginsInstalled(request.data).then(sendResponse);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -215,15 +224,13 @@ function handleGetProveRequests(
|
||||
sendResponse: (data?: any) => void,
|
||||
): boolean {
|
||||
getNotaryRequests().then(async (reqs) => {
|
||||
for (const req of reqs) {
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(req),
|
||||
});
|
||||
}
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: setRequests(reqs),
|
||||
});
|
||||
sendResponse(reqs);
|
||||
});
|
||||
|
||||
@@ -318,7 +325,6 @@ async function handleProveRequestStart(
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
@@ -334,7 +340,6 @@ async function handleProveRequestStart(
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
@@ -359,7 +364,6 @@ async function handleProveRequestStart(
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
@@ -389,14 +393,12 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
|
||||
const maxSentData = _maxSentData || (await getMaxSent());
|
||||
const maxRecvData = _maxRecvData || (await getMaxRecv());
|
||||
const maxTranscriptSize = 16384;
|
||||
|
||||
const { id } = await addNotaryRequest(now, {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
@@ -423,7 +425,6 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
@@ -798,7 +799,6 @@ async function handleNotarizeRequest(request: BackgroundAction) {
|
||||
body,
|
||||
maxSentData = await getMaxSent(),
|
||||
maxRecvData = await getMaxRecv(),
|
||||
maxTranscriptSize,
|
||||
notaryUrl = await getNotaryApi(),
|
||||
websocketProxyUrl = await getProxyApi(),
|
||||
origin,
|
||||
@@ -813,7 +813,6 @@ async function handleNotarizeRequest(request: BackgroundAction) {
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
@@ -856,7 +855,6 @@ async function handleNotarizeRequest(request: BackgroundAction) {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ContentScriptTypes, RPCClient } from './rpc';
|
||||
import { RequestHistory } from '../Background/rpc';
|
||||
import { PluginConfig, PluginMetadata } from '../../utils/misc';
|
||||
import { Proof } from '../../utils/types';
|
||||
import { PresentationJSON } from '../../utils/types';
|
||||
|
||||
const client = new RPCClient();
|
||||
|
||||
@@ -27,7 +27,7 @@ class TLSN {
|
||||
return resp || [];
|
||||
}
|
||||
|
||||
async getProof(id: string): Promise<Proof | null> {
|
||||
async getProof(id: string): Promise<PresentationJSON | null> {
|
||||
const resp = await client.call(ContentScriptTypes.get_proof, {
|
||||
id,
|
||||
});
|
||||
@@ -47,12 +47,11 @@ class TLSN {
|
||||
websocketProxyUrl?: string;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
maxTranscriptSize?: number;
|
||||
metadata?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
},
|
||||
): Promise<Proof> {
|
||||
): Promise<PresentationJSON> {
|
||||
const resp = await client.call(ContentScriptTypes.notarize, {
|
||||
url,
|
||||
method: requestOptions?.method,
|
||||
@@ -60,7 +59,6 @@ class TLSN {
|
||||
body: requestOptions?.body,
|
||||
maxSentData: proofOptions?.maxSentData,
|
||||
maxRecvData: proofOptions?.maxRecvData,
|
||||
maxTranscriptSize: proofOptions?.maxTranscriptSize,
|
||||
notaryUrl: proofOptions?.notaryUrl,
|
||||
websocketProxyUrl: proofOptions?.websocketProxyUrl,
|
||||
metadata: proofOptions?.metadata,
|
||||
|
||||
@@ -84,7 +84,6 @@ import { urlify } from '../../utils/misc';
|
||||
websocketProxyUrl?: string;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
maxTranscriptSize?: number;
|
||||
}>,
|
||||
) => {
|
||||
const {
|
||||
@@ -94,7 +93,6 @@ import { urlify } from '../../utils/misc';
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
@@ -112,7 +110,6 @@ import { urlify } from '../../utils/misc';
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
|
||||
@@ -3,19 +3,20 @@ import * as Comlink from 'comlink';
|
||||
import { OffscreenActionTypes } from './types';
|
||||
import {
|
||||
NotaryServer,
|
||||
Prover as _Prover,
|
||||
NotarizedSession as _NotarizedSession,
|
||||
TlsProof as _TlsProof,
|
||||
Prover as TProver,
|
||||
Presentation as TPresentation,
|
||||
Transcript,
|
||||
} from 'tlsn-js';
|
||||
import { verify } from 'tlsn-jsV5.3';
|
||||
import { verify } from 'tlsn-js-v5';
|
||||
|
||||
import { urlify } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Proof, ProofV1 } from '../../utils/types';
|
||||
import { PresentationJSON } from '../../utils/types';
|
||||
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
|
||||
import { Method } from 'tlsn-js/wasm/pkg';
|
||||
|
||||
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
|
||||
const { init, Prover, Presentation }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
@@ -111,7 +112,7 @@ const Offscreen = () => {
|
||||
}
|
||||
case BackgroundActiontype.verify_prove_request: {
|
||||
(async () => {
|
||||
const proof: Proof = request.data.proof;
|
||||
const proof: PresentationJSON = request.data.proof;
|
||||
const result: { sent: string; recv: string } =
|
||||
await verifyProof(proof);
|
||||
|
||||
@@ -194,7 +195,7 @@ async function createProof(options: {
|
||||
id: string;
|
||||
secretHeaders: string[];
|
||||
secretResps: string[];
|
||||
}): Promise<ProofV1> {
|
||||
}): Promise<PresentationJSONa7> {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
@@ -211,7 +212,7 @@ async function createProof(options: {
|
||||
|
||||
const hostname = urlify(url)?.hostname || '';
|
||||
const notary = NotaryServer.from(notaryUrl);
|
||||
const prover: _Prover = await new Prover({
|
||||
const prover: TProver = await new Prover({
|
||||
id,
|
||||
serverDns: hostname,
|
||||
maxSentData,
|
||||
@@ -260,24 +261,21 @@ async function createProof(options: {
|
||||
),
|
||||
};
|
||||
|
||||
const session: _NotarizedSession = await new NotarizedSession(
|
||||
await prover.notarize(commit),
|
||||
);
|
||||
const notarizationOutputs = await prover.notarize(commit);
|
||||
|
||||
const proofHex = await session.proof(commit);
|
||||
const proof: ProofV1 = {
|
||||
version: '1.0',
|
||||
meta: {
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
data: proofHex,
|
||||
};
|
||||
return proof;
|
||||
const presentation = (await new Presentation({
|
||||
attestationHex: notarizationOutputs.attestation,
|
||||
secretsHex: notarizationOutputs.secrets,
|
||||
notaryUrl: notarizationOutputs.notaryUrl,
|
||||
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
|
||||
reveal: commit,
|
||||
})) as TPresentation;
|
||||
const presentationJSON = await presentation.json();
|
||||
return presentationJSON;
|
||||
}
|
||||
|
||||
async function verifyProof(
|
||||
proof: Proof,
|
||||
proof: PresentationJSON,
|
||||
): Promise<{ sent: string; recv: string }> {
|
||||
let result: { sent: string; recv: string };
|
||||
|
||||
@@ -286,12 +284,17 @@ async function verifyProof(
|
||||
result = await verify(proof);
|
||||
break;
|
||||
}
|
||||
case '1.0': {
|
||||
const tlsProof: _TlsProof = await new TlsProof(proof.data);
|
||||
result = await tlsProof.verify({
|
||||
typ: 'P256',
|
||||
key: await NotaryServer.from(proof.meta.notaryUrl).publicKey(),
|
||||
case '0.1.0-alpha.7': {
|
||||
const presentation: TPresentation = await new Presentation(proof.data);
|
||||
const verifierOutput = await presentation.verify();
|
||||
const transcript = new Transcript({
|
||||
sent: verifierOutput.transcript.sent,
|
||||
recv: verifierOutput.transcript.recv,
|
||||
});
|
||||
result = {
|
||||
sent: transcript.sent(),
|
||||
recv: transcript.recv(),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, NotarizedSession, TlsProof } from 'tlsn-js';
|
||||
import init, { Prover, Presentation } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
NotarizedSession,
|
||||
TlsProof,
|
||||
Presentation,
|
||||
});
|
||||
|
||||
@@ -33,6 +33,8 @@ import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { getConnection } from '../Background/db';
|
||||
import { useIsConnected, setConnection } from '../../reducers/requests';
|
||||
import { MenuIcon } from '../../components/Menu';
|
||||
import Plugins from '../../pages/Plugins';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -84,7 +86,7 @@ const Popup = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
||||
<div className="flex flex-col w-full h-full overflow-hidden lg:w-[600px] lg:h-[800px] lg:border lg:m-auto lg:mt-40 lg:bg-white lg:shadow">
|
||||
<div className="flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
|
||||
<img
|
||||
className="absolute left-2 h-5 cursor-pointer"
|
||||
@@ -92,17 +94,21 @@ const Popup = () => {
|
||||
alt="logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
<AppConnectionLogo />
|
||||
<div className="flex flex-row flex-grow items-center justify-end gap-4">
|
||||
<AppConnectionLogo />
|
||||
<MenuIcon />
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/requests/:requestId/*" element={<Request />} />
|
||||
<Route path="/notary/:requestId" element={<Notarize />} />
|
||||
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
|
||||
<Route path="/verify" element={<ProofUploader />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/requests" element={<Requests />} />
|
||||
<Route path="/history" element={<Home tab="history" />} />
|
||||
<Route path="/requests" element={<Home tab="network" />} />
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/plugins" element={<Plugins />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/plugininfo" element={<PluginUploadInfo />} />
|
||||
<Route path="/connection-approval" element={<ConnectionApproval />} />
|
||||
@@ -141,7 +147,7 @@ function AppConnectionLogo() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
|
||||
className="flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
|
||||
onClick={() => setShowConnectionDetails(true)}
|
||||
>
|
||||
<div className="flex flex-row relative bg-black border-[1px] border-black rounded-full">
|
||||
|
||||
@@ -30,6 +30,10 @@ code {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@apply bg-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -235,7 +235,7 @@ function StepContent(
|
||||
)}
|
||||
onClick={viewProofInPopup}
|
||||
>
|
||||
<span className="text-sm">View Proof</span>
|
||||
<span className="text-sm">View</span>
|
||||
</button>
|
||||
);
|
||||
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
|
||||
|
||||
@@ -28,12 +28,13 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js"],
|
||||
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "discord_dm.wasm", "twitter_profile.wasm"],
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
|
||||
}
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"permissions": [
|
||||
"contextMenus".
|
||||
"offscreen",
|
||||
"storage",
|
||||
"webRequest",
|
||||
|
||||
@@ -25,9 +25,11 @@ export default function History(): ReactElement {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap overflow-y-auto">
|
||||
{history.map((id) => {
|
||||
return <OneRequestHistory key={id} requestId={id} />;
|
||||
})}
|
||||
{history
|
||||
.map((id) => {
|
||||
return <OneRequestHistory key={id} requestId={id} />;
|
||||
})
|
||||
.reverse()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -160,7 +162,7 @@ export function OneRequestHistory(props: {
|
||||
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
|
||||
onClick={onView}
|
||||
fa="fa-solid fa-receipt"
|
||||
ctaText="View Proof"
|
||||
ctaText="View"
|
||||
hidden={hideActions.includes('view')}
|
||||
/>
|
||||
<ActionButton
|
||||
|
||||
5
src/pages/Home/index.scss
Normal file
5
src/pages/Home/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
#home {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -2,78 +2,284 @@ import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
import { PluginList } from '../../components/PluginList';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
import { ErrorModal } from '../../components/ErrorModal';
|
||||
import History from '../History';
|
||||
import './index.scss';
|
||||
import Requests from '../Requests';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
import {
|
||||
useOnPluginClick,
|
||||
usePluginConfig,
|
||||
usePluginHashes,
|
||||
} from '../../reducers/plugins';
|
||||
import { PluginConfig } from '../../utils/misc';
|
||||
import { getPluginConfigByHash } from '../../entries/Background/db';
|
||||
import { fetchPluginHashes } from '../../utils/rpc';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
const requests = useRequests();
|
||||
const navigate = useNavigate();
|
||||
export default function Home(props: {
|
||||
tab?: 'history' | 'network';
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
|
||||
const scrollableContent = useRef<HTMLDivElement | null>(null);
|
||||
const [shouldFix, setFix] = useState(false);
|
||||
const [actionPanelElement, setActionPanelElement] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPluginHashes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollableContent.current;
|
||||
if (!element) return;
|
||||
if (!actionPanelElement) return;
|
||||
|
||||
let timer = Date.now();
|
||||
const onScroll = () => {
|
||||
const now = Date.now();
|
||||
if (now - timer > 20) {
|
||||
timer = now;
|
||||
setScrollTop(element.scrollTop);
|
||||
if (element.scrollTop >= actionPanelElement.clientHeight) {
|
||||
setFix(true);
|
||||
} else {
|
||||
setFix(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('scroll', onScroll);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, [scrollableContent, actionPanelElement]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
|
||||
<div
|
||||
id="home"
|
||||
ref={scrollableContent}
|
||||
className="flex flex-col flex-grow overflow-y-auto"
|
||||
>
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
|
||||
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
|
||||
<span>Requests</span>
|
||||
<span>{`(${requests.length})`}</span>
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
|
||||
Custom
|
||||
</NavButton>
|
||||
<NavButton
|
||||
fa="fa-solid fa-certificate"
|
||||
onClick={() => navigate('/verify')}
|
||||
<ActionPanel
|
||||
setActionPanelElement={setActionPanelElement}
|
||||
scrollTop={scrollTop}
|
||||
/>
|
||||
<div
|
||||
className={classNames('flex flex-row justify-center items-center', {
|
||||
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40': shouldFix,
|
||||
})}
|
||||
>
|
||||
<TabSelector
|
||||
onClick={() => setTab('network')}
|
||||
selected={tab === 'network'}
|
||||
>
|
||||
Network
|
||||
</TabSelector>
|
||||
<TabSelector
|
||||
onClick={() => setTab('history')}
|
||||
selected={tab === 'history'}
|
||||
>
|
||||
Verify
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
|
||||
History
|
||||
</NavButton>
|
||||
<NavButton className="relative" fa="fa-solid fa-plus">
|
||||
<PluginUploadInfo />
|
||||
Add a plugin
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
|
||||
Options
|
||||
</NavButton>
|
||||
</TabSelector>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{tab === 'history' && <History />}
|
||||
{tab === 'network' && <Requests shouldFix={shouldFix} />}
|
||||
</div>
|
||||
<PluginList className="mx-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionPanel({
|
||||
setActionPanelElement,
|
||||
scrollTop,
|
||||
}: {
|
||||
scrollTop: number;
|
||||
setActionPanelElement: (el: HTMLDivElement) => void;
|
||||
}) {
|
||||
const pluginHashes = usePluginHashes();
|
||||
const navigate = useNavigate();
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const [isOverflow, setOverflow] = useState(false);
|
||||
const [expanded, setExpand] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = container.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
setActionPanelElement(element);
|
||||
|
||||
const onCheckSize = () => {
|
||||
if (element.scrollWidth > element.clientWidth) {
|
||||
setOverflow(true);
|
||||
} else {
|
||||
setOverflow(false);
|
||||
}
|
||||
};
|
||||
|
||||
onCheckSize();
|
||||
|
||||
window.addEventListener('resize', onCheckSize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onCheckSize);
|
||||
};
|
||||
}, [container, pluginHashes]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = container.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
if (scrollTop >= element.clientHeight) {
|
||||
setExpand(false);
|
||||
}
|
||||
}, [container, scrollTop]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
className={classNames(
|
||||
'flex flex-row justify-start items-center gap-4 p-4 border-b relative',
|
||||
{
|
||||
'flex-wrap': expanded,
|
||||
'flex-nowrap': !expanded,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NavButton
|
||||
fa="fa-solid fa-hammer"
|
||||
onClick={() => navigate('/custom')}
|
||||
title="Build a custom request"
|
||||
>
|
||||
Custom
|
||||
</NavButton>
|
||||
<NavButton
|
||||
fa="fa-solid fa-certificate"
|
||||
onClick={() => navigate('/verify')}
|
||||
title="Visualize an attestation"
|
||||
>
|
||||
Verify
|
||||
</NavButton>
|
||||
{pluginHashes.map((hash) => (
|
||||
<PluginIcon hash={hash} />
|
||||
))}
|
||||
<button
|
||||
className={
|
||||
'flex flex-row items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
|
||||
}
|
||||
title="Install a plugin"
|
||||
>
|
||||
<PluginUploadInfo />
|
||||
<Icon fa="fa-solid fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute right-0 top-0 w-6 h-full bg-slate-100 hover:bg-slate-200 font-semibold',
|
||||
'flex flex-row items-center justify-center gap-2 text-slate-500 hover:text-slate-700',
|
||||
{
|
||||
hidden: !isOverflow || expanded,
|
||||
},
|
||||
)}
|
||||
onClick={() => setExpand(true)}
|
||||
>
|
||||
<Icon fa="fa-solid fa-caret-down" size={0.875} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginIcon({ hash }: { hash: string }) {
|
||||
const config = usePluginConfig(hash);
|
||||
const onPluginClick = useOnPluginClick(hash);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!config) return;
|
||||
onPluginClick();
|
||||
}, [onPluginClick, config]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center justify-center',
|
||||
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
className="rounded-full flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
|
||||
url={config?.icon || DefaultPluginIcon}
|
||||
size={2}
|
||||
/>
|
||||
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
|
||||
{config?.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TabSelector(props: {
|
||||
children: string;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
onClick: MouseEventHandler;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
className={classNames(
|
||||
'flex flex-grow items-center justify-center p-2 font-semibold hover:text-slate-700 border-b-2 ',
|
||||
{
|
||||
'font-semibold text-slate-400 border-white': !props.selected,
|
||||
'font-bold text-primary border-primary': props.selected,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function NavButton(props: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
onClick?: MouseEventHandler;
|
||||
className?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center justify-center',
|
||||
'text-white rounded px-2 py-1 gap-1',
|
||||
{
|
||||
'bg-primary/[.8] hover:bg-primary/[.7] active:bg-primary':
|
||||
!props.disabled,
|
||||
'bg-primary/[.5]': props.disabled,
|
||||
},
|
||||
'flex flex-col flex-nowrap items-center justify-center',
|
||||
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
title={props.title}
|
||||
>
|
||||
<Icon className="flex-grow-0 flex-shrink-0" fa={props.fa} size={1} />
|
||||
<span className="flex-grow flex-shrink w-0 flex-grow font-bold">
|
||||
<Icon
|
||||
className="w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
|
||||
fa={props.fa}
|
||||
size={0.875}
|
||||
/>
|
||||
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
|
||||
{props.children}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
ReactEventHandler,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { notarizeRequest, useRequest } from '../../reducers/requests';
|
||||
@@ -19,8 +20,6 @@ import {
|
||||
} from '../../utils/storage';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const maxTranscriptSize = 16384;
|
||||
|
||||
export default function Notarize(): ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const req = useRequest(params.requestId);
|
||||
@@ -58,7 +57,6 @@ export default function Notarize(): ReactElement {
|
||||
body: req.requestBody,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
@@ -281,34 +279,79 @@ export function RevealHeaderTable(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function HideResponseStep(props: {
|
||||
export function HideResponseStep(props: {
|
||||
onNext: () => void;
|
||||
onCancel: () => void;
|
||||
setSecretResps: (secrets: string[]) => void;
|
||||
}): ReactElement {
|
||||
}): React.ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const req = useRequest(params.requestId);
|
||||
const [responseText, setResponseText] = useState('');
|
||||
const [start, setStart] = useState(0);
|
||||
const [end, setEnd] = useState(0);
|
||||
const [redactedRanges, setRedactedRanges] = useState<
|
||||
{ start: number; end: number }[]
|
||||
>([]);
|
||||
const [isRedactMode, setIsRedactMode] = useState(true);
|
||||
const taRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(e) => {
|
||||
const ta = e.currentTarget;
|
||||
if (ta.selectionEnd > ta.selectionStart) {
|
||||
setStart(ta.selectionStart);
|
||||
setEnd(ta.selectionEnd);
|
||||
props.setSecretResps(
|
||||
[
|
||||
responseText.substring(0, ta.selectionStart),
|
||||
responseText.substring(ta.selectionEnd, responseText.length),
|
||||
].filter((d) => !!d),
|
||||
);
|
||||
const onSelectionChange: React.MouseEventHandler<HTMLTextAreaElement> =
|
||||
useCallback(
|
||||
(e) => {
|
||||
const ta = e.currentTarget;
|
||||
if (isRedactMode && ta.selectionEnd > ta.selectionStart) {
|
||||
const newRange: { start: number; end: number } = {
|
||||
start: ta.selectionStart,
|
||||
end: ta.selectionEnd,
|
||||
};
|
||||
|
||||
setRedactedRanges((prevRanges) => {
|
||||
let updatedRanges = [...prevRanges, newRange].sort(
|
||||
(a, b) => a.start - b.start,
|
||||
);
|
||||
updatedRanges = mergeRanges(updatedRanges);
|
||||
|
||||
const secretResps = updatedRanges
|
||||
.map(({ start, end }) => responseText.substring(start, end))
|
||||
.filter((d) => !!d);
|
||||
props.setSecretResps(secretResps);
|
||||
|
||||
return updatedRanges;
|
||||
});
|
||||
} else if (!isRedactMode) {
|
||||
const clickPosition = ta.selectionStart;
|
||||
setRedactedRanges((prevRanges) => {
|
||||
const updatedRanges = prevRanges.filter(
|
||||
({ start, end }) => clickPosition < start || clickPosition > end,
|
||||
);
|
||||
|
||||
const secretResps = updatedRanges
|
||||
.map(({ start, end }) => responseText.substring(start, end))
|
||||
.filter((d) => !!d);
|
||||
props.setSecretResps(secretResps);
|
||||
|
||||
return updatedRanges;
|
||||
});
|
||||
}
|
||||
},
|
||||
[responseText, props, isRedactMode],
|
||||
);
|
||||
|
||||
const mergeRanges = (
|
||||
ranges: { start: number; end: number }[],
|
||||
): { start: number; end: number }[] => {
|
||||
if (ranges.length === 0) return [];
|
||||
const mergedRanges: { start: number; end: number }[] = [ranges[0]];
|
||||
|
||||
for (let i = 1; i < ranges.length; i++) {
|
||||
const lastRange = mergedRanges[mergedRanges.length - 1];
|
||||
if (ranges[i].start <= lastRange.end) {
|
||||
lastRange.end = Math.max(lastRange.end, ranges[i].end);
|
||||
} else {
|
||||
mergedRanges.push(ranges[i]);
|
||||
}
|
||||
},
|
||||
[responseText],
|
||||
);
|
||||
}
|
||||
|
||||
return mergedRanges;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!req) return;
|
||||
@@ -344,37 +387,46 @@ function HideResponseStep(props: {
|
||||
|
||||
if (current) {
|
||||
current.focus();
|
||||
current.setSelectionRange(start, end);
|
||||
}
|
||||
}, [taRef, start, end]);
|
||||
}, [taRef]);
|
||||
|
||||
if (!req) return <></>;
|
||||
|
||||
let shieldedText = '';
|
||||
|
||||
if (end > start) {
|
||||
shieldedText = Array(start)
|
||||
.fill('*')
|
||||
.join('')
|
||||
.concat(responseText.substring(start, end))
|
||||
.concat(
|
||||
Array(responseText.length - end)
|
||||
.fill('*')
|
||||
.join(''),
|
||||
);
|
||||
}
|
||||
const shieldedText = responseText.split('');
|
||||
redactedRanges.forEach(({ start, end }) => {
|
||||
for (let i = start; i < end; i++) {
|
||||
shieldedText[i] = '*';
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
|
||||
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
|
||||
Step 2 of 2: Highlight text to show only selected text from response
|
||||
Step 2 of 2:{' '}
|
||||
{isRedactMode
|
||||
? 'Highlight text to redact selected portions'
|
||||
: 'Click redacted text to unredact'}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-0.5 gap-2 border-t">
|
||||
<button
|
||||
className={`bg-${isRedactMode ? 'red-500' : 'green-500'} text-white font-bold hover:bg-${isRedactMode ? 'red-400' : 'green-400'} px-2 py-0.5 active:bg-${isRedactMode ? 'red-600' : 'green-600'}`}
|
||||
onClick={() => setIsRedactMode(!isRedactMode)}
|
||||
>
|
||||
{isRedactMode ? 'Unredact Text' : 'Redact Text'}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-500 text-white font-bold hover:bg-gray-400 px-2 py-0.5 active:bg-gray-600"
|
||||
onClick={() => setRedactedRanges([])}
|
||||
>
|
||||
Unredact All
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
|
||||
<textarea
|
||||
ref={taRef}
|
||||
className="flex-grow textarea bg-slate-100 font-mono"
|
||||
value={shieldedText || responseText}
|
||||
onSelect={onSelectionChange}
|
||||
value={shieldedText.join('')}
|
||||
onMouseUp={onSelectionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-2 gap-2 border-t">
|
||||
|
||||
@@ -80,6 +80,10 @@ export default function Options(): ReactElement {
|
||||
setAdvanced(!advanced);
|
||||
}, [advanced]);
|
||||
|
||||
const openInTab = useCallback((url: string) => {
|
||||
browser.tabs.create({ url });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
{showReloadModal && (
|
||||
@@ -152,6 +156,22 @@ export default function Options(): ReactElement {
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col w-full items-end gap-2 p-2">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() =>
|
||||
openInTab('https://github.com/tlsnotary/tlsn-extension/issues/new')
|
||||
}
|
||||
>
|
||||
File an issue
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => openInTab('https://discord.gg/9XwESXtcN7')}
|
||||
>
|
||||
Join our Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/pages/Plugins/index.tsx
Normal file
10
src/pages/Plugins/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import { PluginList } from "../../components/PluginList";
|
||||
|
||||
export default function Plugins(): ReactElement {
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<PluginList className="p-2 overflow-y-auto" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
InputBody,
|
||||
FormBodyTable,
|
||||
parseResponse,
|
||||
} from '../../utils/requestbuilder';
|
||||
} from '../../components/RequestBuilder';
|
||||
|
||||
enum TabType {
|
||||
Params = 'Params',
|
||||
@@ -56,15 +56,7 @@ export default function RequestBuilder(props?: {
|
||||
const [method, setMethod] = useState<string>(props?.method || 'GET');
|
||||
const [type, setType] = useState<string>('text/plain');
|
||||
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
|
||||
props?.headers || [
|
||||
['Content-Type', type, true],
|
||||
['Accept', '*/*', false],
|
||||
['Host', '', false],
|
||||
['Connection', 'keep-alive', false],
|
||||
['Accept-Encoding', 'gzip, deflate, br', false],
|
||||
['Accept-Language', 'en-US,en;q=0.9', false],
|
||||
['User-Agent', window.navigator.userAgent, false],
|
||||
],
|
||||
props?.headers || [['Content-Type', type, true]],
|
||||
);
|
||||
|
||||
const [responseData, setResponseData] = useState<{
|
||||
@@ -193,9 +185,6 @@ export default function RequestBuilder(props?: {
|
||||
if (k !== 'Cookie') {
|
||||
map[k] = v;
|
||||
}
|
||||
if (k === 'Host') {
|
||||
map[k] ? map[k] : (map[k] = url?.host || '');
|
||||
}
|
||||
return map;
|
||||
}, {}),
|
||||
body: body ? formatForRequest(body, type) : undefined,
|
||||
@@ -203,7 +192,6 @@ export default function RequestBuilder(props?: {
|
||||
maxRecvData,
|
||||
secretHeaders: [],
|
||||
secretResps: [],
|
||||
maxTranscriptSize: 0,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
@@ -217,7 +205,7 @@ export default function RequestBuilder(props?: {
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === 'GET' || value === 'HEAD') {
|
||||
// setType(''); Leaving this here for now - I feel like I did this for a specific reason but I can't remember why
|
||||
setType('');
|
||||
setMethod(value);
|
||||
} else {
|
||||
setMethod(value);
|
||||
@@ -423,7 +411,7 @@ function HeaderTable(props: {
|
||||
}): ReactElement {
|
||||
const headers: [string, string, boolean?][] = [
|
||||
...props.headers,
|
||||
['', ' ', true],
|
||||
['', '', true],
|
||||
];
|
||||
const last = props.headers.length;
|
||||
|
||||
@@ -461,7 +449,7 @@ function HeaderTable(props: {
|
||||
<input
|
||||
className="input py-1 px-2 w-full py-1 px-2"
|
||||
type="text"
|
||||
value={value ? value : 'Calculated when request is sent'}
|
||||
value={value}
|
||||
placeholder="Value"
|
||||
onChange={(e) => {
|
||||
props.setHeader(i, key, e.target.value);
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { ReactElement } from 'react';
|
||||
import RequestTable from '../../components/RequestTable';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
|
||||
export default function Requests(): ReactElement {
|
||||
export default function Requests(props: { shouldFix?: boolean }): ReactElement {
|
||||
const requests = useRequests();
|
||||
return (
|
||||
<>
|
||||
<RequestTable requests={requests} />
|
||||
<RequestTable shouldFix={props.shouldFix} requests={requests} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import deepEqual from 'fast-deep-equal';
|
||||
|
||||
enum ActionType {
|
||||
'/history/addRequest' = '/history/addRequest',
|
||||
'/history/setRequests' = '/history/setRequests',
|
||||
'/history/deleteRequest' = '/history/deleteRequest',
|
||||
}
|
||||
|
||||
@@ -37,6 +38,13 @@ export const addRequestHistory = (request?: RequestHistory | null) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const setRequests = (requests: RequestHistory[]) => {
|
||||
return {
|
||||
type: ActionType['/history/setRequests'],
|
||||
payload: requests,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteRequestHistory = (id: string) => {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.delete_prove_request,
|
||||
@@ -72,6 +80,18 @@ export default function history(
|
||||
order: newOrder,
|
||||
};
|
||||
}
|
||||
case ActionType['/history/setRequests']: {
|
||||
const payload: RequestHistory[] = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
map: payload.reduce((map: { [id: string]: RequestHistory }, req) => {
|
||||
map[req.id] = req;
|
||||
return map;
|
||||
}, {}),
|
||||
order: payload.map(({ id }) => id),
|
||||
};
|
||||
}
|
||||
case ActionType['/history/deleteRequest']: {
|
||||
const reqId: string = action.payload;
|
||||
const newMap = { ...state.map };
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from './index';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { getPluginConfigByHash } from '../entries/Background/db';
|
||||
import { PluginConfig } from '../utils/misc';
|
||||
import { runPlugin } from '../utils/rpc';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
enum ActionType {
|
||||
'/plugin/addPlugin' = '/plugin/addPlugin',
|
||||
@@ -52,3 +57,31 @@ export const usePluginHashes = (): string[] => {
|
||||
return state.plugins.order;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const usePluginConfig = (hash: string) => {
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setConfig(await getPluginConfigByHash(hash));
|
||||
})();
|
||||
}, [hash]);
|
||||
return config;
|
||||
};
|
||||
|
||||
export const useOnPluginClick = (hash: string) => {
|
||||
return useCallback(async () => {
|
||||
await runPlugin(hash, 'start');
|
||||
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
await browser.storage.local.set({ plugin_hash: hash });
|
||||
|
||||
// @ts-ignore
|
||||
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
|
||||
|
||||
window.close();
|
||||
}, [hash]);
|
||||
};
|
||||
|
||||
@@ -69,7 +69,6 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
maxTranscriptSize: options.maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
secretHeaders: options.secretHeaders,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
|
||||
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.5';
|
||||
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.7';
|
||||
export const NOTARY_PROXY = 'wss://notary.pse.dev/proxy';
|
||||
export const MAX_RECV = 16384;
|
||||
export const MAX_SENT = 4096;
|
||||
|
||||
@@ -284,24 +284,24 @@ export const makePlugin = async (
|
||||
};
|
||||
|
||||
export type StepConfig = {
|
||||
title: string;
|
||||
description?: string;
|
||||
cta: string;
|
||||
action: string;
|
||||
prover?: boolean;
|
||||
title: string; // Text for the step's title
|
||||
description?: string; // Text for the step's description (optional)
|
||||
cta: string; // Text for the step's call-to-action button
|
||||
action: string; // The function name that this step will execute
|
||||
prover?: boolean; // Boolean indicating if this step outputs a notarization (optional)
|
||||
};
|
||||
|
||||
export type PluginConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
steps?: StepConfig[];
|
||||
hostFunctions?: string[];
|
||||
cookies?: string[];
|
||||
headers?: string[];
|
||||
requests: { method: string; url: string }[];
|
||||
notaryUrls?: string[];
|
||||
proxyUrls?: string[];
|
||||
title: string; // The name of the plugin
|
||||
description: string; // A description of the plugin's purpose
|
||||
icon?: string; // A base64-encoded image string representing the plugin's icon (optional)
|
||||
steps?: StepConfig[]; // An array describing the UI steps and behavior (see Step UI below) (optional)
|
||||
hostFunctions?: string[]; // Host functions that the plugin will have access to
|
||||
cookies?: string[]; // Cookies the plugin will have access to, cached by the extension from specified hosts (optional)
|
||||
headers?: string[]; // Headers the plugin will have access to, cached by the extension from specified hosts (optional)
|
||||
requests: { method: string; url: string }[]; // List of requests that the plugin is allowed to make
|
||||
notaryUrls?: string[]; // List of notary services that the plugin is allowed to use (optional)
|
||||
proxyUrls?: string[]; // List of websocket proxies that the plugin is allowed to use (optional)
|
||||
};
|
||||
|
||||
export type PluginMetadata = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LoggingLevel } from 'tlsn-js';
|
||||
import { MAX_RECV, MAX_SENT, NOTARY_API, NOTARY_PROXY } from './constants';
|
||||
|
||||
export const NOTARY_API_LS_KEY = 'notary-api';
|
||||
export const PROXY_API_LS_KEY = 'proxy-api';
|
||||
@@ -18,19 +19,19 @@ export async function get(key: string, defaultValue?: string) {
|
||||
}
|
||||
|
||||
export async function getMaxSent() {
|
||||
return parseInt(await get(MAX_SENT_LS_KEY, '4096'));
|
||||
return parseInt(await get(MAX_SENT_LS_KEY, MAX_SENT.toString()));
|
||||
}
|
||||
|
||||
export async function getMaxRecv() {
|
||||
return parseInt(await get(MAX_RECEIVED_LS_KEY, '16384'));
|
||||
return parseInt(await get(MAX_RECEIVED_LS_KEY, MAX_RECV.toString()));
|
||||
}
|
||||
|
||||
export async function getNotaryApi() {
|
||||
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.6');
|
||||
return await get(NOTARY_API_LS_KEY, NOTARY_API);
|
||||
}
|
||||
|
||||
export async function getProxyApi() {
|
||||
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
|
||||
return await get(PROXY_API_LS_KEY, NOTARY_PROXY);
|
||||
}
|
||||
|
||||
export async function getLoggingFilter(): Promise<LoggingLevel> {
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
export type Proof = ProofV0 | ProofV1;
|
||||
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
|
||||
|
||||
export type ProofV0 = {
|
||||
export type PresentationJSON = PresentationJSONa5 | PresentationJSONa7;
|
||||
|
||||
export type PresentationJSONa5 = {
|
||||
version?: undefined;
|
||||
session: any;
|
||||
substrings: any;
|
||||
notaryUrl: string;
|
||||
};
|
||||
|
||||
export type ProofV1 = {
|
||||
version: '1.0';
|
||||
data: string;
|
||||
meta: {
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
pluginUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,7 +40,8 @@ var options = {
|
||||
mode: process.env.NODE_ENV || "development",
|
||||
ignoreWarnings: [
|
||||
/Circular dependency between chunks with runtime/,
|
||||
/ResizeObserver loop completed with undelivered notifications/
|
||||
/ResizeObserver loop completed with undelivered notifications/,
|
||||
/Should not import the named export/,
|
||||
],
|
||||
|
||||
entry: {
|
||||
@@ -82,6 +83,9 @@ var options = {
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
sassOptions: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -199,31 +203,21 @@ var options = {
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/7.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/250.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/278.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/349.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
{
|
||||
from: "node_modules/tlsn-js/build",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: "src/assets/plugins/discord_dm.wasm",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: "src/assets/plugins/twitter_profile.wasm",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
|
||||
Reference in New Issue
Block a user