Refactor to minimal extension boilerplate

This commit is contained in:
tsukino
2025-08-24 11:47:33 +00:00
parent e497dcae27
commit 92ecb55d6c
75 changed files with 298 additions and 11201 deletions

77
CLAUDE.md Normal file
View File

@@ -0,0 +1,77 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Development
- `npm install` - Install dependencies
- `npm run dev` - Start development server with hot reload
- `npm run build` - Build production extension
- `npm run lint` - Run ESLint
- `npm run lint:fix` - Run ESLint with auto-fix
## Architecture Overview
### Extension Structure
This is a minimal Chrome Extension boilerplate (Manifest V3) with a clean multi-component architecture:
1. **Background Service Worker** (`src/entries/Background/`)
- Persistent background script for extension logic
- Handles extension lifecycle and API interactions
- Central hub for message passing between components
2. **Content Scripts** (`src/entries/Content/`)
- Scripts injected into web pages
- DOM interaction and page manipulation
- Bridge between web pages and extension
3. **Popup** (`src/entries/Popup/`)
- Extension action popup UI
- React-based interface
- Quick access to extension features
4. **Offscreen Document** (`src/entries/Offscreen/`)
- Isolated context for special operations
- Can be used for tasks requiring DOM or other APIs not available in service workers
### Core Dependencies
- **React 18**: UI components for popup and offscreen pages
- **Redux**: State management across extension components
- **TypeScript**: Type safety and better developer experience
- **Webpack 5**: Module bundling and build process
- **PostCSS + Tailwind**: Styling and design system
### Build System
- Webpack 5 with TypeScript compilation
- Separate bundles for each extension component
- Hot reload support for development
- Production builds with optimizations
### Project Structure
```
src/
├── entries/ # Extension entry points
│ ├── Background/ # Service worker
│ ├── Content/ # Content scripts
│ ├── Offscreen/ # Offscreen document
│ └── Popup/ # Popup UI
├── reducers/ # Redux state management
├── utils/ # Shared utilities
└── manifest.json # Extension manifest
```
## Development Workflow
1. **Install dependencies**: `npm install`
2. **Start development**: `npm run dev`
3. **Load extension**: Open Chrome, navigate to chrome://extensions/, enable Developer Mode, and load unpacked from the `build/` directory
4. **Make changes**: Edit code and webpack will auto-rebuild
5. **Build for production**: `npm run build`
## Important Notes
- Manifest V3 compliant with service worker architecture
- Content scripts inject into all HTTP/HTTPS pages by default
- Redux store provides centralized state management
- TypeScript configured for strict type checking

View File

@@ -1,36 +1,21 @@
{
"name": "tlsn-extension",
"version": "0.1.0.1201",
"version": "0.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tlsnotary/tlsn-extension.git"
},
"scripts": {
"clone:tlsn": "bash ./utils/download-tlsn.sh",
"build": "NODE_ENV=production node utils/build.js",
"build:webpack": "NODE_ENV=production webpack --config webpack.config.js",
"websockify": "docker run -it --rm -p 55688:80 -v $(pwd):/app novnc/websockify 80 --target-config /app/websockify_config",
"dev": "NODE_ENV=development node utils/webserver.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^2.0.0-rc11",
"@fortawesome/fontawesome-free": "^6.4.2",
"async-mutex": "^0.4.0",
"buffer": "^6.0.3",
"charwise": "^3.0.1",
"classnames": "^2.3.2",
"comlink": "^4.4.1",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.6.2",
"http-parser-js": "^0.5.9",
"level": "^8.0.0",
"minimatch": "^9.0.4",
"node-cache": "^5.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
@@ -39,8 +24,7 @@
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.12.0"
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.20.12",

View File

@@ -1,70 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {
useActiveTabUrl,
setConnection,
useIsConnected,
} from '../../reducers/requests';
import Modal, { ModalHeader, ModalContent } from '../../components/Modal/Modal';
import { deleteConnection, getConnection } from '../../entries/Background/db';
const ConnectionDetailsModal = (props: {
showConnectionDetails: boolean;
setShowConnectionDetails: (show: boolean) => void;
}) => {
const dispatch = useDispatch();
const activeTabOrigin = useActiveTabUrl();
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (activeTabOrigin) {
const isConnected: boolean | null = await getConnection(
activeTabOrigin.origin,
);
dispatch(setConnection(!!isConnected));
}
})();
}, [activeTabOrigin, dispatch]);
const handleDisconnect = useCallback(async () => {
if (activeTabOrigin?.origin) {
await deleteConnection(activeTabOrigin.origin);
props.setShowConnectionDetails(false);
dispatch(setConnection(false));
}
}, [activeTabOrigin?.origin, dispatch, props]);
return (
<Modal
onClose={() => props.setShowConnectionDetails(false)}
className="flex flex-col gap-2 items-center text-base cursor-default justify-center mx-4 min-h-24"
>
<ModalHeader
className="w-full rounded-t-lg pb-0 border-b-0"
onClose={() => props.setShowConnectionDetails(false)}
>
<span className="text-lg font-semibold">
{activeTabOrigin?.hostname || 'Connections'}
</span>
</ModalHeader>
<ModalContent className="w-full gap-2 flex-grow flex flex-col items-center justify-between px-4 pt-0 pb-4">
<div className="flex flex-row gap-2 items-start w-full text-xs font-semibold text-slate-800">
{connected
? 'TLSN Extension is connected to this site.'
: 'TLSN Extension is not connected to this site. To connect to this site, find and click the connect button.'}
</div>
{connected && (
<button
className="button disabled:opacity-50 self-end"
onClick={handleDisconnect}
>
Disconnect
</button>
)}
</ModalContent>
</Modal>
);
};
export default ConnectionDetailsModal;

View File

@@ -1,26 +0,0 @@
import React, { ReactElement } from 'react';
import Modal, { ModalContent } from '../Modal/Modal';
export function ErrorModal(props: {
onClose: () => void;
message: string;
}): ReactElement {
const { onClose, message } = props;
return (
<Modal
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500 !bg-red-100"
onClose={onClose}
>
<ModalContent className="flex justify-center items-center text-red-500">
{message || 'Something went wrong :('}
</ModalContent>
<button
className="m-0 w-24 bg-red-200 text-red-400 hover:bg-red-200 hover:text-red-500"
onClick={onClose}
>
OK
</button>
</Modal>
);
}

View File

@@ -1,5 +0,0 @@
.icon {
display: flex;
flex-flow: row nowrap;
align-items: center;
}

View File

@@ -1,37 +0,0 @@
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
import classNames from 'classnames';
import './icon.scss';
type Props = {
url?: string;
fa?: string;
className?: string;
size?: number;
onClick?: MouseEventHandler;
children?: ReactNode;
};
export default function Icon(props: Props): ReactElement {
const { url, size = 1, className = '', fa, onClick, children } = props;
return (
<div
className={classNames(
'bg-contain bg-center bg-no-repeat icon',
{
'cursor-pointer': onClick,
},
className,
)}
style={{
backgroundImage: url ? `url(${url})` : undefined,
width: !fa ? `${size}rem` : undefined,
height: !fa ? `${size}rem` : undefined,
}}
onClick={onClick}
>
{!url && !!fa && <i className={fa} style={{ fontSize: `${size}rem` }} />}
{children}
</div>
);
}

View File

@@ -1,114 +0,0 @@
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';
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-hammer"
className="relative"
onClick={() => {
navigate('/custom');
props.setOpen(false);
}}
>
<span>Custom</span>
</MenuRow>
<MenuRow
fa="fa-solid fa-certificate"
className="relative"
onClick={() => {
props.setOpen(false);
navigate('/verify');
}}
>
Verify
</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>
);
}

View File

@@ -1,100 +0,0 @@
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import './modal.scss';
import Icon from '../Icon';
import classNames from 'classnames';
type Props = {
className?: string;
onClose: MouseEventHandler;
children: ReactNode | ReactNode[];
};
export default function Modal(props: Props): ReactElement {
const { className, onClose, children } = props;
const modalRoot = document.querySelector('#modal-root');
if (!modalRoot) return <></>;
return ReactDOM.createPortal(
<div
className={classNames('bg-black bg-opacity-80', 'modal__overlay')}
onClick={(e) => {
e.stopPropagation();
onClose && onClose(e);
}}
>
<div
className={classNames(`modal__wrapper bg-white`, className)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
modalRoot,
);
}
type HeaderProps = {
className?: string;
onClose?: () => void;
children?: ReactNode;
};
export function ModalHeader(props: HeaderProps): ReactElement {
return (
<div
className={classNames(
'border-b modal__header border-gray-100',
props.className,
)}
>
<div className="modal__header__title">{props.children}</div>
<div className="modal__header__content">
{props.onClose && (
<div
className={classNames(
'flex flex-row items-center justify-center',
'p-2 rounded-full opacity-50',
'hover:opacity-100 text-black',
)}
>
<Icon fa="fas fa-times" size={1} onClick={props.onClose} />
</div>
)}
</div>
</div>
);
}
type ContentProps = {
children: ReactNode;
className?: string;
};
export function ModalContent(props: ContentProps): ReactElement {
return (
<div className={classNames('modal__content', props.className)}>
{props.children}
</div>
);
}
type FooterProps = {
children: ReactNode;
className?: string;
};
export function ModalFooter(props: FooterProps): ReactElement {
return (
<div
className={classNames(
'border-t modal__footer border-gray-100 w-full',
props.className,
)}
>
{props.children}
</div>
);
}

View File

@@ -1,72 +0,0 @@
.modal {
display: flex;
flex-flow: column nowrap;
&__overlay {
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 9999;
overflow: auto;
}
&__wrapper {
margin: 3rem auto;
border-radius: 0.5rem;
z-index: 200;
overflow: hidden;
@media only screen and (max-width: 768px) {
width: 100vw !important;
}
}
&__header {
display: flex;
flex-flow: row nowrap;
flex: 0 0 auto;
align-items: center;
padding: 0.5rem 1rem;
&__title {
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__content {
display: flex;
flex-flow: row nowrap;
flex: 1 1 auto;
justify-content: flex-end;
}
}
&__content {
flex: 1 1 auto;
max-height: calc(100vh - 20rem);
overflow-y: auto;
p:nth-of-type(1) {
margin-top: 0;
}
.error-message {
font-size: 0.8125rem;
text-align: center;
margin-top: 1rem;
}
}
&__footer {
display: flex;
flex-flow: row nowrap;
align-content: center;
justify-content: flex-end;
flex: 0 0 auto;
padding: 1rem 1.25rem;
}
}

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { ReactElement } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
export default function NavigateWithParams(props: {
to: string;
}): ReactElement {
const location = useLocation();
return <Navigate to={location.pathname + props.to} />;
}

View File

@@ -1,20 +0,0 @@
.custom-modal {
height: 100%;
max-width: 800px;
max-height: 100vh;
display: flex;
margin: 0 auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,154 +0,0 @@
import React, { Children, MouseEventHandler, ReactNode } from 'react';
import Modal, {
ModalHeader,
ModalContent,
ModalFooter,
} from '../../components/Modal/Modal';
import type { PluginConfig } from '../../utils/misc';
import './index.scss';
import logo from '../../assets/img/icon-128.png';
import {
HostFunctionsDescriptions,
MultipleParts,
PermissionDescription,
} from '../../utils/plugins';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModalContent(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModal(props: {
pluginContent: PluginConfig;
onClose: () => void;
onAddPlugin?: MouseEventHandler;
children?: ReactNode | ReactNode[];
}) {
const { pluginContent, onClose, onAddPlugin, children } = props;
const header = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalHeader',
)[0];
const content = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalContent',
)[0];
return (
<Modal
onClose={onClose}
className="custom-modal !rounded-none flex items-center justify-center gap-4 cursor-default"
>
<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 || DefaultPluginIcon} alt="logo" />
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
</div>
)}
</ModalHeader>
<ModalContent className="flex flex-col flex-grow-0 flex-shrink-0 items-center px-8 py-2 gap-2 w-full max-h-none">
{content || (
<>
<img
className="w-12 h-12"
src={pluginContent.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-center">
<span>
<span className="text-blue-600 font-semibold">
{pluginContent.title}
</span>{' '}
wants access to your browser
</span>
</span>
</>
)}
</ModalContent>
<div className="flex-grow flex-shrink overflow-y-auto w-full px-8">
<PluginPermissions pluginContent={pluginContent} />
</div>
<ModalFooter className="flex justify-end gap-2 p-4">
<button className="button" onClick={onClose}>
Cancel
</button>
{onAddPlugin && (
<button className="button button--primary" onClick={onAddPlugin}>
Allow
</button>
)}
</ModalFooter>
</Modal>
);
}
export function PluginPermissions({
pluginContent,
className,
}: {
pluginContent: PluginConfig;
className?: string;
}) {
return (
<div className={classNames('flex flex-col p-2 gap-5', className)}>
{pluginContent.hostFunctions?.map((hostFunction: string) => {
const HFComponent = HostFunctionsDescriptions[hostFunction];
return <HFComponent key={hostFunction} {...pluginContent} />;
})}
{pluginContent.cookies && (
<PermissionDescription fa="fa-solid fa-cookie-bite">
<span className="cursor-default">
<span className="mr-1">Access cookies from</span>
<MultipleParts parts={pluginContent.cookies} />
</span>
</PermissionDescription>
)}
{pluginContent.headers && (
<PermissionDescription fa="fa-solid fa-envelope">
<span className="cursor-default">
<span className="mr-1">Access headers from</span>
<MultipleParts parts={pluginContent.headers} />
</span>
</PermissionDescription>
)}
{pluginContent.localStorage && (
<PermissionDescription fa="fa-solid fa-database">
<span className="cursor-default">
<span className="mr-1">Access local storage storage from</span>
<MultipleParts parts={pluginContent.localStorage} />
</span>
</PermissionDescription>
)}
{pluginContent.sessionStorage && (
<PermissionDescription fa="fa-solid fa-database">
<span className="cursor-default">
<span className="mr-1">Access session storage from</span>
<MultipleParts parts={pluginContent.sessionStorage} />
</span>
</PermissionDescription>
)}
{pluginContent.requests && (
<PermissionDescription fa="fa-solid fa-globe">
<span className="cursor-default">
<span className="mr-1">Submit network requests to</span>
<MultipleParts
parts={pluginContent?.requests.map(({ url }) => url)}
/>
</span>
</PermissionDescription>
)}
</div>
);
}

View File

@@ -1,45 +0,0 @@
.plugin-box {
&__remove-icon {
opacity: 0;
height: 0;
width: 0;
padding: 0;
overflow: hidden;
transition: 200ms opacity;
transition-delay: 200ms;
}
&:hover {
.plugin-box__remove-icon {
height: 1.25rem;
width: 1.25rem;
padding: .5rem;
opacity: .5;
&:hover {
opacity: 1;
}
}
}
}
.custom-modal {
width: 100vw;
height: 100vh;
max-width: 800px;
max-height: 90vh;
display: flex;
margin: 1rem auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,312 +0,0 @@
import React, {
MouseEventHandler,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import {
fetchPluginHashes,
removePlugin,
runPlugin,
addPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import {
getPluginConfig,
hexToArrayBuffer,
PluginConfig,
} from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import classNames from 'classnames';
import Icon from '../Icon';
import './index.scss';
import browser from 'webextension-polyfill';
import { ErrorModal } from '../ErrorModal';
import {
PluginInfoModal,
PluginInfoModalContent,
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByUrl } from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { openSidePanel } from '../../entries/utils';
export function PluginList({
className,
unremovable,
onClick,
showAddButton = false,
}: {
className?: string;
unremovable?: boolean;
onClick?: (hash: string) => void;
showAddButton?: boolean;
}): ReactElement {
const hashes = usePluginHashes();
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchPluginHashes();
}, []);
const handleFileUpload = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.wasm')) {
alert('Please select a .wasm file');
return;
}
setUploading(true);
try {
const arrayBuffer = await file.arrayBuffer();
const hex = Buffer.from(arrayBuffer).toString('hex');
const url = `file://${file.name}`;
await addPlugin(hex, url);
await fetchPluginHashes();
} catch (error: any) {
alert(`Failed to add plugin: ${error.message}`);
} finally {
setUploading(false);
e.target.value = '';
}
},
[],
);
return (
<div className={classNames('flex flex-col flex-nowrap gap-1', className)}>
{showAddButton && (
<div className="relative">
<input
type="file"
accept=".wasm"
onChange={handleFileUpload}
disabled={uploading}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<button
className="flex flex-row items-center justify-center gap-2 p-3 border-2 border-dashed border-slate-300 rounded-lg text-slate-500 hover:text-slate-700 hover:border-slate-400 transition-colors cursor-pointer w-full"
disabled={uploading}
>
{uploading ? (
<>
<Icon fa="fa-solid fa-spinner" className="animate-spin" />
<span>Adding Plugin...</span>
</>
) : (
<>
<Icon fa="fa-solid fa-plus" />
<span>Add Plugin (.wasm file)</span>
</>
)}
</button>
</div>
)}
{!hashes.length && !showAddButton && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>
)}
{hashes.map((hash) => (
<Plugin
key={hash}
hash={hash}
unremovable={unremovable}
onClick={onClick}
/>
))}
</div>
);
}
export function Plugin({
hash,
hex,
unremovable,
onClick,
className,
}: {
hash: string;
hex?: string;
className?: string;
onClick?: (hash: string) => void;
unremovable?: boolean;
}): ReactElement {
const [error, showError] = useState('');
const [config, setConfig] = useState<PluginConfig | null>(null);
const [pluginInfo, showPluginInfo] = useState(false);
const [remove, showRemove] = useState(false);
const onRunPlugin = useCallback(async () => {
if (!config || remove) return;
if (onClick) {
onClick(hash);
return;
}
try {
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
},
});
await runPlugin(hash, 'start');
window.close();
} catch (e: any) {
showError(e.message);
}
}, [hash, config, remove, onClick]);
useEffect(() => {
(async function () {
if (hex) {
setConfig(await getPluginConfig(hexToArrayBuffer(hex)));
} else {
setConfig(await getPluginConfigByUrl(hash));
}
})();
}, [hash, hex]);
const onRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
removePlugin(hash);
showRemove(false);
},
[hash, remove],
);
const onConfirmRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showRemove(true);
},
[hash, remove],
);
const onPluginInfo: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showPluginInfo(true);
},
[hash, pluginInfo],
);
if (!config) return <></>;
return (
<div
className={classNames(
'flex flex-row justify-center border rounded border-slate-300 p-2 gap-2 plugin-box',
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
className,
)}
onClick={onRunPlugin}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
{!remove ? (
<div className="flex flex-row w-full gap-2">
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
{config.title}
<div className="flex flex-row items-center justify-center">
<Icon
fa="fa-solid fa-circle-info"
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
onClick={onPluginInfo}
/>
{!unremovable && (
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onConfirmRemove}
/>
)}
</div>
</div>
<div>{config.description}</div>
</div>
</div>
) : (
<RemovePlugin
onRemove={onRemove}
showRemove={showRemove}
config={config}
/>
)}
{pluginInfo && (
<PluginInfoModal
pluginContent={config}
onClose={() => showPluginInfo(false)}
>
<PluginInfoModalHeader>
<div className="flex flex-row items-end justify-start gap-2">
<Icon
className="text-slate-500 hover:text-slate-700 cursor-pointer"
size={1}
fa="fa-solid fa-caret-left"
onClick={() => showPluginInfo(false)}
/>
</div>
</PluginInfoModalHeader>
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
<img
className="w-12 h-12 mb-2"
src={config.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-blue-600 font-semibold">
{config.title}
</span>
<div className="text-slate-500 text-lg">{config.description}</div>
</PluginInfoModalContent>
</PluginInfoModal>
)}
</div>
);
}
function RemovePlugin(props: {
onRemove: MouseEventHandler;
showRemove: (show: boolean) => void;
config: PluginConfig;
}): ReactElement {
const { onRemove, showRemove, config } = props;
const onCancel: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
showRemove(false);
}, []);
return (
<div className="flex flex-col items-center w-full gap-1">
<div className="font-bold text-red-700">
{`Are you sure you want to remove "${config.title}" plugin?`}
</div>
<div className="mb-1">Warning: this cannot be undone.</div>
<div className="flex flex-row w-full gap-1">
<button className="flex-grow button p-1" onClick={onCancel}>
Cancel
</button>
<button
className="flex-grow font-bold bg-red-500 hover:bg-red-600 text-white rounded p-1"
onClick={onRemove}
>
Remove
</button>
</div>
</div>
);
}

View File

@@ -1,154 +0,0 @@
import React, { useCallback } from 'react';
import c from 'classnames';
export function InputBody(props: {
body: string;
setBody: (body: string) => void;
}) {
return (
<textarea
className="textarea h-[90%] w-full resize-none"
value={props.body}
onChange={(e) => props.setBody(e.target.value)}
/>
);
}
export function FormBodyTable(props: {
formBody: [string, string, boolean?][];
setFormBody: (formBody: [string, string, boolean?][]) => void;
}) {
const toggleKV = useCallback(
(index: number) => {
const newFormBody = [...props.formBody];
newFormBody[index][2] = !newFormBody[index][2];
props.setFormBody(newFormBody);
},
[props.formBody],
);
const setKV = useCallback(
(index: number, key: string, value: string) => {
const newFormBody = [...props.formBody];
newFormBody[index] = [key, value];
props.setFormBody(newFormBody);
if (index === props.formBody.length - 1 && (key || value)) {
props.setFormBody([...newFormBody, ['', '', true]]);
}
},
[props.formBody],
);
const last = props.formBody.length - 1;
return (
<table className="border border-slate-300 border-collapse table-fixed w-full">
<tbody>
{props.formBody.map(([key, value, silent], i) => (
<tr
key={i}
className={c('border-b border-slate-200', {
'opacity-30': !!silent,
})}
>
<td className="w-8 text-center pt-2">
{last !== i && (
<input
type="checkbox"
onChange={() => toggleKV(i)}
checked={!silent}
/>
)}
</td>
<td className="border border-slate-300 font-bold align-top break-all w-fit">
<input
className="input py-1 px-2 w-full"
type="text"
value={key}
placeholder="Key"
onChange={(e) => {
setKV(i, e.target.value, value);
}}
/>
</td>
<td className="border border-slate-300 break-all align-top">
<input
className="input py-1 px-2 w-full"
type="text"
value={value}
placeholder="Value"
onChange={(e) => {
setKV(i, key, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
);
}
export function formatForRequest(
input: string | [string, string, boolean?][],
type: string,
): string {
try {
let pairs: [string, string][] = [];
if (typeof input === 'string') {
const lines = input.split('\n').filter((line) => line.trim() !== '');
pairs = lines.map((line) => {
const [key, value] = line.split('=').map((part) => part.trim());
return [key, value];
});
} else {
pairs = input
.filter(([, , silent]) => silent !== true)
.map(([key, value]) => [key, value]);
}
if (type === 'text/plain') {
return JSON.stringify(input as string);
}
if (type === 'application/json') {
const jsonObject = JSON.parse(input as string);
return JSON.stringify(jsonObject);
}
if (type === 'application/x-www-form-urlencoded') {
const searchParams = new URLSearchParams();
pairs.forEach(([key, value]) => {
searchParams.append(key, value);
});
return searchParams.toString();
}
return pairs.map(([key, value]) => `${key}=${value}`).join('&');
} catch (e) {
console.error('Error formatting for request:', e);
return '';
}
}
export async function parseResponse(contentType: string, res: Response) {
const parsedResponseData = {
json: '',
text: '',
img: '',
headers: Array.from(res.headers.entries()),
};
if (contentType?.includes('application/json')) {
parsedResponseData.json = await res.json();
} else if (contentType?.includes('text')) {
parsedResponseData.text = await res.text();
} else if (contentType?.includes('image')) {
const blob = await res.blob();
parsedResponseData.img = URL.createObjectURL(blob);
} else {
parsedResponseData.text = await res.text();
}
return parsedResponseData;
}

View File

@@ -1,529 +0,0 @@
import React, {
ReactElement,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import {
Route,
Routes,
useLocation,
useNavigate,
useParams,
} from 'react-router';
import Icon from '../Icon';
import NavigateWithParams from '../NavigateWithParams';
import {
set,
get,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxRecv,
getMaxSent,
getDeveloperMode,
} from '../../utils/storage';
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
type Props = {
requestId: string;
};
export default function RequestDetail(props: Props): ReactElement {
const request = useRequest(props.requestId);
const navigate = useNavigate();
const notarize = useCallback(async () => {
if (!request) return;
navigate('/notary/' + request.requestId);
}, [request, props.requestId]);
if (!request) return <></>;
return (
<>
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
<Icon
className="cursor-point text-slate-400 hover:text-slate-700"
fa="fa-solid fa-xmark"
onClick={() => navigate('/requests')}
/>
<RequestDetailsHeaderTab path="/headers">
Headers
</RequestDetailsHeaderTab>
<RequestDetailsHeaderTab path="/payloads">
Payload
</RequestDetailsHeaderTab>
<RequestDetailsHeaderTab path="/response">
Response
</RequestDetailsHeaderTab>
<RequestDetailsHeaderTab path="/advanced">
Advanced
</RequestDetailsHeaderTab>
<button
className="absolute right-2 bg-primary/[0.9] text-white font-bold px-2 py-0.5 hover:bg-primary/[0.8] active:bg-primary"
onClick={notarize}
>
Notarize
</button>
</div>
<Routes>
<Route
path="headers"
element={<RequestHeaders requestId={props.requestId} />}
/>
<Route
path="payloads"
element={<RequestPayload requestId={props.requestId} />}
/>
<Route
path="response"
element={<WebResponse requestId={props.requestId} />}
/>
<Route path="advanced" element={<AdvancedOptions />} />
<Route path="/" element={<NavigateWithParams to="/headers" />} />
</Routes>
</>
);
}
function RequestDetailsHeaderTab(props: {
children: ReactNode;
path: string;
}): ReactElement {
const loc = useLocation();
const params = useParams<{ requestId: string }>();
const navigate = useNavigate();
const selected = loc.pathname.includes(props.path);
return (
<div
className={classNames('font-bold', {
'text-slate-700 cursor-default': selected,
'text-slate-400 hover:text-slate-500 cursor-pointer': !selected,
})}
onClick={() => navigate('/requests/' + params.requestId + props.path)}
>
{props.children}
</div>
);
}
function AdvancedOptions(): ReactElement {
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxRecv, setMaxRecv] = useState(MAX_RECV);
const [dirty, setDirty] = useState(false);
useEffect(() => {
(async () => {
setMaxRecv((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
})();
}, []);
const onSave = useCallback(async () => {
await set(MAX_RECEIVED_LS_KEY, maxRecv.toString());
await set(MAX_SENT_LS_KEY, maxSent.toString());
setDirty(false);
}, [maxSent, maxRecv]);
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Max Sent Data</div>
<input
type="number"
className="input border"
value={maxSent}
min={0}
onChange={(e) => {
setMaxSent(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="font-semibold">Max Received Data</div>
<input
type="number"
className="input border"
value={maxRecv}
min={0}
onChange={(e) => {
setMaxRecv(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
disabled={!dirty}
onClick={onSave}
>
Save
</button>
</div>
</div>
);
}
function RequestPayload(props: Props): ReactElement {
const data = useRequest(props.requestId);
const [url, setUrl] = useState<URL | null>();
const [json, setJson] = useState<any | null>();
const [formData, setFormData] = useState<URLSearchParams | null>(null);
useEffect(() => {
if (data?.formData) {
const params = new URLSearchParams();
Object.entries(data.formData).forEach(([key, values]) => {
values.forEach((v) => params.append(key, v));
});
setFormData(params);
}
}, [data?.formData]);
useEffect(() => {
try {
setUrl(new URL(data!.url));
} catch (e) {}
try {
if (data?.requestBody) {
setJson(JSON.parse(data.requestBody));
}
} catch (e) {
console.error(e);
setJson(null);
}
}, [data]);
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
<table className="border border-slate-300 border-collapse table-fixed w-full">
{!!url?.searchParams.size && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Query String Parameters
</td>
</tr>
</thead>
<tbody>
{Array.from(url.searchParams).map((param) => {
return (
<tr key={param[0]} className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 break-all">
{param[0]}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2 break-all">
{param[1]}
</td>
</tr>
);
})}
</tbody>
</>
)}
{!!json && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Body Payload
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={10}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={JSON.stringify(json, null, 2)}
></textarea>
</td>
</tr>
</>
)}
{!!formData && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Form Data
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={10}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={formData.toString()}
></textarea>
</td>
</tr>
</>
)}
{!json && !!data?.requestBody && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Body
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={6}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={data?.requestBody}
></textarea>
</td>
</tr>
</>
)}
</table>
</div>
);
}
function WebResponse(props: Props): ReactElement {
const data = useRequest(props.requestId);
const [response, setResponse] = useState<Response | null>(null);
const [json, setJSON] = useState<any | null>(null);
const [text, setText] = useState<string | null>(null);
const [img, setImg] = useState<string | null>(null);
const [formData, setFormData] = useState<URLSearchParams | null>(null);
useEffect(() => {
if (data?.formData) {
const params = new URLSearchParams();
Object.entries(data.formData).forEach(([key, values]) => {
values.forEach((v) => params.append(key, v));
});
setFormData(params);
}
}, [data?.formData]);
const replay = useCallback(async () => {
if (!data) return null;
const options = {
method: data.method,
headers: data.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
}
return acc;
},
{},
),
body: data?.requestBody,
};
if (formData) {
options.body = formData.toString();
}
// @ts-ignore
const resp = await fetch(data.url, options);
setResponse(resp);
const contentType =
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
resp.json().then((json) => {
if (json) {
setJSON(json);
}
});
} else if (contentType?.includes('text')) {
resp.text().then((_text) => {
if (_text) {
setText(_text);
}
});
} else if (contentType?.includes('image')) {
resp.blob().then((blob) => {
if (blob) {
setImg(URL.createObjectURL(blob));
}
});
} else {
resp
.blob()
.then((blob) => blob.text())
.then((_text) => {
if (_text) {
setText(_text);
}
});
}
}, [data, formData]);
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
{!response && (
<div className="p-2">
<button className="button" onClick={replay}>
Fetch Response
</button>
</div>
)}
<table className="border border-slate-300 border-collapse table-fixed w-full">
{!!response?.headers && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Headers
</td>
</tr>
</thead>
<tbody>
{Array.from(response.headers.entries()).map(([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
})}
</tbody>
</>
)}
{!!json && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
JSON
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={JSON.stringify(json, null, 2)}
></textarea>
</td>
</tr>
</>
)}
{!!text && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Text
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={text}
></textarea>
</td>
</tr>
</>
)}
{!!img && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Img
</td>
</tr>
</thead>
<tr>
<td className="bg-slate-100" colSpan={2}>
<img src={img} />
</td>
</tr>
</>
)}
</table>
</div>
);
}
function RequestHeaders(props: Props): ReactElement {
const data = useRequest(props.requestId);
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
<table className="border border-slate-300 border-collapse table-fixed">
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
General
</td>
</tr>
</thead>
<tbody>
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
Method
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{data?.method}
</td>
</tr>
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
Type
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{data?.type}
</td>
</tr>
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
URL
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{data?.url}
</td>
</tr>
</tbody>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Headers
</td>
</tr>
</thead>
<tbody className="">
{data?.requestHeaders?.map((h) => (
<tr key={h.name} className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{h.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -1,121 +0,0 @@
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 {
const { requests } = props;
const navigate = useNavigate();
const dispatch = useDispatch();
const [query, setQuery] = useState('');
const fuse = new Fuse(requests, {
isCaseSensitive: true,
minMatchCharLength: 2,
shouldSort: true,
findAllMatches: true,
threshold: 0.2,
includeMatches: true,
ignoreLocation: true,
keys: [
{ name: 'method', weight: 2 },
{ name: 'type', weight: 2 },
{ name: 'requestHeaders.name', weight: 1 },
{ name: 'requestHeaders.value', weight: 1 },
{ name: 'responseHeaders.name', weight: 1 },
{ name: 'responseHeaders.value', weight: 1 },
{ name: 'url', weight: 1 },
],
});
const result = query ? fuse.search(query) : null;
const list = result ? result.map((r) => r.item) : requests;
const reset = useCallback(async () => {
await chrome.runtime.sendMessage({
type: BackgroundActiontype.clear_requests,
});
dispatch(setRequests([]));
}, [dispatch]);
return (
<div className="flex flex-col flex-nowrap flex-grow">
<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"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
value={query}
></input>
<Icon
className="text-slate-400"
fa="fa-solid fa-trash"
onClick={reset}
/>
</div>
<div className="flex-grow">
<table className="border border-slate-300 border-collapse table-fixed w-full">
<thead className="bg-slate-200">
<tr>
<td className="border border-slate-300 py-1 px-2 w-2/12">
Method
</td>
<td className="border border-slate-300 py-1 px-2 w-3/12">Type</td>
<td className="border border-slate-300 py-1 px-2">Name</td>
</tr>
</thead>
<tbody>
{list.map((r) => {
let url;
try {
url = new URL(r.url);
} catch (e) {}
return (
<tr
key={r.requestId}
onClick={() => navigate('/requests/' + r.requestId)}
className="cursor-pointer hover:bg-slate-100"
>
<td className="border border-slate-200 align-top py-1 px-2 whitespace-nowrap w-2/12">
{r.method}
</td>
<td className="border border-slate-200 align-top py-1 px-2 whitespace-nowrap w-3/12">
{r.type}
</td>
<td className="border border-slate-200 py-1 px-2 break-all truncate">
{url?.pathname}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -1,105 +0,0 @@
import classNames from 'classnames';
import React, { ReactElement } from 'react';
export default function ResponseDetail(props: {
responseData: {
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null;
className?: string;
}): ReactElement {
return (
<div
className={classNames(
'flex flex-col flex-nowrap overflow-y-auto',
props.className,
)}
>
<table className="border border-slate-300 border-collapse table-fixed w-full">
{!!props.responseData?.json && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
JSON
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={JSON.stringify(props.responseData.json, null, 2)}
></textarea>
</td>
</tr>
</>
)}
{!!props.responseData?.text && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Text
</td>
</tr>
</thead>
<tr>
<td colSpan={2}>
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={props.responseData.text}
></textarea>
</td>
</tr>
</>
)}
{!!props.responseData?.img && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Img
</td>
</tr>
</thead>
<tr>
<td className="bg-slate-100" colSpan={2}>
<img src={props.responseData.img} />
</td>
</tr>
</>
)}
{!!props.responseData?.headers && (
<>
<thead className="bg-slate-200">
<tr>
<td colSpan={2} className="border border-slate-300 py-1 px-2">
Headers
</td>
</tr>
</thead>
<tbody>
{props.responseData?.headers.map(([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
})}
</tbody>
</>
)}
</table>
</div>
);
}

View File

@@ -1,635 +0,0 @@
import { Level } from 'level';
import { AbstractSublevel } from 'abstract-level';
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
import {
RequestHistory,
RequestLog,
RequestProgress,
UpsertRequestLog,
} from './rpc';
import mutex from './mutex';
import { minimatch } from 'minimatch';
const charwise = require('charwise');
export const db = new Level('./ext-db', {
valueEncoding: 'json',
});
const historyDb = db.sublevel<string, RequestHistory>('history', {
valueEncoding: 'json',
});
const pluginDb = db.sublevel<string, string>('plugin', {
valueEncoding: 'hex',
});
const pluginConfigDb = db.sublevel<string, PluginConfig>('pluginConfig', {
valueEncoding: 'json',
});
const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
valueEncoding: 'json',
});
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
});
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
valueEncoding: 'json',
});
const sessionStorageDb = db.sublevel<string, any>('localStorage', {
valueEncoding: 'json',
});
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
const requestDb = db.sublevel<string, any>('requests', {
valueEncoding: 'json',
});
export async function upsertRequestLog(request: UpsertRequestLog) {
const existing = await getRequestLog(request.requestId);
if (existing) {
await requestDb.put(request.requestId, {
...existing,
...request,
});
} else if (request.url) {
const host = urlify(request.url)?.host;
if (host) {
await requestDb.put(request.requestId, request);
await requestDb
.sublevel(request.tabId.toString())
.put(request.requestId, '');
await requestDb.sublevel(host).put(request.requestId, '');
}
}
}
export async function getRequestLog(
requestId: string,
): Promise<RequestLog | null> {
return requestDb.get(requestId).catch(() => null);
}
export async function removeRequestLog(requestId: string) {
const existing = await getRequestLog(requestId);
if (existing) {
await requestDb.del(requestId);
await requestDb.sublevel(existing.tabId.toString()).del(requestId);
// Removing requestId for asset url
const host = urlify(existing.url)?.host;
if (host) {
await requestDb.sublevel(host).del(requestId);
}
// Removing requestId for initiator url
if (existing.initiator) {
const host = urlify(existing.initiator)?.host;
if (host) {
await requestDb.sublevel(host).del(requestId);
}
}
}
}
export async function removeRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
for await (const [requestId] of requests.iterator()) {
await removeRequestLog(requestId);
}
}
export async function getRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function getRequestLogsByHost(host: string) {
const requests = requestDb.sublevel(host);
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function clearAllRequestLogs() {
await requestDb.clear();
}
export async function addNotaryRequest(
now = Date.now(),
request: Omit<RequestHistory, 'status' | 'id'>,
): Promise<RequestHistory> {
const id = charwise.encode(now).toString('hex');
const newReq: RequestHistory = {
...request,
id,
status: '',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function addNotaryRequestProofs(
id: string,
proof: { session: any; substrings: any },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
proof,
status: 'success',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestSessionId(
id: string,
sessionId: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = { ...existing, sessionId };
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestStatus(
id: string,
status: '' | 'pending' | 'success' | 'error',
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
status,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestError(
id: string,
error: any,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
error,
status: 'error',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
progress,
errorMessage,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestVerification(
id: string,
verification: {
sent: string;
recv: string;
verifierKey: string;
notaryKey?: string;
},
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
verification,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function removeNotaryRequest(
id: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
await historyDb.del(id);
return existing;
}
export async function getNotaryRequests(): Promise<RequestHistory[]> {
const retVal = [];
for await (const [key, value] of historyDb.iterator()) {
retVal.push(value);
}
return retVal;
}
export async function getNotaryRequest(
id: string,
): Promise<RequestHistory | null> {
return historyDb.get(id).catch(() => null);
}
export async function getPluginHashes(): Promise<string[]> {
const retVal: string[] = [];
for await (const [key] of pluginDb.iterator()) {
retVal.push(key);
}
return retVal;
}
export async function getPluginByUrl(url: string): Promise<string | null> {
try {
const plugin = await pluginDb.get(url);
return plugin;
} catch (e) {
return null;
}
}
export async function addPlugin(
hex: string,
url: string,
): Promise<string | null> {
const hash = await sha256(hex);
if (await getPluginByUrl(url)) {
return url;
}
await pluginDb.put(url, hex);
return hash;
}
export async function removePlugin(url: string): Promise<string | null> {
const existing = await pluginDb.get(url);
if (!existing) return null;
await pluginDb.del(url);
return url;
}
export async function getPluginConfigByUrl(
url: string,
): Promise<PluginConfig | null> {
try {
const config = await pluginConfigDb.get(url);
return config;
} catch (e) {
return null;
}
}
export async function addPluginConfig(
url: string,
config: PluginConfig,
): Promise<PluginConfig | null> {
if (await getPluginConfigByUrl(url)) {
return null;
}
await pluginConfigDb.put(url, config);
return config;
}
export async function removePluginConfig(
url: string,
): Promise<PluginConfig | null> {
const existing = await pluginConfigDb.get(url);
if (!existing) return null;
await pluginConfigDb.del(url);
return existing;
}
export async function getPlugins(): Promise<
(PluginConfig & { hash: string; metadata: PluginMetadata })[]
> {
const hashes = await getPluginHashes();
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
for (const hash of hashes) {
const config = await getPluginConfigByUrl(hash);
const metadata = await getPluginMetadataByUrl(hash);
if (config) {
ret.push({
...config,
hash,
metadata: metadata
? {
...metadata,
hash,
}
: {
filePath: '',
origin: '',
hash,
},
});
}
}
return ret;
}
export async function getPluginMetadataByUrl(
url: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(url);
return metadata;
} catch (e) {
return null;
}
}
export async function addPluginMetadata(
url: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(url, metadata);
return metadata;
}
export async function removePluginMetadata(
url: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(url);
if (!existing) return null;
await pluginMetadataDb.del(url);
return existing;
}
export async function setNotaryRequestCid(
id: string,
cid: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
cid,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setConnection(origin: string) {
if (await getConnection(origin)) return null;
await connectionDb.put(origin, true);
return true;
}
export async function getCookiesByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
}
if (!filteredRequest) return ret;
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() === 'cookie') {
header.value?.split(';').forEach((cookie) => {
const i = cookie.indexOf('=');
if (i !== -1) {
const name = cookie.slice(0, i).trim();
ret[name] = cookie.slice(i + 1).trim();
}
});
}
}
return ret;
}
export async function deleteConnection(origin: string) {
return mutex.runExclusive(async () => {
if (await getConnection(origin)) {
await connectionDb.del(origin);
}
});
}
export async function getConnection(origin: string) {
try {
const existing = await connectionDb.get(origin);
return existing;
} catch (e) {
return null;
}
}
export async function getHeadersByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
}
if (!filteredRequest) return ret;
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() !== 'cookie') {
ret[header.name] = header.value || '';
}
}
return ret;
}
export async function setLocalStorage(
host: string,
name: string,
value: string,
) {
return mutex.runExclusive(async () => {
await localStorageDb.sublevel(host).put(name, value);
return true;
});
}
export async function setSessionStorage(
host: string,
name: string,
value: string,
) {
return mutex.runExclusive(async () => {
await sessionStorageDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearLocalStorage(host: string) {
return mutex.runExclusive(async () => {
await localStorageDb.sublevel(host).clear();
return true;
});
}
export async function clearSessionStorage(host: string) {
return mutex.runExclusive(async () => {
await sessionStorageDb.sublevel(host).clear();
return true;
});
}
export async function getLocalStorageByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of localStorageDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
export async function getSessionStorageByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of sessionStorageDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
export async function resetDB() {
return mutex.runExclusive(async () => {
return Promise.all([
localStorageDb.clear(),
sessionStorageDb.clear(),
requestDb.clear(),
]);
});
}
export async function getDBSizeByRoot(
rootDB: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of rootDB.keys({ keyEncoding: 'utf8' })) {
const link = sublevel.split('!')[1];
const sub = rootDB.sublevel(link);
for await (const [key, value] of sub.iterator()) {
size += key.length + value.length;
}
}
resolve(size);
});
}
export async function getRecursiveDBSize(
db: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of db.keys({ keyEncoding: 'utf8' })) {
const parts = sublevel.split('!');
if (parts.length === 1) {
const value = await db.get(parts[0]);
size += parts[0].length + (value ? JSON.stringify(value).length : 0);
} else {
const sub = db.sublevel(parts[1]);
size +=
(await getRecursiveDBSize(
sub as unknown as AbstractSublevel<Level, any, any, any>,
)) + parts[1].length;
}
}
resolve(size);
});
}
export async function getDBSize(): Promise<number> {
const sizes = await Promise.all([
getDBSizeByRoot(localStorageDb),
getDBSizeByRoot(sessionStorageDb),
getRecursiveDBSize(requestDb),
]);
return sizes.reduce((a, b) => a + b, 0);
}

View File

@@ -1,109 +0,0 @@
import { BackgroundActiontype } from './rpc';
import mutex from './mutex';
import browser from 'webextension-polyfill';
import { addRequest } from '../../reducers/requests';
import { urlify } from '../../utils/misc';
import { getRequestLog, upsertRequestLog } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
) => {
return mutex.runExclusive(async () => {
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
const { origin, pathname } = urlify(details.url) || {};
const link = [origin, pathname].join('');
if (link && details.requestHeaders) {
upsertRequestLog({
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
updatedAt: Date.now(),
});
}
}
});
};
export const onBeforeRequest = (
details: browser.WebRequest.OnBeforeRequestDetailsType,
) => {
mutex.runExclusive(async () => {
const { method, requestBody, tabId, requestId } = details;
if (method === 'OPTIONS') return;
if (requestBody) {
if (requestBody.raw && requestBody.raw[0]?.bytes) {
try {
await upsertRequestLog({
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
'utf-8',
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
});
} catch (e) {
console.error(e);
}
} else if (requestBody.formData) {
await upsertRequestLog({
formData: Object.fromEntries(
Object.entries(requestBody.formData).map(([key, value]) => [
key,
Array.isArray(value) ? value : [value],
]),
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
});
}
}
});
};
export const onResponseStarted = (
details: browser.WebRequest.OnResponseStartedDetailsType,
) => {
mutex.runExclusive(async () => {
const { method, responseHeaders, tabId, requestId } = details;
if (method === 'OPTIONS') return;
await upsertRequestLog({
method: details.method,
type: details.type,
url: details.url,
initiator: details.initiator || null,
tabId: tabId,
requestId: requestId,
responseHeaders,
updatedAt: Date.now(),
});
const newLog = await getRequestLog(requestId);
if (!newLog) {
console.error('Request log not found', requestId);
return;
}
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: details.tabId,
request: newLog,
},
action: addRequest(newLog),
});
});
};

View File

@@ -1,47 +1,37 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import browser from 'webextension-polyfill';
import { removePlugin, removeRequestLogsByTabId } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
browser.webRequest.onSendHeaders.addListener(
onSendHeaders,
{
urls: ['<all_urls>'],
},
['requestHeaders', 'extraHeaders'],
);
// Basic background script setup
console.log('Background script loaded');
browser.webRequest.onBeforeRequest.addListener(
onBeforeRequest,
{
urls: ['<all_urls>'],
},
['requestBody'],
);
// Handle extension install/update
browser.runtime.onInstalled.addListener((details) => {
console.log('Extension installed/updated:', details.reason);
});
browser.webRequest.onResponseStarted.addListener(
onResponseStarted,
{
urls: ['<all_urls>'],
},
['responseHeaders', 'extraHeaders'],
);
// Basic message handler
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('Message received in background:', request);
browser.tabs.onRemoved.addListener((tabId) => {
removeRequestLogsByTabId(tabId);
});
// Example response
if (request.type === 'PING') {
sendResponse({ type: 'PONG' });
}
const { initRPC } = await import('./rpc');
await createOffscreenDocument();
initRPC();
})();
return true; // Keep message channel open for async response
});
let creatingOffscreen: any;
// Create offscreen document if needed (Chrome 109+)
async function createOffscreenDocument() {
// Check if we're in a Chrome environment that supports offscreen documents
if (!chrome?.offscreen) {
console.log('Offscreen API not available');
return;
}
const offscreenUrl = browser.runtime.getURL('offscreen.html');
// @ts-ignore
const existingContexts = await browser.runtime.getContexts({
// Check if offscreen document already exists
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl],
});
@@ -50,15 +40,15 @@ async function createOffscreenDocument() {
return;
}
if (creatingOffscreen) {
await creatingOffscreen;
} else {
creatingOffscreen = (chrome as any).offscreen.createDocument({
url: 'offscreen.html',
reasons: ['WORKERS'],
justification: 'workers for multithreading',
});
await creatingOffscreen;
creatingOffscreen = null;
}
// Create offscreen document
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_SCRAPING' as chrome.offscreen.Reason],
justification: 'Offscreen document for background processing',
});
}
// Initialize offscreen document
createOffscreenDocument().catch(console.error);
export {};

View File

@@ -1,5 +0,0 @@
import { Mutex } from 'async-mutex';
const mutex = new Mutex();
export default mutex;

View File

@@ -1,38 +0,0 @@
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
import { getPluginConfig } from '../../../utils/misc';
export async function installPlugin(
url: string,
origin = '',
filePath = '',
metadata: {[key: string]: string} = {},
) {
const resp = await fetch(url);
const arrayBuffer = await resp.arrayBuffer();
const config = await getPluginConfig(arrayBuffer);
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex, url);
await addPluginConfig(url, config);
await addPluginMetadata(url, {
...metadata,
origin,
filePath,
});
return hash;
}
export function mapSecretsToRange(secrets: string[], text: string) {
return secrets
.map((secret: string) => {
const index = text.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,481 +0,0 @@
import { devlog, safeParseJSON, sha256 } from '../../utils/misc';
import {
appendIncomingPairingRequests,
appendIncomingProofRequests,
appendOutgoingPairingRequests,
appendOutgoingProofRequest,
setClientId,
setConnected,
setIncomingPairingRequest,
setIncomingProofRequest,
setIsProving,
setIsVerifying,
setOutgoingPairingRequest,
setOutgoingProofRequest,
setP2PError,
setP2PPresentation,
setPairing,
} from '../../reducers/p2p';
import { pushToRedux } from '../utils';
import { getPluginByUrl } from './db';
import browser from 'webextension-polyfill';
import { OffscreenActionTypes } from '../Offscreen/types';
import { getMaxRecv, getMaxSent, getRendezvousApi } from '../../utils/storage';
import { SidePanelActionTypes } from '../SidePanel/types';
import { Transcript, VerifierOutput } from 'tlsn-js';
const state: {
clientId: string;
pairing: string;
socket: WebSocket | null;
connected: boolean;
reqId: number;
incomingPairingRequests: string[];
outgoingPairingRequests: string[];
incomingProofRequests: string[];
outgoingProofRequests: string[];
isProving: boolean;
isVerifying: boolean;
presentation: null | { sent: string; recv: string };
} = {
clientId: '',
pairing: '',
socket: null,
connected: false,
reqId: 0,
incomingPairingRequests: [],
outgoingPairingRequests: [],
incomingProofRequests: [],
outgoingProofRequests: [],
isProving: false,
isVerifying: false,
presentation: null,
};
export const getP2PState = async () => {
pushToRedux(setPairing(state.pairing));
pushToRedux(setConnected(state.connected));
pushToRedux(setClientId(state.clientId));
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
pushToRedux(setIncomingProofRequest(state.incomingProofRequests));
pushToRedux(setOutgoingProofRequest(state.outgoingProofRequests));
pushToRedux(setIsProving(state.isProving));
pushToRedux(setIsVerifying(state.isVerifying));
pushToRedux(setP2PPresentation(state.presentation));
};
export const connectSession = async () => {
if (state.socket) return;
const rendezvousAPI = await getRendezvousApi();
const socket = new WebSocket(rendezvousAPI);
socket.onopen = () => {
devlog('Connected to websocket');
state.connected = true;
state.socket = socket;
pushToRedux(setConnected(true));
const heartbeatInterval = setInterval(() => {
if (socket.readyState === 1) {
// Check if connection is open
socket.send(bufferify({ method: 'ping' }));
} else {
disconnectSession();
clearInterval(heartbeatInterval); // Stop heartbeat if connection is closed
}
}, 55000);
};
socket.onmessage = async (event) => {
const message: any = safeParseJSON(await event.data.text());
if (message.error) {
pushToRedux(setP2PError(message.error.message));
return;
}
switch (message.method) {
case 'client_connect': {
const { clientId } = message.params;
state.clientId = clientId;
pushToRedux(setClientId(clientId));
break;
}
case 'pair_request': {
const { from } = message.params;
state.incomingPairingRequests = [
...new Set(state.incomingPairingRequests.concat(from)),
];
pushToRedux(appendIncomingPairingRequests(from));
sendMessage(from, 'pair_request_sent', { pairId: state.clientId });
break;
}
case 'pair_request_sent': {
const { pairId } = message.params;
state.outgoingPairingRequests = [
...new Set(state.outgoingPairingRequests.concat(pairId)),
];
pushToRedux(appendOutgoingPairingRequests(pairId));
break;
}
case 'pair_request_cancel': {
const { from } = message.params;
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
sendMessage(from, 'pair_request_cancelled', { pairId: state.clientId });
break;
}
case 'pair_request_cancelled': {
const { pairId } = message.params;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
break;
}
case 'pair_request_reject': {
const { from } = message.params;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
sendMessage(from, 'pair_request_rejected', { pairId: state.clientId });
break;
}
case 'pair_request_accept': {
const { from } = message.params;
state.pairing = from;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
pushToRedux(setPairing(from));
sendMessage(from, 'pair_request_success', { pairId: state.clientId });
break;
}
case 'pair_request_success': {
const { pairId } = message.params;
state.pairing = pairId;
pushToRedux(setPairing(pairId));
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
break;
}
case 'pair_request_rejected': {
const { pairId } = message.params;
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
break;
}
case 'request_proof': {
const { plugin, pluginHash, from } = message.params;
state.incomingProofRequests = [
...new Set(state.incomingProofRequests.concat(plugin)),
];
pushToRedux(appendIncomingProofRequests(plugin));
sendMessage(from, 'proof_request_received', { pluginHash });
break;
}
case 'request_proof_by_hash': {
const { pluginHash, from } = message.params;
const plugin = await getPluginByUrl(pluginHash);
if (plugin) {
state.incomingProofRequests = [
...new Set(state.incomingProofRequests.concat(plugin)),
];
pushToRedux(appendIncomingProofRequests(plugin));
sendMessage(from, 'proof_request_received', { pluginHash });
} else {
sendMessage(from, 'request_proof_by_hash_failed', { pluginHash });
}
break;
}
case 'request_proof_by_hash_failed': {
const { pluginHash } = message.params;
requestProof(pluginHash);
break;
}
case 'proof_request_received': {
const { pluginHash } = message.params;
state.outgoingProofRequests = [
...new Set(state.outgoingProofRequests.concat(pluginHash)),
];
pushToRedux(appendOutgoingProofRequest(pluginHash));
break;
}
case 'proof_request_cancelled':
await handleRemoveOutgoingProofRequest(message);
break;
case 'proof_request_reject': {
const { pluginHash, from } = message.params;
await handleRemoveOutgoingProofRequest(message);
sendMessage(from, 'proof_request_rejected', { pluginHash });
break;
}
case 'proof_request_cancel': {
const { pluginHash, from } = message.params;
await handleRemoveIncomingProofRequest(message);
sendMessage(from, 'proof_request_cancelled', { pluginHash });
break;
}
case 'proof_request_rejected':
await handleRemoveIncomingProofRequest(message);
break;
case 'proof_request_accept': {
const { pluginHash, from } = message.params;
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const rendezvousApi = await getRendezvousApi();
browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_verifier,
data: {
pluginHash,
maxSentData,
maxRecvData,
verifierUrl:
rendezvousApi + '?clientId=' + state.clientId + ':proof',
peerId: state.pairing,
},
});
state.isVerifying = true;
pushToRedux(setIsVerifying(true));
break;
}
case 'verifier_started': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: SidePanelActionTypes.start_p2p_plugin,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'prover_setup': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.prover_setup,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'prover_started': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.prover_started,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'proof_request_start': {
const { pluginHash, from } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_proof_request,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'proof_request_end': {
const { pluginHash, proof } = message.params;
const transcript = new Transcript({
sent: proof.transcript.sent,
recv: proof.transcript.recv,
});
state.presentation = {
sent: transcript.sent(),
recv: transcript.recv(),
};
pushToRedux(setP2PPresentation(state.presentation));
browser.runtime.sendMessage({
type: OffscreenActionTypes.end_p2p_proof_request,
data: {
pluginHash: pluginHash,
proof: proof,
},
});
break;
}
default:
console.warn(`Unknown message type "${message.method}"`);
break;
}
};
socket.onerror = (error) => {
console.error('Error connecting to websocket:', error);
pushToRedux(setConnected(false));
pushToRedux(
setP2PError(
'Failed to connect to rendezvous server. Please check your connection and server URL.',
),
);
};
socket.onclose = (event) => {
console.log('WebSocket connection closed:', event.code, event.reason);
pushToRedux(setConnected(false));
if (event.code !== 1000 && event.code !== 1001) {
pushToRedux(
setP2PError(
`WebSocket connection lost: ${event.reason || 'Unknown error'}`,
),
);
}
};
};
async function handleRemoveOutgoingProofRequest(message: {
params: { pluginHash: string };
}) {
const { pluginHash } = message.params;
state.outgoingProofRequests = state.outgoingProofRequests.filter(
(hash) => hash !== pluginHash,
);
pushToRedux(setOutgoingProofRequest(state.outgoingProofRequests));
}
async function handleRemoveIncomingProofRequest(message: {
params: { pluginHash: string };
}) {
const { pluginHash } = message.params;
const plugin = await getPluginByUrl(pluginHash);
const incomingProofRequest = [];
for (const hex of state.incomingProofRequests) {
if (plugin) {
if (plugin !== hex) incomingProofRequest.push(hex);
} else {
if ((await sha256(hex)) !== pluginHash) incomingProofRequest.push(hex);
}
}
state.incomingProofRequests = incomingProofRequest;
pushToRedux(setIncomingProofRequest(state.incomingProofRequests));
}
export const disconnectSession = async () => {
if (!state.socket) return;
const socket = state.socket;
state.socket = null;
state.clientId = '';
state.pairing = '';
state.connected = false;
state.incomingPairingRequests = [];
state.outgoingPairingRequests = [];
state.incomingProofRequests = [];
state.outgoingProofRequests = [];
state.isProving = false;
state.isVerifying = false;
state.presentation = null;
pushToRedux(setPairing(''));
pushToRedux(setConnected(false));
pushToRedux(setClientId(''));
pushToRedux(setIncomingPairingRequest([]));
pushToRedux(setOutgoingPairingRequest([]));
pushToRedux(setIncomingProofRequest([]));
pushToRedux(setOutgoingProofRequest([]));
pushToRedux(setIsProving(false));
pushToRedux(setIsVerifying(false));
pushToRedux(setP2PPresentation(null));
await socket.close();
};
export async function sendMessage(
target: string,
method: string,
params?: any,
) {
const { socket, clientId } = state;
if (clientId === target) {
console.error('client cannot send message to itself.');
return;
}
if (!socket) {
console.error('socket connection not found.');
return;
}
if (!clientId) {
console.error('clientId not found.');
return;
}
socket.send(
bufferify({
method,
params: {
from: clientId,
to: target,
id: state.reqId++,
...params,
},
}),
);
}
export async function sendPairedMessage(method: string, params?: any) {
const { pairing } = state;
if (!pairing) {
console.error('not paired to a peer.');
return;
}
sendMessage(pairing, method, params);
}
export const requestProof = async (pluginHash: string) => {
const pluginHex = await getPluginByUrl(pluginHash);
sendPairedMessage('request_proof', {
plugin: pluginHex,
pluginHash,
});
};
export const endProofRequest = async (data: {
pluginHash: string;
proof: VerifierOutput;
}) => {
const transcript = new Transcript({
sent: data.proof.transcript?.sent || [],
recv: data.proof.transcript?.recv || [],
});
state.presentation = {
sent: transcript.sent(),
recv: transcript.recv(),
};
pushToRedux(setP2PPresentation(state.presentation));
sendPairedMessage('proof_request_end', {
pluginHash: data.pluginHash,
proof: data.proof,
});
};
export const onProverInstantiated = async () => {
state.isProving = true;
pushToRedux(setIsProving(true));
};
function bufferify(data: any): Buffer {
return Buffer.from(JSON.stringify(data));
}

View File

@@ -1,58 +1,42 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { PresentationJSON } from 'tlsn-js/build/types';
// Script injected into the page context
console.log('Page script injected');
const client = new RPCClient();
class TLSN {
async notarize(
url: string,
requestOptions?: {
method?: string;
headers?: { [key: string]: string };
body?: string;
},
proofOptions?: {
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
metadata?: {
[k: string]: string;
};
},
): Promise<PresentationJSON> {
const resp = await client.call(ContentScriptTypes.notarize, {
url,
method: requestOptions?.method,
headers: requestOptions?.headers,
body: requestOptions?.body,
maxSentData: proofOptions?.maxSentData,
maxRecvData: proofOptions?.maxRecvData,
notaryUrl: proofOptions?.notaryUrl,
websocketProxyUrl: proofOptions?.websocketProxyUrl,
metadata: proofOptions?.metadata,
});
return resp;
}
async runPlugin(url: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin_by_url, {
url,
params,
});
return resp;
// Simple API exposed to the page
class ExtensionAPI {
sendMessage(data: any) {
window.postMessage(
{
type: 'FROM_PAGE',
payload: data,
},
window.location.origin,
);
}
}
const connect = async () => {
return new TLSN();
};
// Expose API to the page
(window as any).extensionAPI = new ExtensionAPI();
// @ts-ignore
window.tlsn = {
connect,
};
// Listen for messages from the page
window.addEventListener('message', (event) => {
// Only accept messages from the same origin
if (event.origin !== window.location.origin) return;
window.dispatchEvent(new CustomEvent('tlsn_loaded'));
if (event.data?.type === 'FROM_PAGE') {
console.log('Received message from page:', event.data);
// Forward to content script/extension
window.postMessage(
{
type: 'TO_EXTENSION',
payload: event.data.payload,
},
window.location.origin,
);
}
});
// Dispatch event to notify page that extension is loaded
window.dispatchEvent(new CustomEvent('extension_loaded'));
export {};

View File

@@ -1,119 +1,45 @@
import browser, { browserAction } from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
(async () => {
loadScript('content.bundle.js');
const server = new RPCServer();
console.log('Content script loaded on:', window.location.href);
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_local_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_local_storage,
data: { ...localStorage },
});
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_session_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_session_storage,
data: { ...sessionStorage },
});
}
});
server.on(
ContentScriptTypes.notarize,
async (
request: ContentScriptRequest<{
url: string;
method?: string;
headers?: { [key: string]: string };
metadata?: { [key: string]: string };
body?: string;
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
}>,
) => {
const {
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
metadata,
} = request.params || {};
if (!url || !urlify(url)) throw new Error('invalid url.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_request,
data: {
...getPopupData(),
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
metadata,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.run_plugin_by_url,
async (
request: ContentScriptRequest<{
url: string;
params?: Record<string, string>;
}>,
) => {
const { url, params } = request.params || {};
if (!url) throw new Error('params must include url');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_request,
data: {
...getPopupData(),
url,
params,
},
});
return response;
},
);
})();
function loadScript(filename: string) {
const url = browser.runtime.getURL(filename);
// Inject a script into the page if needed
function injectScript() {
const script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', url);
document.body.appendChild(script);
script.src = browser.runtime.getURL('content.bundle.js');
script.type = 'text/javascript';
(document.head || document.documentElement).appendChild(script);
script.onload = () => script.remove();
}
function getPopupData() {
return {
origin: window.origin,
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
};
// Listen for messages from the extension
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('Content script received message:', request);
if (request.type === 'GET_PAGE_INFO') {
// Example: Get page information
sendResponse({
title: document.title,
url: window.location.href,
domain: window.location.hostname,
});
}
return true; // Keep the message channel open
});
// Send a message to background script when ready
browser.runtime
.sendMessage({
type: 'CONTENT_SCRIPT_READY',
url: window.location.href,
})
.catch(console.error);
// Inject script if document is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectScript);
} else {
injectScript();
}
export {};

View File

@@ -1,113 +0,0 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise';
export enum ContentScriptTypes {
notarize = 'tlsn/cs/notarize',
run_plugin_by_url = 'tlsn/cs/run_plugin_by_url',
}
export type ContentScriptRequest<params> = {
tlsnrpc: string;
} & RPCRequest<ContentScriptTypes, params>;
export type ContentScriptResponse = {
tlsnrpc: string;
} & RPCResponse;
export type RPCRequest<method, params> = {
id: number;
method: method;
params?: params;
};
export type RPCResponse = {
id: number;
result?: never;
error?: never;
};
export class RPCServer {
#handlers: Map<
ContentScriptTypes,
(message: ContentScriptRequest<any>) => Promise<any>
> = new Map();
constructor() {
window.addEventListener(
'message',
async (event: MessageEvent<ContentScriptRequest<never>>) => {
const data = event.data;
if (data.tlsnrpc !== '1.0') return;
if (!data.method) return;
const handler = this.#handlers.get(data.method);
if (handler) {
try {
const result = await handler(data);
window.postMessage({
tlsnrpc: '1.0',
id: data.id,
result,
});
} catch (error) {
window.postMessage({
tlsnrpc: '1.0',
id: data.id,
error,
});
}
} else {
throw new Error(`unknown method - ${data.method}`);
}
},
);
}
on(
method: ContentScriptTypes,
handler: (message: ContentScriptRequest<any>) => Promise<any>,
) {
this.#handlers.set(method, handler);
}
}
export class RPCClient {
#requests: Map<number, PromiseResolvers> = new Map();
#id = 0;
get id() {
return this.#id++;
}
constructor() {
window.addEventListener(
'message',
(event: MessageEvent<ContentScriptResponse>) => {
const data = event.data;
if (data.tlsnrpc !== '1.0') return;
const promise = this.#requests.get(data.id);
if (promise) {
if (typeof data.result !== 'undefined') {
promise.resolve(data.result);
this.#requests.delete(data.id);
} else if (typeof data.error !== 'undefined') {
promise.reject(data.error);
this.#requests.delete(data.id);
}
}
},
);
}
async call(method: ContentScriptTypes, params?: any): Promise<never> {
const request = { tlsnrpc: '1.0', id: this.id, method, params };
const defer = deferredPromise();
this.#requests.set(request.id, defer);
window.postMessage(request, '*');
return defer.promise;
}
}

View File

@@ -1,66 +0,0 @@
import React, { useEffect } from 'react';
import { OffscreenActionTypes } from './types';
import { BackgroundActiontype } from '../Background/rpc';
import {
initThreads,
onCreatePresentationRequest,
onCreateProverRequest,
onNotarizationRequest,
onProcessProveRequest,
onVerifyProof,
onVerifyProofRequest,
startP2PProver,
startP2PVerifier,
} from './rpc';
const Offscreen = () => {
useEffect(() => {
(async () => {
await initThreads();
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case OffscreenActionTypes.notarization_request: {
onNotarizationRequest(request);
break;
}
case OffscreenActionTypes.create_prover_request: {
onCreateProverRequest(request);
break;
}
case OffscreenActionTypes.create_presentation_request: {
onCreatePresentationRequest(request);
break;
}
case BackgroundActiontype.process_prove_request: {
onProcessProveRequest(request);
break;
}
case BackgroundActiontype.verify_proof: {
onVerifyProof(request, sendResponse);
return true;
}
case BackgroundActiontype.verify_prove_request: {
onVerifyProofRequest(request);
break;
}
case OffscreenActionTypes.start_p2p_verifier: {
startP2PVerifier(request);
break;
}
case OffscreenActionTypes.start_p2p_prover: {
startP2PProver(request);
break;
}
default:
break;
}
});
})();
}, []);
return <div className="App" />;
};
export default Offscreen;

View File

@@ -1,8 +1,32 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import Offscreen from './Offscreen';
const OffscreenApp: React.FC = () => {
React.useEffect(() => {
console.log('Offscreen document loaded');
// Listen for messages from background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('Offscreen received message:', request);
// Example message handling
if (request.type === 'PROCESS_DATA') {
// Process data in offscreen context
sendResponse({ success: true, data: 'Processed in offscreen' });
}
});
}, []);
return (
<div className="offscreen-container">
<h1>Offscreen Document</h1>
<p>This document runs in the background for processing tasks.</p>
</div>
);
};
const container = document.getElementById('app-container');
const root = createRoot(container!);
root.render(<Offscreen />);
if (container) {
const root = createRoot(container);
root.render(<OffscreenApp />);
}

View File

@@ -1,699 +0,0 @@
import browser from 'webextension-polyfill';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../Background/rpc';
import {
mapStringToRange,
NotaryServer,
Method,
Presentation as TPresentation,
Prover as TProver,
subtractRanges,
Transcript,
Verifier as TVerifier,
} from 'tlsn-js';
import { convertNotaryWsToHttp, devlog, urlify } from '../../utils/misc';
import * as Comlink from 'comlink';
import { OffscreenActionTypes } from './types';
import { PresentationJSON } from 'tlsn-js/build/types';
import { waitForEvent } from '../utils';
import {
setNotaryRequestError,
setNotaryRequestStatus,
} from '../Background/db';
const { init, Prover, Presentation, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
const provers: { [id: string]: TProver } = {};
export const initThreads = async () => {
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
hardwareConcurrency: navigator.hardwareConcurrency,
});
await init({
loggingLevel,
hardwareConcurrency: navigator.hardwareConcurrency,
});
};
export const onNotarizationRequest = async (request: any) => {
const { id } = request.data;
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
proof,
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onCreateProverRequest = async (request: any) => {
const { id } = request.data;
try {
const prover = await createProver(request.data);
provers[id] = prover;
updateRequestProgress(id, RequestProgress.ReadingTranscript);
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_response,
data: {
id,
transcript: await prover.transcript(),
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_response,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onCreatePresentationRequest = async (request: any) => {
const { id, commit, notaryUrl, websocketProxyUrl } = request.data;
const prover = provers[id];
try {
if (!prover) throw new Error(`Cannot find prover ${id}.`);
updateRequestProgress(id, RequestProgress.FinalizingOutputs);
const notarizationOutputs = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: { ...commit, server_identity: false },
})) as TPresentation;
const json = await presentation.json();
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: {
...json,
},
},
});
delete provers[id];
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onProcessProveRequest = async (request: any) => {
const { id } = request.data;
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onVerifyProof = async (request: any, sendResponse: any) => {
const result = await verifyProof(request.data);
sendResponse(result);
};
export const onVerifyProofRequest = async (request: any) => {
const proof: PresentationJSON = request.data.proof;
const result: {
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
} = await verifyProof(proof);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id: request.data.id,
verification: {
sent: result.sent,
recv: result.recv,
verifierKey: result.verifierKey,
notaryKey: result.notaryKey,
},
},
});
};
export const startP2PVerifier = async (request: any) => {
const { pluginHash, maxSentData, maxRecvData, verifierUrl } = request.data;
const verifier: TVerifier = await new Verifier({
id: pluginHash,
maxSentData: maxSentData,
maxRecvData: maxRecvData,
});
await verifier.connect(verifierUrl);
const proverStarted = waitForEvent(OffscreenActionTypes.prover_started);
browser.runtime.sendMessage({
type: BackgroundActiontype.verifier_started,
data: {
pluginHash,
},
});
await waitForEvent(OffscreenActionTypes.prover_setup);
verifier.verify().then((res) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.proof_request_end,
data: {
pluginHash,
proof: res,
},
});
});
await proverStarted;
browser.runtime.sendMessage({
type: BackgroundActiontype.start_proof_request,
data: {
pluginHash,
},
});
};
export const startP2PProver = async (request: any) => {
const {
id,
pluginUrl,
pluginHex,
url,
method,
headers,
body,
proverUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
getSecretResponse,
verifierPlugin,
} = request.data;
const hostname = urlify(url)?.hostname || '';
updateRequestProgress(id, RequestProgress.CreatingProver);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
serverIdentity: true,
});
updateRequestProgress(id, RequestProgress.GettingSession);
const resp = await fetch(`${proverUrl}/session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientType: 'Websocket',
maxRecvData,
maxSentData,
plugin: 'plugin-js',
}),
});
const { sessionId } = await resp.json();
const _url = new URL(proverUrl);
const protocol = _url.protocol === 'https:' ? 'wss' : 'ws';
const pathname = _url.pathname;
const sessionUrl = `${protocol}://${_url.host}${pathname === '/' ? '' : pathname}/notarize?sessionId=${sessionId!}`;
updateRequestProgress(id, RequestProgress.SettingUpProver);
await prover.setup(sessionUrl);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
updateRequestProgress(id, RequestProgress.ReadingTranscript);
const transcript = await prover.transcript();
let secretResps: string[] = [];
if (getSecretResponse) {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_secrets_from_transcript,
data: {
pluginUrl,
pluginHex,
method: getSecretResponse,
transcript,
p2p: true,
},
});
const msg: any = await waitForEvent(
OffscreenActionTypes.get_secrets_from_transcript_success,
);
secretResps = msg.data.secretResps;
}
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
await prover.reveal({ ...commit, server_identity: true });
updateRequestProgress(id, RequestProgress.FinalizingOutputs);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
sessionId: sessionId,
},
});
};
async function createProof(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<PresentationJSON> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders = [],
secretResps = [],
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
updateRequestProgress(id, RequestProgress.CreatingProver);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
updateRequestProgress(id, RequestProgress.GettingSession);
const sessionUrl = await notary.sessionUrl(maxSentData, maxRecvData);
updateRequestProgress(id, RequestProgress.SettingUpProver);
await prover.setup(sessionUrl);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
updateRequestProgress(id, RequestProgress.ReadingTranscript);
const transcript = await prover.transcript();
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
updateRequestProgress(id, RequestProgress.FinalizingOutputs);
const notarizationOutputs = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: { ...commit, server_identity: false },
})) as TPresentation;
const json = await presentation.json();
return {
...json,
};
}
async function createProver(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
}): Promise<TProver> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
try {
const prover: TProver = await handleProgress(
id,
RequestProgress.CreatingProver,
() =>
new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
}),
'Error creating prover',
);
const sessionUrl = await handleProgress(
id,
RequestProgress.GettingSession,
() => notary.sessionUrl(maxSentData, maxRecvData),
'Error getting session from Notary',
);
await handleProgress(
id,
RequestProgress.SettingUpProver,
() => prover.setup(sessionUrl),
'Error setting up prover',
);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
return prover;
} catch (error) {
throw error;
}
}
async function verifyProof(proof: PresentationJSON): Promise<{
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
}> {
let result: {
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
};
switch (proof.version) {
case '0.1.0-alpha.12':
result = await verify(proof);
break;
default:
result = {
sent: 'version not supported',
recv: 'version not supported',
};
break;
}
return result!;
}
async function verify(proof: PresentationJSON) {
if (proof.version !== '0.1.0-alpha.12') {
throw new Error('wrong version');
}
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 || [],
});
const vk = await presentation.verifyingKey();
const verifyingKey = Buffer.from(vk.data).toString('hex');
const notaryUrl = proof.meta.notaryUrl
? convertNotaryWsToHttp(proof.meta.notaryUrl)
: '';
const publicKey = await new NotaryServer(notaryUrl)
.publicKey()
.catch(() => '');
return {
sent: transcript.sent(),
recv: transcript.recv(),
verifierKey: verifyingKey,
notaryKey: publicKey,
};
}
function updateRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
) {
const progressMessage =
progress === RequestProgress.Error
? `${errorMessage || 'Notarization Failed'}`
: progressText(progress);
devlog(`Request ${id}: ${progressMessage}`);
browser.runtime.sendMessage({
type: BackgroundActiontype.update_request_progress,
data: {
id,
progress,
errorMessage,
},
});
}
function getWebsocketErrorMessage(
lowerError: string,
fallbackMessage: string,
): string {
const isWebsocketError =
lowerError.includes('websocket') ||
lowerError.includes('proxy') ||
lowerError.includes('connection') ||
lowerError.includes('network') ||
lowerError.includes('prover error') ||
lowerError.includes('io error') ||
lowerError.includes('certificate') ||
lowerError.includes('cert') ||
lowerError.includes('ssl') ||
lowerError.includes('tls');
if (!isWebsocketError) {
return fallbackMessage;
}
const errorPatterns = [
{
patterns: ['protocol', 'must use ws://', 'must use wss://'],
message:
'Invalid websocket proxy URL protocol. Please use ws:// or wss:// protocol in your websocket proxy URL settings.',
},
{
patterns: [
'not allowed',
'not whitelisted',
'forbidden',
'unauthorized',
'permission denied',
'access denied',
],
message:
'Target domain not allowed by websocket proxy. Please check if the website domain is supported by your proxy service.',
},
{
patterns: ['dns', 'resolve'],
message:
'Cannot resolve websocket proxy domain. Please check your websocket proxy URL in settings.',
},
{
patterns: ['timeout'],
message:
'Websocket proxy connection timeout. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['refused', 'unreachable'],
message:
'Cannot reach websocket proxy server. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['cert', 'certificate', 'certnotvalidforname'],
message:
'Cannot connect to websocket proxy server. Please check your websocket proxy URL in settings and ensure it points to a valid websocket proxy service.',
},
];
for (const { patterns, message } of errorPatterns) {
if (patterns.some((pattern) => lowerError.includes(pattern))) {
return message;
}
}
return 'Websocket proxy connection failed. Please check your websocket proxy URL in settings and ensure the server is accessible.';
}
async function handleProgress<T>(
id: string,
progress: RequestProgress,
action: () => Promise<T>,
errorMessage: string,
): Promise<T> {
try {
updateRequestProgress(id, progress);
return await action();
} catch (error: any) {
const specificError = error?.message || '';
const lowerError = specificError.toLowerCase();
const finalErrorMessage = getWebsocketErrorMessage(
lowerError,
errorMessage,
);
updateRequestProgress(id, RequestProgress.Error, finalErrorMessage);
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(id, finalErrorMessage);
throw error;
}
}

View File

@@ -1,15 +0,0 @@
export enum OffscreenActionTypes {
notarization_request = 'offscreen/notarization_request',
notarization_response = 'offscreen/notarization_response',
create_prover_request = 'offscreen/create_prover_request',
create_prover_response = 'offscreen/create_prover_response',
create_presentation_request = 'offscreen/create_presentation_request',
create_presentation_response = 'offscreen/create_presentation_response',
get_secrets_from_transcript_success = 'offscreen/get_secrets_from_transcript_success',
start_p2p_verifier = 'offscreen/start_p2p_verifier',
start_p2p_prover = 'offscreen/start_p2p_prover',
prover_started = 'offscreen/prover_started',
prover_setup = 'offscreen/prover_setup',
start_p2p_proof_request = 'offscreen/start_p2p_proof_request',
end_p2p_proof_request = 'offscreen/end_p2p_proof_request',
}

View File

@@ -1,39 +0,0 @@
export function subtractRanges(
ranges: { start: number; end: number },
negatives: { start: number; end: number }[],
): { start: number; end: number }[] {
const returnVal: { start: number; end: number }[] = [ranges];
negatives
.sort((a, b) => (a.start < b.start ? -1 : 1))
.forEach(({ start, end }) => {
const last = returnVal.pop()!;
if (start < last.start || end > last.end) {
console.error('invalid ranges');
return;
}
if (start === last.start && end === last.end) {
return;
}
if (start === last.start && end < last.end) {
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end < last.end) {
returnVal.push({ start: last.start, end: start });
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end === last.end) {
returnVal.push({ start: last.start, end: start });
return;
}
});
return returnVal;
}

View File

@@ -1,9 +0,0 @@
import * as Comlink from 'comlink';
import init, { Prover, Presentation, Verifier } from 'tlsn-js';
Comlink.expose({
init,
Prover,
Presentation,
Verifier,
});

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Settings</title>
</head>
<body>
<div id="app-container"></div>
</body>
</html>

View File

@@ -1,21 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/brands";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/regular";
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import Options from '../../pages/Options';
import './index.scss';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(<Options />);

View File

@@ -1,115 +1,31 @@
import React, { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import { setActiveTab, setRequests } from '../../reducers/requests';
import { BackgroundActiontype } from '../Background/rpc';
import Options from '../../pages/Options';
import Request from '../../pages/Requests/Request';
import Home from '../../pages/Home';
import logo from '../../assets/img/icon-128.png';
import RequestBuilder from '../../pages/RequestBuilder';
import Notarize from '../../pages/Notarize';
import ProofViewer from '../../pages/ProofViewer';
import ProofUploader from '../../pages/ProofUploader';
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../../reducers';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import { isPopupWindow } from '../../utils/misc';
import { NotarizeApproval } from '../../pages/NotarizeApproval';
import { MenuIcon } from '../../components/Menu';
import { P2PHome } from '../../pages/PeerToPeer';
import { fetchP2PState } from '../../reducers/p2p';
import { RunPluginByUrlApproval } from '../../pages/RunPluginByUrlApproval';
const Popup = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [isPopup, setIsPopup] = useState(isPopupWindow());
useEffect(() => {
fetchP2PState();
}, []);
const Popup: React.FC = () => {
const message = useSelector((state: RootState) => state.app.message);
useEffect(() => {
(async () => {
const [tab] = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
dispatch(setActiveTab(tab || null));
const logs = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_requests,
data: tab?.id,
});
dispatch(setRequests(logs));
await browser.runtime.sendMessage({
type: BackgroundActiontype.get_prove_requests,
data: tab?.id,
});
})();
}, []);
useEffect(() => {
chrome.runtime.onMessage.addListener((request) => {
switch (request.type) {
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
request.data.tabId === 'background'
) {
store.dispatch(request.action);
}
break;
}
case BackgroundActiontype.change_route: {
if (request.data.tabId === 'background') {
navigate(request.route);
break;
}
}
}
});
}, []);
const handleClick = async () => {
// Send message to background script
const response = await browser.runtime.sendMessage({ type: 'PING' });
console.log('Response from background:', response);
};
return (
<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"
src={logo}
alt="logo"
onClick={() => navigate('/')}
/>
<div className="flex flex-row flex-grow items-center justify-end gap-4">
{!isPopup && (
<>
<MenuIcon />
</>
)}
</div>
<div className="w-[400px] h-[300px] bg-white p-8">
<div className="flex flex-col items-center justify-center h-full">
<h1 className="text-3xl font-bold text-gray-800 mb-4">Hello World!</h1>
<p className="text-gray-600 mb-6">{message || 'Chrome Extension Boilerplate'}</p>
<button
onClick={handleClick}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Test Background Script
</button>
</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={<Home tab="history" />} />
<Route path="/requests" element={<Home tab="network" />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/home" element={<Home />} />
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route
path="/run-plugin-approval"
element={<RunPluginByUrlApproval />}
/>
<Route path="/p2p" element={<P2PHome />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
);
};
export default Popup;
export default Popup;

View File

@@ -1,23 +1,16 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import Popup from './Popup';
import './index.scss';
import { Provider } from 'react-redux';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
import store from '../../utils/store';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(
<Provider store={store}>
<HashRouter>
if (container) {
const root = createRoot(container);
root.render(
<Provider store={store}>
<Popup />
</HashRouter>
</Provider>,
);
</Provider>,
);
}

View File

@@ -1,583 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import {
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
StepConfig,
InputFieldConfig,
} from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import logo from '../../assets/img/icon-128.png';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import { useRequestHistory } from '../../reducers/history';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../Background/rpc';
import { getPluginByUrl, getPluginConfigByUrl } from '../Background/db';
import { SidePanelActionTypes } from './types';
import { fetchP2PState, useClientId } from '../../reducers/p2p';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
const [url, setUrl] = useState('');
const [hex, setHex] = useState('');
const [p2p, setP2P] = useState(false);
const [params, setParams] = useState<Record<string, string> | undefined>();
const [started, setStarted] = useState(false);
const clientId = useClientId();
useEffect(() => {
fetchP2PState();
browser.runtime.sendMessage({
type: SidePanelActionTypes.panel_opened,
});
}, []);
useEffect(() => {
browser.runtime.onMessage.addListener(async (request) => {
const { type, data } = request;
switch (type) {
case SidePanelActionTypes.execute_plugin_request: {
const pluginIdentifier = data.pluginUrl || data.pluginHash;
setConfig(await getPluginConfigByUrl(pluginIdentifier));
setUrl(pluginIdentifier);
setParams(data.pluginParams);
setStarted(true);
break;
}
case SidePanelActionTypes.run_p2p_plugin_request: {
const { pluginHash, plugin } = data;
const config =
(await getPluginConfigByUrl(pluginHash)) ||
(await getPluginConfig(hexToArrayBuffer(plugin)));
setUrl(pluginHash);
setHex(plugin);
setP2P(true);
setConfig(config);
break;
}
case SidePanelActionTypes.start_p2p_plugin: {
setStarted(true);
break;
}
case SidePanelActionTypes.is_panel_open: {
return { isOpen: true };
}
case SidePanelActionTypes.reset_panel: {
setConfig(null);
setUrl('');
setHex('');
setStarted(false);
break;
}
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.panel_closing,
});
}
});
}, []);
return (
<div className="flex flex-col bg-slate-100 w-screen h-screen">
<div className="relative flex flex-nowrap flex-shrink-0 flex-row items-center gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
<img className="h-5" src={logo} alt="logo" />
<button
className="button absolute right-2"
onClick={() => window.close()}
>
Close
</button>
</div>
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
url={url}
hex={hex}
config={config}
p2p={p2p}
clientId={clientId}
presetParameterValues={params}
/>
)}
</div>
);
}
function PluginBody({
url,
hex,
config,
p2p,
clientId,
presetParameterValues,
}: {
config: PluginConfig;
url: string;
hex?: string;
clientId?: string;
p2p?: boolean;
presetParameterValues?: Record<string, string>;
}): ReactElement {
const { title, description, icon, steps } = config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const setResponse = useCallback(
(response: any, i: number) => {
const result = responses.concat();
result[i] = response;
setResponses(result);
if (i === steps!.length - 1 && !!response) {
setNotarizationId(response);
}
},
[url, responses],
);
useEffect(() => {
if (notaryRequest?.status === 'success') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
proof: notaryRequest.proof,
},
});
} else if (notaryRequest?.sessionId) {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
sessionId: notaryRequest.sessionId,
},
});
} else if (notaryRequest?.status === 'error') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
error:
notaryRequest.errorMessage ||
notaryRequest.error ||
'Notarization failed',
},
});
}
}, [url, notaryRequest?.status, notaryRequest?.sessionId]);
return (
<div className="flex flex-col p-4">
<div className="flex flex-row items-center gap-4">
<img className="w-12 h-12 self-start" src={icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full text-base">
{title}
</div>
<div className="text-slate-500 text-sm">{description}</div>
</div>
</div>
<div className="flex flex-col items-start gap-8 mt-8">
{steps?.map((step, i) => (
<StepContent
key={i}
url={url}
config={config}
hex={hex}
index={i}
setResponse={setResponse}
lastResponse={i > 0 ? responses[i - 1] : undefined}
responses={responses}
p2p={p2p}
clientId={clientId}
parameterValues={presetParameterValues}
{...step}
/>
))}
</div>
</div>
);
}
function StepContent(
props: StepConfig & {
url: string;
hex?: string;
clientId?: string;
index: number;
setResponse: (resp: any, i: number) => void;
responses: any[];
lastResponse?: any;
config: PluginConfig;
p2p?: boolean;
parameterValues?: Record<string, string>;
},
): ReactElement {
const {
index,
title,
description,
cta,
action,
setResponse,
lastResponse,
prover,
url,
hex: _hex,
config,
p2p = false,
clientId = '',
parameterValues,
inputs,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
const [error, setError] = useState('');
const [notarizationId, setNotarizationId] = useState('');
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const notaryRequest = useRequestHistory(notarizationId);
useEffect(() => {
if (inputs) {
const initialValues: Record<string, string> = {};
inputs.forEach((input) => {
if (input.defaultValue) {
initialValues[input.name] = input.defaultValue;
}
});
setInputValues(initialValues);
}
}, [inputs]);
const getPlugin = useCallback(async () => {
const hex = (await getPluginByUrl(url)) || _hex;
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config, { p2p, clientId });
}, [url, _hex, config, p2p, clientId]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
if (!plugin) return;
if (index > 0 && !lastResponse) return;
// Validate required input fields
if (inputs) {
for (const input of inputs) {
if (
input.required &&
(!inputValues[input.name] || inputValues[input.name].trim() === '')
) {
setError(`${input.label} is required`);
return;
}
}
}
setPending(true);
setError('');
try {
let stepData: any;
if (index > 0) {
stepData = lastResponse;
} else {
stepData = { ...parameterValues, ...inputValues };
}
const out = await plugin.call(action, JSON.stringify(stepData));
const val = JSON.parse(out!.string());
if (val && prover) {
setNotarizationId(val);
} else {
setCompleted(!!val);
}
setResponse(val, index);
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unknown error');
} finally {
setPending(false);
}
}, [
action,
index,
lastResponse,
prover,
getPlugin,
inputs,
inputValues,
parameterValues,
]);
const onClick = useCallback(() => {
if (
pending ||
completed ||
notaryRequest?.status === 'pending' ||
notaryRequest?.status === 'success'
)
return;
processStep();
}, [processStep, pending, completed, notaryRequest]);
const viewProofInPopup = useCallback(async () => {
if (!notaryRequest) return;
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
data: notaryRequest,
});
await browser.runtime.sendMessage({
type: BackgroundActiontype.open_popup,
data: {
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
route: `/verify/${notaryRequest.id}`,
},
});
}, [notaryRequest, notarizationId]);
const viewP2P = useCallback(async () => {
await browser.runtime.sendMessage({
type: BackgroundActiontype.open_popup,
data: {
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
route: `/p2p`,
},
});
}, []);
useEffect(() => {
// only auto-progress if this step does need inputs
if (!inputs || inputs.length === 0) {
processStep();
}
}, [processStep, inputs]);
let btnContent = null;
console.log('notaryRequest', notaryRequest);
console.log('notarizationId', notarizationId);
if (prover && p2p) {
btnContent = (
<button
className={classNames(
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
onClick={viewP2P}
>
<span className="text-sm">View in P2P</span>
</button>
);
} else if (completed || notaryRequest?.sessionId) {
btnContent = (
<button
className={classNames(
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
'!bg-green-200 !text-black cursor-default border border-green-500 rounded',
)}
>
<Icon className="text-green-600" fa="fa-solid fa-check" />
<span className="text-sm">DONE</span>
</button>
);
} else if (notaryRequest?.status === 'success') {
btnContent = (
<button
className={classNames(
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
onClick={viewProofInPopup}
>
<span className="text-sm">View</span>
</button>
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
btnContent = (
<div className="flex flex-col gap-2">
{notaryRequest?.progress === RequestProgress.Error && (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
</div>
)}
{notaryRequest?.progress !== RequestProgress.Error && (
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-sm">
{notaryRequest?.progress !== undefined
? `(${(((notaryRequest.progress + 1) / 6.06) * 100).toFixed()}%) ${progressText(notaryRequest.progress)}`
: 'Pending...'}
</span>
</button>
)}
</div>
);
} else {
btnContent = (
<button
className={classNames(
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
disabled={index > 0 && typeof lastResponse === 'undefined'}
onClick={onClick}
>
<span className="text-sm">{cta}</span>
</button>
);
}
return (
<div className="flex flex-row gap-4 text-base w-full">
<div className="text-slate-500 self-start">{index + 1}.</div>
<div className="flex flex-col flex-grow flex-shrink w-0">
<div
className={classNames('font-semibold', {
'line-through text-slate-500': completed,
})}
>
{title}
</div>
{!!description && (
<div className="text-slate-500 text-sm">{description}</div>
)}
{!!error && (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<div className="text-red-500 text-sm">{error}</div>
</div>
</div>
)}
{inputs && inputs.length > 0 && !completed && (
<div className="flex flex-col gap-3 mt-3">
{inputs.map((input) => (
<InputField
key={input.name}
config={input}
value={inputValues[input.name] || ''}
onChange={(value) =>
setInputValues((prev) => ({ ...prev, [input.name]: value }))
}
/>
))}
</div>
)}
{btnContent}
</div>
</div>
);
}
interface InputFieldProps {
config: InputFieldConfig;
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}
function InputField({
config,
value,
onChange,
disabled = false,
}: InputFieldProps): ReactElement {
const { name, label, type, placeholder, required, options } = config;
const baseClasses =
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
const renderInput = () => {
switch (type) {
case 'textarea':
return (
<textarea
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className={classNames(baseClasses, 'resize-y min-h-[80px]')}
rows={3}
/>
);
case 'select':
return (
<select
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
required={required}
disabled={disabled}
className={baseClasses}
>
<option value="">{placeholder || 'Select an option'}</option>
{options?.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
default:
return (
<input
type={type}
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className={baseClasses}
/>
);
}
};
return (
<div className="flex flex-col gap-1">
<label htmlFor={name} className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
{renderInput()}
</div>
);
}

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" width="480px">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Side Panel</title>
</head>
<body>
<div id="app-container"></div>
<div id="modal-root"></div>
</body>
</html>

View File

@@ -1,29 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import SidePanel from './SidePanel';
import store from '../../utils/store';
import { Provider } from 'react-redux';
import { BackgroundActiontype } from '../Background/rpc';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
chrome.runtime.onMessage.addListener((request) => {
switch (request.type) {
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
request.data.tabId === 'background'
) {
store.dispatch(request.action);
}
break;
}
}
});
root.render(
<Provider store={store}>
<SidePanel />
</Provider>,
);

View File

@@ -1,28 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
@import "../Popup/index.scss";
html {
width: 100vw;
height: 100vh;
}
body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
}
#app-container {
width: 100vw;
height: 100vh;
}

View File

@@ -1,11 +0,0 @@
export enum SidePanelActionTypes {
panel_opened = 'sidePanel/panel_opened',
panel_closing = 'sidePanel/panel_closing',
execute_plugin_request = 'sidePanel/execute_plugin_request',
execute_plugin_response = 'sidePanel/execute_plugin_response',
run_p2p_plugin_request = 'sidePanel/run_p2p_plugin_request',
run_p2p_plugin_response = 'sidePanel/run_p2p_plugin_response',
start_p2p_plugin = 'sidePanel/start_p2p_plugin',
is_panel_open = 'sidePanel/is_panel_open',
reset_panel = 'sidePanel/reset_panel',
}

View File

@@ -1,8 +1,7 @@
{
"manifest_version": 3,
"name": "TLSN Extension",
"description": "A chrome extension for TLSN",
"options_page": "options.html",
"name": "Extension Boilerplate",
"description": "A minimal Chrome extension boilerplate",
"background": {
"service_worker": "background.bundle.js"
},
@@ -10,34 +9,27 @@
"default_popup": "popup.html",
"default_icon": "icon-34.png"
},
"side_panel": {
"default_path": "sidePanel.html"
},
"icons": {
"128": "icon-128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
"extension_pages": "script-src 'self'; object-src 'self';"
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"matches": ["http://*/*", "https://*/*"],
"js": ["contentScript.bundle.js"],
"css": ["content.styles.css"]
}
],
"web_accessible_resources": [
{
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
"resources": ["content.bundle.js"],
"matches": ["http://*/*", "https://*/*"]
}
],
"host_permissions": ["<all_urls>"],
"permissions": [
"offscreen",
"storage",
"webRequest",
"activeTab",
"sidePanel"
"activeTab"
]
}
}

View File

@@ -1,44 +0,0 @@
import React, { ReactElement, ReactNode } from 'react';
import logo from '../../assets/img/icon-128.png';
export function BaseApproval({
onSecondaryClick,
onPrimaryClick,
header,
children,
secondaryCTAText = 'Cancel',
primaryCTAText = 'Accept',
}: {
header: ReactNode;
children: ReactNode;
onSecondaryClick: () => void;
onPrimaryClick: () => void;
secondaryCTAText?: string;
primaryCTAText?: string;
}): ReactElement {
return (
<div className="absolute flex flex-col items-center w-screen h-screen bg-white gap-2 cursor-default">
<div className="w-full p-2 border-b border-gray-200 text-gray-500">
<div className="flex flex-row items-end justify-start gap-2">
<img className="h-5" src={logo} alt="logo" />
<span className="font-semibold">{header}</span>
</div>
</div>
<div className="flex flex-col flex-grow gap-2 overflow-y-auto w-full">
{children}
</div>
<div className="flex flex-row w-full gap-2 justify-end border-t p-4">
{!!onSecondaryClick && !!secondaryCTAText && (
<button className="button" onClick={onSecondaryClick}>
{secondaryCTAText}
</button>
)}
{!!onPrimaryClick && !!primaryCTAText && (
<button className="button button--primary" onClick={onPrimaryClick}>
{primaryCTAText}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,168 +0,0 @@
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
import NotarizeIcon from '../../assets/img/notarize.png';
import { urlify } from '../../utils/misc';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../../entries/Background/rpc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import classNames from 'classnames';
import dayjs from 'dayjs';
import RequestMenu from './request-menu';
const charwise = require('charwise');
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto pb-36">
{history
.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})
.reverse()}
</div>
);
}
export function OneRequestHistory(props: {
requestId: string;
className?: string;
hideActions?: string[];
}): ReactElement {
const { hideActions = [] } = props;
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
const [showingMenu, showMenu] = useState(false);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
const onView = useCallback(() => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
data: request,
});
navigate('/verify/' + request?.id);
}, [request]);
const onShowError = useCallback(async () => {
showError(true);
}, [request?.error, showError]);
const closeAllModal = useCallback(() => {
showError(false);
}, [showError]);
const day = dayjs(charwise.decode(props.requestId, 'hex'));
return (
<div
className={classNames(
'flex flex-row items-center flex-nowrap border rounded-md px-2.5 py-3 gap-0.5 hover:bg-slate-50 cursor-pointer relative',
{
'!cursor-default !bg-slate-200': status === 'pending',
},
props.className,
)}
onClick={() => {
if (status === 'success') onView();
if (status === 'error') onShowError();
}}
>
<ErrorModal />
<div className="w-12 h-12 rounded-full flex flex-row items-center justify-center bg-slate-300">
<img
className="relative w-7 h-7 top-[-1px] opacity-60"
src={NotarizeIcon}
/>
</div>
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0 gap-1">
<div className="flex flex-row text-black text-sm font-semibold px-2 rounded-md overflow-hidden text-ellipsis gap-1">
<span>Notarize request</span>
<span className="font-normal border-b border-dashed border-slate-400 text-slate-500">
{requestUrl?.hostname}
</span>
</div>
<div
className={classNames('font-semibold px-2 rounded-sm w-fit', {
'text-green-600': status === 'success',
'text-red-600': status === 'error',
})}
>
{status === 'success' && 'Success'}
{status === 'error' && 'Error'}
{status === 'pending' && (
<div className="text-center flex flex-row flex-grow-0 gap-2 self-end items-center justify-center text-slate-600">
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
<span className="">
{request?.progress === RequestProgress.Error
? `${progressText(request.progress, request.errorMessage)}`
: request?.progress
? `(${(
((request.progress + 1) / 6.06) *
100
).toFixed()}%) ${progressText(request.progress)}`
: 'Pending...'}
</span>
</div>
)}
</div>
</div>
<div className="flex flex-col items-end gap-1">
<div className="h-4">
{!hideActions.length && (
<Icon
className="text-slate-500 hover:text-slate-600 relative"
fa="fa-solid fa-ellipsis"
onClick={(e) => {
e.stopPropagation();
showMenu(true);
}}
>
{showingMenu && (
<RequestMenu requestId={props.requestId} showMenu={showMenu} />
)}
</Icon>
)}
</div>
<div className="text-slate-500" title={day.format('LLLL')}>
{day.fromNow()}
</div>
</div>
</div>
);
function ErrorModal(): ReactElement {
const msg = typeof request?.error === 'string' && request?.error;
return !showingError ? (
<></>
) : (
<Modal
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500"
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || request?.errorMessage}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
onClick={closeAllModal}
>
OK
</button>
</Modal>
);
}
}

View File

@@ -1,307 +0,0 @@
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import {
addRequestCid,
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import { download, upload } from '../../utils/misc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import { EXPLORER_API } from '../../utils/constants';
import copy from 'copy-to-clipboard';
import { setNotaryRequestCid } from '../../entries/Background/db';
import { useDispatch } from 'react-redux';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { BackgroundActiontype } from '../../entries/Background/rpc';
export default function RequestMenu({
requestId,
showMenu,
}: {
showMenu: (opened: boolean) => void;
requestId: string;
}): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(requestId);
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [showRemoveModal, setShowRemoveModal] = useState(false);
const onRetry = useCallback(async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
id: requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [requestId]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(requestId));
}, [requestId]);
if (!request) return <></>;
const { status } = request;
return (
<>
{showingShareConfirmation && (
<ShareConfirmationModal
requestId={requestId}
setShowingShareConfirmation={setShowingShareConfirmation}
showMenu={showMenu}
/>
)}
<RemoveHistory
onRemove={onDelete}
showRemovalModal={showRemoveModal}
setShowRemoveModal={setShowRemoveModal}
onCancel={() => setShowRemoveModal(false)}
/>
<div
className="fixed top-0 left-0 w-screen h-screen z-10 cursor-default"
onClick={(e) => {
e.stopPropagation();
showMenu(false);
}}
/>
<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">
{status === 'success' && (
<>
<RequestMenuRow
fa="fa-solid fa-download"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
showMenu(false);
download(`${request.id}.json`, JSON.stringify(request.proof));
}}
>
Download
</RequestMenuRow>
<RequestMenuRow
fa="fa-solid fa-upload"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
setShowingShareConfirmation(true);
}}
>
Share
</RequestMenuRow>
</>
)}
{status === 'error' && (
<RequestMenuRow
fa="fa-solid fa-arrows-rotate"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
onRetry();
showMenu(false);
}}
>
Retry
</RequestMenuRow>
)}
<RequestMenuRow
fa="fa-solid fa-trash"
className="border-b border-slate-300 !text-red-500"
onClick={(e) => {
e.stopPropagation();
setShowRemoveModal(true);
}}
>
Delete
</RequestMenuRow>
</div>
</div>
</>
);
}
function RequestMenuRow(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>
);
}
function ShareConfirmationModal({
setShowingShareConfirmation,
requestId,
showMenu,
}: {
showMenu: (opened: boolean) => void;
setShowingShareConfirmation: (showing: boolean) => void;
requestId: string;
}): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(requestId);
const [uploadError, setUploadError] = useState('');
const [uploading, setUploading] = useState(false);
const handleUpload = useCallback(async () => {
setUploading(true);
try {
const data = await upload(
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
await setNotaryRequestCid(requestId, data);
dispatch(addRequestCid(requestId, data));
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [requestId, request, request?.cid]);
const onClose = useCallback(() => {
setShowingShareConfirmation(false);
showMenu(false);
}, [showMenu]);
return !request ? (
<></>
) : (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={(e) => {
e.stopPropagation();
onClose();
}}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!request.cid ? (
<p className="text-slate-500 text-center">
{uploadError ||
'This will make your proof publicly accessible by anyone with the CID'}
</p>
) : (
<input
className="input w-full bg-slate-100 border border-slate-200"
readOnly
value={`${EXPLORER_API}/ipfs/${request.cid}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!request.cid ? (
<>
{!uploadError && (
<button
onClick={handleUpload}
className="button button--primary flex flex-row items-center justify-center gap-2 m-0"
disabled={uploading}
>
{uploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
I understand
</button>
)}
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={onClose}
>
Close
</button>
</>
) : (
<>
<button
onClick={() => copy(`${EXPLORER_API}/ipfs/${request.cid}`)}
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
>
Copy
</button>
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={onClose}
>
Close
</button>
</>
)}
</div>
</Modal>
);
}
export function RemoveHistory(props: {
onRemove: () => void;
showRemovalModal: boolean;
setShowRemoveModal: (show: boolean) => void;
onCancel: () => void;
}): ReactElement {
const { onRemove, setShowRemoveModal, showRemovalModal } = props;
const onCancel = useCallback(() => {
setShowRemoveModal(false);
}, [showRemovalModal]);
return !showRemovalModal ? (
<></>
) : (
<Modal
onClose={onCancel}
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
<div className="text-base">
Are you sure you want to delete this attestation?
</div>
<div className="mb-1">
<span className="text-red-500 font-bold">Warning:</span> this cannot
be undone.
</div>
<div className="flex flex-row gap-2 justify-end">
<button
className="m-0 w-24 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
onClick={onCancel}
>
Cancel
</button>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
onClick={onRemove}
>
Delete
</button>
</div>
</ModalContent>
</Modal>
);
}

View File

@@ -1,5 +0,0 @@
#home {
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -1,134 +0,0 @@
import React, {
MouseEventHandler,
ReactElement,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { ErrorModal } from '../../components/ErrorModal';
import History from '../History';
import './index.scss';
import Requests from '../Requests';
import { fetchPluginHashes } from '../../utils/rpc';
import { PluginList } from '../../components/PluginList';
import { getDeveloperMode } from '../../utils/storage';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network' | 'plugins'>(
props.tab || 'history',
);
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [developerMode, setDeveloperMode] = useState(false);
useEffect(() => {
fetchPluginHashes();
getDeveloperMode().then(setDeveloperMode);
}, []);
useEffect(() => {
if (props.tab === 'network' && !developerMode) {
setTab('history');
}
}, [props.tab, developerMode]);
useEffect(() => {
const element = scrollableContent.current;
if (!element) return;
const onScroll = () => {
if (element.scrollTop > 0) {
setFix(true);
} else {
setFix(false);
}
};
element.addEventListener('scroll', onScroll);
return () => {
element.removeEventListener('scroll', onScroll);
};
}, [scrollableContent]);
return (
<div
id="home"
ref={scrollableContent}
className="flex flex-col flex-grow overflow-y-auto"
>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<div
className={classNames(
'flex flex-row justify-center items-center z-10',
{
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40':
shouldFix,
},
)}
>
{developerMode && (
<TabSelector
onClick={() => setTab('network')}
selected={tab === 'network'}
>
Network
</TabSelector>
)}
<TabSelector
onClick={() => setTab('history')}
selected={tab === 'history'}
>
History
</TabSelector>
{developerMode && (
<TabSelector
onClick={() => setTab('plugins')}
selected={tab === 'plugins'}
>
Plugins
</TabSelector>
)}
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && developerMode && (
<Requests shouldFix={shouldFix} />
)}
{tab === 'plugins' && (
<PluginList
className="p-2 overflow-y-auto"
showAddButton={developerMode}
/>
)}
</div>
</div>
);
}
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>
);
}

View File

@@ -1,567 +0,0 @@
import classNames from 'classnames';
import React, {
ReactElement,
useState,
useCallback,
ReactEventHandler,
useEffect,
useRef,
useMemo,
} from 'react';
import { useNavigate, useParams } from 'react-router';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import Icon from '../../components/Icon';
import { urlify } from '../../utils/misc';
import {
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
export default function Notarize(): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const navigate = useNavigate();
const dispatch = useDispatch();
const [step, setStep] = useState(0);
const [secretHeaders, setSecretHeaders] = useState<string[]>([]);
const [secretResps, setSecretResps] = useState<string[]>([]);
const notarize = useCallback(async () => {
if (!req) return;
const hostname = urlify(req.url)?.hostname;
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const headers: { [k: string]: string } = req.requestHeaders.reduce(
(acc: any, h) => {
acc[h.name] = h.value;
return acc;
},
{ Host: hostname },
);
//TODO: for some reason, these needs to be override to work
headers['Accept-Encoding'] = 'identity';
headers['Connection'] = 'close';
dispatch(
// @ts-ignore
notarizeRequest({
url: req.url,
method: req.method,
headers,
body: req.requestBody,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
}),
);
navigate(`/history`);
}, [req, secretHeaders, secretResps]);
if (!req) return <></>;
let body;
switch (step) {
case 0:
body = (
<RevealHeaderStep
onNext={() => setStep(1)}
onCancel={() => navigate(-1)}
setSecretHeaders={setSecretHeaders}
/>
);
break;
case 1:
body = (
<HideResponseStep
onNext={notarize}
onCancel={() => setStep(0)}
setSecretResps={setSecretResps}
/>
);
break;
default:
body = null;
break;
}
return (
<div className="flex flex-col flex-nowrap flex-grow">
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
<Icon
className="cursor-point text-slate-400 hover:text-slate-700"
fa="fa-solid fa-xmark"
onClick={() => navigate(-1)}
/>
<div className="flex flex-col flex-shrink flex-grow mr-20 w-0 select-none">
<span className="font-bold text-slate-700">
{`Notarizing a ${req.method} request`}
</span>
<span
className="text-ellipsis whitespace-nowrap overflow-hidden"
title={req.url}
>
{req.url}
</span>
</div>
</div>
{body}
</div>
);
}
export function RevealHeaderStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretHeaders: (secrets: string[]) => void;
}): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const headers = req?.requestHeaders;
useEffect(() => {
if (!req) return;
props.setSecretHeaders(
req.requestHeaders
.map((h) => {
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}
return '';
})
.filter((d) => !!d),
);
}, [revealed]);
const changeHeaderKey = useCallback(
(key: string, shouldReveal: boolean) => {
if (!req) return;
setRevealed({
...revealed,
[key]: shouldReveal,
});
},
[revealed, req],
);
if (!headers) return <></>;
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 1 of 2: Select which request headers you want to reveal`
</div>
<div className="flex-grow flex-shrink h-0 overflow-y-auto">
<table className="border border-slate-300 border-collapse table-fixed">
<tbody className="bg-slate-200">
{headers.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top">
<input
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
.join('')}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Cancel
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Next
</button>
</div>
</div>
);
}
export function RevealHeaderTable(props: {
headers: { name: string; value: string }[];
className?: string;
onChange: (revealed: { [key: string]: boolean }) => void;
}) {
const { headers } = props;
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const changeHeaderKey = useCallback(
(key: string, shouldReveal: boolean) => {
const result = {
...revealed,
[key]: shouldReveal,
};
setRevealed(result);
props.onChange(result);
},
[revealed],
);
return (
<table
className={classNames(
'border border-slate-300 border-collapse table-fixed',
props.className,
)}
>
<thead className="bg-slate-200">
<th className="border border-slate-300 py-1 px-2 align-middle w-8"></th>
<th className="border border-slate-300 py-1 px-2 align-middle">Name</th>
<th className="border border-slate-300 py-1 px-2 align-middle">
Value
</th>
</thead>
<tbody className="bg-slate-100">
{headers.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top w-8">
<input
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
.join('')}
</td>
</tr>
))}
</tbody>
</table>
);
}
export function HideResponseStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretResps: (secrets: string[]) => void;
}): React.ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [responseText, setResponseText] = useState('');
const [redactedRanges, setRedactedRanges] = useState<
{ start: number; end: number }[]
>([]);
const [isRedactMode, setIsRedactMode] = useState(true);
const taRef = useRef<HTMLTextAreaElement | null>(null);
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]);
}
}
return mergedRanges;
};
useEffect(() => {
if (!req) return;
const options = {
method: req.method,
headers: req.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
}
return acc;
},
{},
),
body: req.requestBody,
};
if (req?.formData) {
const formData = new URLSearchParams();
Object.entries(req.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
replay(req.url, options).then((resp) => setResponseText(resp));
}, [req]);
useEffect(() => {
const current = taRef.current;
if (current) {
current.focus();
}
}, [taRef]);
if (!req) return <></>;
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:{' '}
{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.join('')}
onMouseUp={onSelectionChange}
/>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Back
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Notarize
</button>
</div>
</div>
);
}
export function RedactBodyTextarea(props: {
className?: string;
onChange: (secretResponse: string[]) => void;
request: {
url: string;
method?: string;
headers?: { [name: string]: string };
formData?: { [k: string]: string[] };
body?: string;
};
}) {
const { className, onChange, request } = props;
const [loading, setLoading] = useState(false);
const [responseText, setResponseText] = useState('');
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
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);
onChange(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
},
[responseText],
);
useEffect(() => {
const options = {
method: request.method,
headers: request.headers,
body: request.body,
};
if (request?.formData) {
const formData = new URLSearchParams();
Object.entries(request.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
setLoading(true);
replay(request.url, options).then((resp) => {
setResponseText(resp);
setLoading(false);
});
}, [request]);
useEffect(() => {
const current = taRef.current;
if (current) {
current.focus();
current.setSelectionRange(start, end);
}
}, [taRef, start, end]);
let shieldedText = '';
if (end > start) {
shieldedText = Array(start)
.fill('*')
.join('')
.concat(responseText.substring(start, end))
.concat(
Array(responseText.length - end)
.fill('*')
.join(''),
);
}
if (loading) {
return (
<div className="flex flex-col items-center !pt-4 flex-grow textarea bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
);
}
return (
<textarea
ref={taRef}
className={classNames(
'flex-grow textarea bg-slate-100 font-mono',
className,
)}
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
);
}
const replay = async (url: string, options: any) => {
const resp = await fetch(url, options);
const contentType =
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return resp.text();
} else if (contentType?.includes('text')) {
return resp.text();
} else if (contentType?.includes('image')) {
return resp.blob().then((blob) => blob.text());
} else {
return resp.blob().then((blob) => blob.text());
}
};

View File

@@ -1,178 +0,0 @@
import React, { ReactElement, useCallback, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { RedactBodyTextarea, RevealHeaderTable } from '../Notarize';
export function NotarizeApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const config = JSON.parse(params.get('config')!);
const hostname = urlify(origin || '')?.hostname;
const [step, setStep] = useState<'overview' | 'headers' | 'response'>(
'overview',
);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const [secretResps, setSecretResps] = useState<string[]>([]);
const headerList = Object.entries(config.headers || {}).map(
([name, value]) => ({
name,
value: String(value),
}),
);
const onCancel = useCallback(() => {
if (step === 'headers') return setStep('overview');
if (step === 'response') return setStep('headers');
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: false,
});
}, [step]);
const onAccept = useCallback(() => {
if (step === 'overview') return setStep('headers');
if (step === 'headers') return setStep('response');
const secretHeaders = headerList
.map((h) => {
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}
return '';
})
.filter((d) => !!d);
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: {
...config,
secretHeaders,
secretResps,
},
});
}, [revealed, step, secretResps, config]);
let body, headerText, primaryCta, secondaryCta;
switch (step) {
case 'overview':
headerText = 'Notarizing Request';
primaryCta = 'Next';
secondaryCta = 'Cancel';
body = (
<>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
<b className="text-blue-500">{hostname}</b> wants to notarize the
following request:
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow break-all">
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<TableRow label="Method" value={config.method?.toUpperCase()} />
<TableRow label="Request URL" value={config.url} />
<TableRow label="Notary URL" value={config.notaryUrl} />
<TableRow label="Proxy URL" value={config.websocketProxyUrl} />
<TableRow label="Max Sent" value={config.maxSentData} />
<TableRow label="Max Recv" value={config.maxRecvData} />
{config.metadata && (
<TableRow
label="Metadata"
value={JSON.stringify(config.metadata)}
/>
)}
</tbody>
</table>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
You will be able to review and redact headers and response body.
</div>
</>
);
break;
case 'headers':
headerText = 'Step 1 of 2: Select headers to reveal';
primaryCta = 'Next';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col">
<RevealHeaderTable
className="w-full"
onChange={setRevealed}
headers={headerList}
/>
</div>
);
break;
case 'response':
headerText = 'Step 2 of 2: Highlight response to keep';
primaryCta = 'Notarize';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col flex-grow">
<RedactBodyTextarea
className="w-full "
onChange={setSecretResps}
request={{
url: config.url,
method: config.method,
headers: config.headers,
body: config.body,
formData: config.formData,
}}
/>
</div>
);
break;
}
return (
<BaseApproval
header={headerText}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
primaryCTAText={primaryCta}
secondaryCTAText={secondaryCta}
>
{body}
</BaseApproval>
);
}
function TableRow({ label, value }: { label: string; value: string }) {
return (
<tr>
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top text-left w-24">
{label}
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-slate-800 text-left">
<input
className="outline-0 flex-grow cursor-default w-full"
type="text"
value={value}
/>
</td>
</tr>
);
}

View File

@@ -1,408 +0,0 @@
import React, {
ReactElement,
useState,
useEffect,
useCallback,
MouseEvent,
} from 'react';
import {
set,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxSent,
getMaxRecv,
getNotaryApi,
getProxyApi,
getLoggingFilter,
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
getDeveloperMode,
DEVELOPER_MODE_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
NOTARY_API,
NOTARY_PROXY,
MAX_RECV,
MAX_SENT,
RENDEZVOUS_API,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
import { LoggingLevel } from 'tlsn-js';
import { version } from '../../../package.json';
import { getDBSize, resetDB } from '../../entries/Background/db';
export default function Options(): ReactElement {
const [notary, setNotary] = useState(NOTARY_API);
const [proxy, setProxy] = useState(NOTARY_PROXY);
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
const [developerMode, setDeveloperMode] = useState(false);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
const [advanced, setAdvanced] = useState(false);
const [showReloadModal, setShowReloadModal] = useState(false);
const [dbSize, setDbSize] = useState(0);
const [isCalculatingDbSize, setIsCalculatingDbSize] = useState(false);
useEffect(() => {
(async () => {
setIsCalculatingDbSize(true);
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
})();
}, []);
useEffect(() => {
(async () => {
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
setDeveloperMode(await getDeveloperMode());
})();
}, []);
useEffect(() => {
(async () => {
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || 'Info');
setRendezvous((await getRendezvousApi()) || RENDEZVOUS_API);
})();
}, [advanced]);
const onSave = useCallback(
async (e: MouseEvent<HTMLButtonElement>, skipCheck = false) => {
if (!skipCheck && shouldReload) {
setShowReloadModal(true);
return;
}
await set(NOTARY_API_LS_KEY, notary);
await set(PROXY_API_LS_KEY, proxy);
await set(MAX_SENT_LS_KEY, maxSent.toString());
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
await set(DEVELOPER_MODE_LS_KEY, developerMode.toString());
setDirty(false);
},
[
notary,
proxy,
maxSent,
maxReceived,
loggingLevel,
rendezvous,
developerMode,
shouldReload,
],
);
const onSaveAndReload = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
await onSave(e, true);
browser.runtime.reload();
},
[onSave],
);
const onAdvanced = useCallback(() => {
setAdvanced(!advanced);
}, [advanced]);
const openInTab = useCallback((url: string) => {
browser.tabs.create({ url });
}, []);
const onCleanCache = useCallback(async () => {
setIsCalculatingDbSize(true);
await resetDB();
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
{showReloadModal && (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={() => setShowReloadModal(false)}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
Modifying your logging your will require your extension to reload.
Do you want to proceed?
</ModalContent>
<div className="flex flex-row justify-end items-center gap-2 w-full">
<button
className="button"
onClick={() => setShowReloadModal(false)}
>
No
</button>
<button
className="button button--primary"
onClick={onSaveAndReload}
>
Yes
</button>
</div>
</Modal>
)}
<div className="flex flex-row flex-nowrap justify-between items-between py-1 px-2 gap-2">
<p className="font-bold text-base">Settings</p>
</div>
<NormalOptions
notary={notary}
setNotary={setNotary}
proxy={proxy}
setProxy={setProxy}
setDirty={setDirty}
developerMode={developerMode}
setDeveloperMode={setDeveloperMode}
/>
<div className="justify-left px-2 pt-3 gap-2">
<button className="font-bold" onClick={onAdvanced}>
<i
className={
advanced
? 'fa-solid fa-caret-down pr-1'
: 'fa-solid fa-caret-right pr-1'
}
></i>
Advanced
</button>
</div>
{!advanced ? (
<></>
) : (
<AdvancedOptions
maxSent={maxSent}
setMaxSent={setMaxSent}
maxReceived={maxReceived}
setMaxReceived={setMaxReceived}
setDirty={setDirty}
loggingLevel={loggingLevel}
setLoggingLevel={setLoggingLevel}
setShouldReload={setShouldReload}
rendezvous={rendezvous}
setRendezvous={setRendezvous}
/>
)}
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
disabled={!dirty}
onClick={onSave}
>
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>
<button className="button" onClick={onCleanCache}>
<span>Clean Cache (</span>
{isCalculatingDbSize ? (
<i className="fa-solid fa-spinner fa-spin"></i>
) : (
<span>{(dbSize / 1024 / 1024).toFixed(2)} MB</span>
)}
<span>)</span>
</button>
</div>
</div>
);
}
function InputField(props: {
label?: string;
placeholder?: string;
value?: string;
type?: string;
min?: number;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
const { label, placeholder, value, type, min, onChange } = props;
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold cursor-default">{label}</div>
<input
type={type}
className="input border"
onChange={onChange}
value={value}
min={min}
placeholder={placeholder}
/>
</div>
);
}
function NormalOptions(props: {
notary: string;
setNotary: (value: string) => void;
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
developerMode: boolean;
setDeveloperMode: (value: boolean) => void;
}) {
const {
notary,
setNotary,
proxy,
setProxy,
setDirty,
developerMode,
setDeveloperMode,
} = props;
return (
<div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
<div className="font-semibold">Version</div>
<div className="input border bg-slate-100">{version}</div>
</div>
<InputField
label="Notary API"
placeholder="https://api.tlsnotary.org"
value={notary}
type="text"
onChange={(e) => {
setNotary(e.target.value);
setDirty(true);
}}
/>
<InputField
label="Proxy API"
placeholder="https://proxy.tlsnotary.org"
value={proxy}
type="text"
onChange={(e) => {
setProxy(e.target.value);
setDirty(true);
}}
/>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
</div>
<div className="flex flex-row items-center py-3 px-2 gap-2">
<div className="font-semibold">Developer Mode</div>
<div className="relative inline-block w-9 h-5">
<input
type="checkbox"
id="developer-mode"
checked={developerMode}
onChange={(e) => {
setDeveloperMode(e.target.checked);
setDirty(true);
}}
className="sr-only"
/>
<label
htmlFor="developer-mode"
className={`block h-5 rounded-full cursor-pointer transition-all duration-300 ease-in-out ${
developerMode ? 'bg-blue-500' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-sm transform transition-all duration-300 ease-in-out ${
developerMode ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</label>
</div>
</div>
</div>
);
}
function AdvancedOptions(props: {
maxSent: number;
maxReceived: number;
loggingLevel: LoggingLevel;
rendezvous: string;
setShouldReload: (reload: boolean) => void;
setMaxSent: (value: number) => void;
setMaxReceived: (value: number) => void;
setDirty: (value: boolean) => void;
setLoggingLevel: (level: LoggingLevel) => void;
setRendezvous: (api: string) => void;
}) {
const {
maxSent,
setMaxSent,
maxReceived,
setMaxReceived,
setDirty,
setLoggingLevel,
loggingLevel,
setShouldReload,
rendezvous,
setRendezvous,
} = props;
return (
<div>
<InputField
label="Set Max Received Data"
value={maxReceived.toString()}
type="number"
min={0}
onChange={(e) => {
setMaxReceived(parseInt(e.target.value));
setDirty(true);
}}
/>
<InputField
label="Set Max Sent Data"
value={maxSent.toString()}
type="number"
min={0}
onChange={(e) => {
setMaxSent(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Logging Level</div>
<select
className="select !bg-white border !px-2 !py-1"
onChange={(e) => {
setLoggingLevel(e.target.value as LoggingLevel);
setDirty(true);
setShouldReload(true);
}}
value={loggingLevel}
>
<option value="Error">Error</option>
<option value="Warn">Warn</option>
<option value="Info">Info</option>
<option value="Debug">Debug</option>
<option value="Trace">Trace</option>
</select>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2"></div>
</div>
);
}

View File

@@ -1,497 +0,0 @@
import React, {
ChangeEvent,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import {
connectRendezvous,
disconnectRendezvous,
fetchP2PState,
sendPairRequest,
useClientId,
useIncomingPairingRequests,
useOutgoingPairingRequests,
cancelPairRequest,
useP2PError,
setP2PError,
acceptPairRequest,
rejectPairRequest,
usePairId,
useIncomingProofRequests,
requestProofByHash,
useOutgoingProofRequests,
acceptProofRequest,
rejectProofRequest,
cancelProofRequest,
useP2PProving,
useP2PVerifying,
useP2PPresentation,
} from '../../reducers/p2p';
import { useDispatch } from 'react-redux';
import Modal, { ModalHeader } from '../../components/Modal/Modal';
import { Plugin, PluginList } from '../../components/PluginList';
import browser from 'webextension-polyfill';
import { sha256 } from '../../utils/misc';
import { openSidePanel } from '../../entries/utils';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import ProofViewer from '../ProofViewer';
export function P2PHome(): ReactElement {
const clientId = useClientId();
useEffect(() => {
fetchP2PState();
}, []);
const toggleConnection = useCallback(async () => {
if (!clientId) {
connectRendezvous();
} else {
disconnectRendezvous();
}
}, [clientId]);
return (
<div className="flex flex-col h-full cursor-default gap-2 my-2">
<div className="flex flex-row border border-slate-300 rounded mx-2">
<div className="bg-slate-200 px-2 py-1 flex-grow-0 border-r border-slate-300">
Client ID
</div>
<input
className={classNames(
'flex-grow outline-0 px-2 py-1 cursor-default font-semibold',
{
'text-slate-400 bg-slate-100': !clientId,
'text-green-500 cursor-pointer': clientId,
},
)}
onClick={(e) => {
// @ts-ignore
if (e.target.select && clientId) e.target.select();
}}
value={clientId ? clientId : '--'}
readOnly
/>
<button
className="flex-grow-0 px-2 py-1 button border-l border-slate-300"
onClick={toggleConnection}
>
{clientId ? 'Stop' : 'Start'}
</button>
</div>
<ClientStatus />
<div className="flex flex-row mx-2 flex-grow flex-shrink h-0 p-2">
<div className="text-slate-400 text-center w-full font-semibold">
No proofs history
</div>
</div>
</div>
);
}
function ClientStatus() {
const clientId = useClientId();
const error = useP2PError();
const pairId = usePairId();
const [incomingPairingRequest] = useIncomingPairingRequests();
const [outgoingPairingRequest] = useOutgoingPairingRequests();
let body = null;
if (!clientId) {
body = <ClientNotStarted />;
} else if (pairId) {
body = <Paired />;
} else if (!incomingPairingRequest && !outgoingPairingRequest) {
body = <PendingConnection />;
} else if (incomingPairingRequest) {
body = <IncomingRequest />;
} else if (outgoingPairingRequest) {
body = <OutgoingRequest />;
}
return (
<div
className={classNames(
'flex flex-col items-center justify-center border border-slate-300',
'flex-grow-0 flex-shrink rounded mx-2 bg-slate-100 py-4 gap-4',
)}
>
{body}
{error && <span className="text-xs text-red-500">{error}</span>}
</div>
);
}
function Paired() {
const pairId = usePairId();
const clientId = useClientId();
const [incomingProofRequest] = useIncomingProofRequests();
const [outgoingPluginHash] = useOutgoingProofRequests();
const [incomingPluginHash, setIncomingPluginHash] = useState('');
const [showingModal, showModal] = useState(false);
const isProving = useP2PProving();
const isVerifying = useP2PVerifying();
const presentation = useP2PPresentation();
useEffect(() => {
(async () => {
if (!incomingProofRequest) {
setIncomingPluginHash('');
return;
}
const hash = await sha256(incomingProofRequest);
setIncomingPluginHash(hash);
})();
}, [incomingProofRequest]);
useEffect(() => {
showModal(false);
}, [outgoingPluginHash]);
const accept = useCallback(async () => {
if (incomingPluginHash) {
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.run_p2p_plugin_request,
data: {
pluginHash: incomingPluginHash,
plugin: incomingProofRequest,
},
});
acceptProofRequest(incomingPluginHash);
window.close();
}
}, [incomingPluginHash, incomingProofRequest, clientId]);
const reject = useCallback(() => {
if (incomingPluginHash) rejectProofRequest(incomingPluginHash);
}, [incomingPluginHash]);
const cancel = useCallback(() => {
if (outgoingPluginHash) cancelProofRequest(outgoingPluginHash);
}, [outgoingPluginHash]);
let body;
if (incomingPluginHash) {
body = (
<IncomingProof
incomingProofRequest={incomingProofRequest}
incomingPluginHash={incomingPluginHash}
accept={accept}
reject={reject}
isProving={isProving}
/>
);
} else if (outgoingPluginHash) {
body = (
<OutgoingProof
outgoingPluginHash={outgoingPluginHash}
cancel={cancel}
isVerifying={isVerifying}
/>
);
} else {
body = (
<button
className="button button--primary"
onClick={() => showModal(true)}
>
Request Proof
</button>
);
}
return (
<div className="flex flex-col items-center gap-2 px-4 w-full">
{showingModal && <PluginListModal onClose={() => showModal(false)} />}
<div>
<span>Paired with </span>
<span className="font-semibold text-blue-500">{pairId}</span>
</div>
{body}
</div>
);
}
function IncomingProof({
incomingPluginHash,
incomingProofRequest,
reject,
accept,
isProving,
}: {
incomingPluginHash: string;
incomingProofRequest: string;
reject: () => void;
accept: () => void;
isProving: boolean;
}) {
const presentation = useP2PPresentation();
const [showingTranscript, showTranscript] = useState(false);
if (isProving) {
return (
<>
{presentation && showingTranscript && (
<Modal
className="h-full m-0 rounded-none"
onClose={() => showTranscript(false)}
>
<ProofViewer
className="h-full"
sent={presentation.sent}
recv={presentation.recv}
/>
</Modal>
)}
<div className="font-semibold text-orange-500">
{presentation ? 'Proving Completed' : 'Proving to your peer...'}
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={incomingPluginHash}
hex={incomingProofRequest}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button
className="button button--primary"
onClick={() => showTranscript(true)}
disabled={!presentation}
>
View
</button>
</div>
</>
);
}
return (
<>
<div className="font-semibold text-orange-500">
Your peer is requesting the following proof:
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={incomingPluginHash}
hex={incomingProofRequest}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button className="button" onClick={reject}>
Decline
</button>
<button className="button button--primary" onClick={accept}>
Accept
</button>
</div>
</>
);
}
function OutgoingProof({
outgoingPluginHash,
cancel,
isVerifying,
}: {
isVerifying: boolean;
outgoingPluginHash: string;
cancel: () => void;
}) {
const presentation = useP2PPresentation();
const [showingTranscript, showTranscript] = useState(false);
if (isVerifying) {
return (
<>
{presentation && showingTranscript && (
<Modal
className="h-full m-0 rounded-none"
onClose={() => showTranscript(false)}
>
<ProofViewer
className="h-full"
sent={presentation.sent}
recv={presentation.recv}
/>
</Modal>
)}
<div className="font-semibold text-orange-500">
{presentation
? 'Verification Completed'
: 'Verifying with your peer...'}
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={outgoingPluginHash}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button
className="button button--primary"
onClick={() => showTranscript(true)}
disabled={!presentation}
>
View
</button>
</div>
</>
);
}
return (
<>
<div className="font-semibold text-orange-500">
Sent request for following proof:
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={outgoingPluginHash}
onClick={() => null}
unremovable
/>
<button className="button" onClick={cancel}>
Cancel
</button>
</>
);
}
function PluginListModal({ onClose }: { onClose: () => void }) {
const onRequestProof = useCallback(async (hash: string) => {
requestProofByHash(hash);
}, []);
return (
<Modal className="mx-4" onClose={onClose}>
<ModalHeader onClose={onClose}>Choose a plugin to continue</ModalHeader>
<PluginList className="m-2" onClick={onRequestProof} unremovable />
</Modal>
);
}
function IncomingRequest() {
const [incomingRequest] = useIncomingPairingRequests();
const accept = useCallback(() => {
if (incomingRequest) acceptPairRequest(incomingRequest);
}, [incomingRequest]);
const reject = useCallback(() => {
if (incomingRequest) rejectPairRequest(incomingRequest);
}, [incomingRequest]);
return (
<div className="flex flex-col items-center gap-2">
<div>
<span className="font-semibold text-blue-500">{incomingRequest}</span>
<span> wants to pair with you.</span>
</div>
<div className="flex flex-row gap-2">
<button className="button" onClick={reject}>
Decline
</button>
<button className="button button--primary" onClick={accept}>
Accept
</button>
</div>
</div>
);
}
function OutgoingRequest() {
const [outgoingRequest] = useOutgoingPairingRequests();
const cancel = useCallback(() => {
if (outgoingRequest) {
cancelPairRequest(outgoingRequest);
}
}, [outgoingRequest]);
return (
<div className="flex flex-col items-center gap-2">
<span className="flex flex-row items-center gap-2 mx-2">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
<span>
<span>Awaiting response from </span>
<span className="font-semibold text-blue-500">{outgoingRequest}</span>
<span>...</span>
</span>
</span>
<button className="button" onClick={cancel}>
Cancel
</button>
</div>
);
}
function ClientNotStarted() {
return (
<div className="flex flex-col text-slate-500 font-semibold gap-2">
Client has not started
<button className="button button--primary" onClick={connectRendezvous}>
Start Client
</button>
</div>
);
}
function PendingConnection() {
const dispatch = useDispatch();
const [target, setTarget] = useState('');
const onSend = useCallback(() => {
dispatch(setP2PError(''));
sendPairRequest(target);
}, [target]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
dispatch(setP2PError(''));
setTarget(e.target.value);
}, []);
return (
<div className="flex flex-col w-full items-center gap-2">
<div className="flex flex-row justify-center gap-2">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
<div className="text-slate-500 font-semibold">
Waiting for pairing request...
</div>
</div>
<div className="text-slate-500">or</div>
<div className="w-full flex flex-row px-2 items-center">
<input
className="flex-grow flex-shrink w-0 outline-0 px-2 py-1 cursor-default"
placeholder="Enter Peer ID to send pairing request"
onChange={onChange}
value={target}
/>
<button
className="button button--primary w-fit h-full"
onClick={onSend}
>
Send Pairing Request
</button>
</div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
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>
)
}

View File

@@ -1,104 +0,0 @@
import React, {
ReactElement,
useState,
useCallback,
ChangeEventHandler,
} from 'react';
import Icon from '../../components/Icon';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import ProofViewer from '../ProofViewer';
import { convertNotaryWsToHttp } from '../../utils/misc';
export default function ProofUploader(): ReactElement {
const [proof, setProof] = useState<{
recv: string;
sent: string;
verifierKey?: string;
notaryKey?: string;
} | null>(null);
const [uploading, setUploading] = useState(false);
const [metadata, setMetaData] = useState<any>({ meta: '', version: '' });
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
async (e) => {
// @ts-ignore
const [file] = e.target.files || [];
if (file) {
const reader = new FileReader();
reader.addEventListener('load', async (event) => {
const result = event.target?.result;
if (result) {
const proof = JSON.parse(result as string);
const notaryUrl = convertNotaryWsToHttp(proof.meta.notaryUrl);
proof.meta.notaryUrl = notaryUrl;
setMetaData({ meta: proof.meta, version: proof.version });
const res = await chrome.runtime
.sendMessage<
any,
{
recv: string;
sent: string;
verifierKey?: string;
notaryKey?: string;
}
>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
.catch(() => null);
if (proof) {
setUploading(false);
setProof(res);
}
}
});
setUploading(true);
reader.readAsText(file);
}
},
[],
);
if (proof) {
return (
<ProofViewer
recv={proof.recv}
sent={proof.sent}
verifierKey={proof.verifierKey}
notaryKey={proof.notaryKey}
info={metadata}
/>
);
}
return (
<div className="flex flex-col flex-nowrap flex-grow flex-shrink h-0 overflow-y-auto">
<div className="flex flex-col items-center justify-center relative border-slate-400 border-2 text-slate-500 border-dashed flex-grow flex-shrink h-0 m-2 bg-slate-200">
<input
type="file"
className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
onChange={onFileUpload}
accept=".json"
disabled={uploading}
/>
{uploading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
) : (
<>
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
<div className="text-lg">Drop your proof here to continue</div>
<div className="text-sm">or</div>
<button
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white cursor-pointer"
onClick={() => null}
>
Browse Files
</button>
</>
)}
</div>
</div>
);
}

View File

@@ -1,232 +0,0 @@
import React, {
ReactNode,
ReactElement,
useState,
useEffect,
MouseEventHandler,
useCallback,
} from 'react';
import { useParams, useNavigate } from 'react-router';
import c from 'classnames';
import {
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import {
convertNotaryWsToHttp,
download,
isPopupWindow,
} from '../../utils/misc';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import { RemoveHistory } from '../History/request-menu';
import { PresentationJSON } from 'tlsn-js/build/types';
import { RequestHistory } from '../../entries/Background/rpc';
export default function ProofViewer(props?: {
className?: string;
recv?: string;
sent?: string;
verifierKey?: string;
notaryKey?: string;
info?: {
meta: { notaryUrl: string; websocketProxyUrl: string };
version: string;
};
}): ReactElement {
const dispatch = useDispatch();
const { requestId } = useParams<{ requestId: string }>();
const request = useRequestHistory(requestId);
const navigate = useNavigate();
const [tab, setTab] = useState('sent');
const [isPopup, setIsPopup] = useState(isPopupWindow());
const [showRemoveModal, setShowRemoveModal] = useState(false);
const onDelete = useCallback(async () => {
if (requestId) {
dispatch(deleteRequestHistory(requestId));
if (isPopup) window.close();
navigate(-1);
}
}, [requestId]);
const notaryUrl = extractFromProps('notaryUrl', props, request);
const websocketProxyUrl = extractFromProps(
'websocketProxyUrl',
props,
request,
);
return (
<div
className={classNames(
'flex flex-col w-full py-2 gap-2 flex-grow',
props?.className,
)}
>
<RemoveHistory
onRemove={onDelete}
showRemovalModal={showRemoveModal}
setShowRemoveModal={setShowRemoveModal}
onCancel={() => setShowRemoveModal(false)}
/>
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2 items-center">
{!isPopup && (
<Icon
className={c(
'px-1 select-none cursor-pointer',
'text-slate-400 border-b-2 border-transparent hover:text-slate-500 active:text-slate-800',
)}
onClick={() => navigate(-1)}
fa="fa-solid fa-xmark"
/>
)}
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
Sent
</TabLabel>
<TabLabel onClick={() => setTab('recv')} active={tab === 'recv'}>
Recv
</TabLabel>
<TabLabel
onClick={() => setTab('metadata')}
active={tab === 'metadata'}
>
Metadata
</TabLabel>
<div className="flex flex-row flex-grow items-center justify-end">
{!props?.recv && (
<button
className="button"
onClick={() => {
if (!request) return;
download(request.id, JSON.stringify(request.proof));
}}
>
Download
</button>
)}
<button
className="button !text-red-500"
onClick={() => setShowRemoveModal(true)}
>
Delete
</button>
</div>
</div>
</div>
<div className="flex flex-col flex-grow px-2">
{tab === 'sent' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={props?.sent || request?.verification?.sent}
readOnly
></textarea>
)}
{tab === 'recv' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={props?.recv || request?.verification?.recv}
readOnly
></textarea>
)}
{tab === 'metadata' && (
<div className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono">
<MetadataRow
label="Version"
//@ts-ignore
value={props?.info?.version || request?.proof?.version}
/>
<MetadataRow label="Notary URL" value={notaryUrl} />
<MetadataRow
label="Websocket Proxy URL"
value={websocketProxyUrl}
/>
<MetadataRow
label="Verifying Key"
value={props?.verifierKey || request?.verification?.verifierKey}
/>
<MetadataRow
label="Notary Key"
value={props?.notaryKey || request?.verification?.notaryKey}
/>
{request?.metadata &&
Object.entries(request.metadata).map(([key, value]) => (
<MetadataRow
key={`req-${key}`}
label={`Custom: ${key}`}
value={String(value)}
/>
))}
</div>
)}
</div>
</div>
);
}
function extractFromProps(
key: 'notaryUrl' | 'websocketProxyUrl',
props?: {
className?: string;
recv?: string;
sent?: string;
verifierKey?: string;
notaryKey?: string;
info?: {
meta: { notaryUrl: string; websocketProxyUrl: string };
version: string;
};
},
request?: RequestHistory,
) {
let value;
if (props?.info?.meta) {
value = props.info.meta[key];
} else if (request && (request?.proof as PresentationJSON)?.meta) {
value = (request.proof as PresentationJSON).meta[key];
} else {
value = '';
}
return value;
}
function TabLabel(props: {
children: ReactNode;
onClick: MouseEventHandler;
active?: boolean;
}): ReactElement {
return (
<button
className={c('px-1 select-none cursor-pointer font-bold', {
'text-slate-800 border-b-2 border-green-500': props.active,
'text-slate-400 border-b-2 border-transparent hover:text-slate-500':
!props.active,
})}
onClick={props.onClick}
>
{props.children}
</button>
);
}
function MetadataRow({
label,
value,
}: {
label: string;
value: string | undefined;
}) {
return (
<div>
<div>{label}:</div>
<div className="text-sm font-semibold whitespace-pre-wrap">
{value || 'N/A'}
</div>
</div>
);
}

View File

@@ -1,482 +0,0 @@
import c from 'classnames';
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import NavigateWithParams from '../../components/NavigateWithParams';
import ResponseDetail from '../../components/ResponseDetail';
import { urlify } from '../../utils/misc';
import { notarizeRequest } from '../../reducers/requests';
import {
getMaxRecv,
getMaxSent,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
import {
formatForRequest,
InputBody,
FormBodyTable,
parseResponse,
} from '../../components/RequestBuilder';
enum TabType {
Params = 'Params',
Headers = 'Headers',
Body = 'Body',
}
export default function RequestBuilder(props?: {
subpath?: string;
url?: string;
params?: [string, string, boolean?][];
headers?: [string, string, boolean?][];
body?: string;
method?: string;
}): ReactElement {
const loc = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const subpath = props?.subpath || '/custom';
const [_url, setUrl] = useState(props?.url || '');
const [params, setParams] = useState<[string, string, boolean?][]>(
props?.params || [],
);
const [body, setBody] = useState<string | undefined>(props?.body);
const [formBody, setFormBody] = useState<[string, string, boolean?][]>([
['', '', true],
]);
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]],
);
const [responseData, setResponseData] = useState<{
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null>(null);
const url = urlify(_url);
const href = !url
? ''
: urlify(
`${url.origin}${url.pathname}`,
params.filter(([, , silent]) => !silent),
)?.href;
useEffect(() => {
setParams(Array.from(url?.searchParams || []));
}, [_url]);
useEffect(() => {
updateContentType(type);
}, [type, method]);
const updateContentType = useCallback(
(type: string) => {
const updateHeaders = headers.filter(
([key]) => key.toLowerCase() !== 'content-type',
);
if (method === 'GET' || method === 'HEAD') {
updateHeaders.push(['Content-Type', type, true]);
} else {
updateHeaders.push(['Content-Type', type, false]);
}
setHeaders(updateHeaders);
},
[method, type, headers],
);
const toggleParam = useCallback(
(i: number) => {
params[i][2] = !params[i][2];
setParams([...params]);
},
[params],
);
const setParam = useCallback(
(index: number, key: string, value: string) => {
params[index] = [key, value];
setParams([...params]);
},
[params],
);
const toggleHeader = useCallback(
(i: number) => {
headers[i][2] = !headers[i][2];
setHeaders([...headers]);
},
[headers],
);
const setHeader = useCallback(
(index: number, key: string, value: string) => {
headers[index] = [key, value];
setHeaders([...headers]);
},
[headers],
);
const sendRequest = useCallback(async () => {
if (!href) return;
setResponseData(null);
// eslint-disable-next-line no-undef
const opts: RequestInit = {
method,
headers: headers.reduce((map: { [key: string]: string }, [k, v]) => {
if (k !== 'Cookie') {
map[k] = v;
}
return map;
}, {}),
};
if (method !== 'GET' && method !== 'HEAD') {
if (type === 'application/x-www-form-urlencoded') {
opts.body = formatForRequest(formBody, type);
} else {
opts.body = formatForRequest(body!, type);
}
}
const cookie = headers.find(([key]) => key === 'Cookie');
if (cookie) {
opts.credentials = 'include';
document.cookie = cookie[1];
}
const res = await fetch(href, opts);
const contentType =
res.headers.get('content-type') || res.headers.get('Content-Type');
setResponseData(await parseResponse(contentType!, res));
navigate(subpath + '/response');
}, [href, method, headers, body, type]);
const onNotarize = useCallback(async () => {
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
dispatch(
notarizeRequest(
//@ts-ignore
{
url: href || '',
method,
headers: headers.reduce((map: { [key: string]: string }, [k, v]) => {
if (k !== 'Cookie') {
map[k] = v;
}
return map;
}, {}),
body: body ? formatForRequest(body, type) : undefined,
maxSentData,
maxRecvData,
secretHeaders: [],
secretResps: [],
notaryUrl,
websocketProxyUrl,
},
),
);
navigate('/history');
}, [href, method, headers, body, type]);
const onMethod = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value === 'GET' || value === 'HEAD') {
setType('');
setMethod(value);
} else {
setMethod(value);
}
},
[method, type],
);
return (
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
<div className="flex flex-row px-2">
<select className="select" onChange={(e) => onMethod(e)}>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
<input
className="input border flex-grow"
type="text"
value={_url}
onChange={(e) => setUrl(e.target.value)}
onBlur={() => {
const formattedUrl = urlify(_url);
if (formattedUrl) {
setUrl(formattedUrl.href);
}
}}
/>
<button className="button" disabled={!url} onClick={sendRequest}>
Send
</button>
</div>
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2">
<TabLabel
onClick={() => navigate(subpath + '/params')}
active={loc.pathname.includes('params')}
>
Params
</TabLabel>
<TabLabel
onClick={() => navigate(subpath + '/headers')}
active={loc.pathname.includes('headers')}
>
Headers
</TabLabel>
<TabLabel
onClick={() => navigate(subpath + '/body')}
active={loc.pathname.includes('body')}
>
Body
</TabLabel>
{responseData && (
<div className="flex flex-row justify-between w-full">
<TabLabel
onClick={() => navigate(subpath + '/response')}
active={loc.pathname.includes('response')}
>
Response
</TabLabel>
<button className="button" onClick={onNotarize}>
Notarize
</button>
</div>
)}
</div>
</div>
<div className="h-0 flex-grow overflow-y-auto px-2">
<Routes>
<Route
path="params"
element={
<ParamTable
url={url}
toggleParam={toggleParam}
setParam={setParam}
params={params}
/>
}
/>
<Route
path="headers"
element={
<HeaderTable
toggleHeader={toggleHeader}
setHeader={setHeader}
headers={headers}
/>
}
/>
<Route
path="body"
element={
<div className="h-full">
<select
className={c('select', {
'w-[80px]':
type === 'application/json' ||
type === 'text/plain' ||
type === '',
'w-[200px]': type === 'application/x-www-form-urlencoded',
})}
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="text/plain">Text</option>
<option value="application/json">JSON</option>
<option value="application/x-www-form-urlencoded">
x-www-form-urlencoded
</option>
</select>
{type === 'application/x-www-form-urlencoded' ? (
<FormBodyTable
formBody={formBody}
setFormBody={setFormBody}
/>
) : (
<InputBody body={body!} setBody={setBody} />
)}
</div>
}
/>
<Route
path="response"
element={<ResponseDetail responseData={responseData} />}
/>
<Route path="/" element={<NavigateWithParams to="/params" />} />
</Routes>
</div>
</div>
);
}
function ParamTable(props: {
url: URL | null;
toggleParam: (i: number) => void;
setParam: (index: number, key: string, value: string) => void;
params: [string, string, boolean?][];
}): ReactElement {
const params: [string, string, boolean?][] = [
...props.params,
['', '', true],
];
const last = props.params.length;
return (
<table className="border border-slate-300 border-collapse table-fixed w-full">
<tbody>
{params.map(([key, value, silent], i) => (
<tr
key={i}
className={c('border-b border-slate-200', {
'opacity-30': !!silent,
})}
>
<td className="w-8 text-center pt-2">
{last !== i && (
<input
type="checkbox"
onChange={() => props.toggleParam(i)}
checked={!silent}
/>
)}
</td>
<td className="border border-slate-300 font-bold align-top break-all w-fit">
<input
className="input py-1 px-2 w-full py-1 px-2"
type="text"
value={key}
placeholder="Key"
onChange={(e) => {
props.setParam(i, e.target.value, value);
}}
/>
</td>
<td className="border border-slate-300 break-all align-top break-all">
<input
className="input py-1 px-2 w-full py-1 px-2"
type="text"
value={value}
placeholder="Value"
onChange={(e) => {
props.setParam(i, key, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
);
}
function HeaderTable(props: {
toggleHeader: (i: number) => void;
setHeader: (index: number, key: string, value: string) => void;
headers: [string, string, boolean?][];
}): ReactElement {
const headers: [string, string, boolean?][] = [
...props.headers,
['', '', true],
];
const last = props.headers.length;
return (
<table className="border border-slate-300 border-collapse table-fixed w-full">
<tbody>
{headers.map(([key, value, silent], i) => (
<tr
key={i}
className={c('border-b border-slate-200', {
'opacity-30': !!silent,
})}
>
<td className="w-8 text-center pt-2">
{last !== i && (
<input
type="checkbox"
onChange={() => props.toggleHeader(i)}
checked={!silent}
/>
)}
</td>
<td className="border border-slate-300 font-bold align-top break-all w-fit">
<input
className="input py-1 px-2 w-full py-1 px-2"
type="text"
value={key}
placeholder="Key"
onChange={(e) => {
props.setHeader(i, e.target.value, value);
}}
/>
</td>
<td className="border border-slate-300 break-all align-top break-all">
<input
className="input py-1 px-2 w-full py-1 px-2"
type="text"
value={value}
placeholder="Value"
onChange={(e) => {
props.setHeader(i, key, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
);
}
function TabLabel(props: {
children: ReactNode;
onClick: MouseEventHandler;
active?: boolean;
}): ReactElement {
return (
<button
className={c('px-1 select-none cursor-pointer font-bold', {
'text-slate-800 border-b-2 border-green-500': props.active,
'text-slate-400 border-b-2 border-transparent': !props.active,
})}
onClick={props.onClick}
>
{props.children}
</button>
);
}

View File

@@ -1,11 +0,0 @@
import React, { ReactElement } from 'react';
import RequestDetail from '../../components/RequestDetail';
import { useParams } from 'react-router';
export default function Request(): ReactElement {
const params = useParams<{ requestId: string }>();
return (
<>{!!params.requestId && <RequestDetail requestId={params.requestId} />}</>
);
}

View File

@@ -1,12 +0,0 @@
import React, { ReactElement } from 'react';
import RequestTable from '../../components/RequestTable';
import { useRequests } from '../../reducers/requests';
export default function Requests(props: { shouldFix?: boolean }): ReactElement {
const requests = useRequests();
return (
<>
<RequestTable shouldFix={props.shouldFix} requests={requests} />
</>
);
}

View File

@@ -1,148 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { type PluginConfig, PluginMetadata, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
import {
getPluginConfigByUrl,
getPluginMetadataByUrl,
getPluginByUrl,
} from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { deferredPromise } from '../../utils/promise';
import { installPlugin } from '../../entries/Background/plugins/utils';
export function RunPluginByUrlApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const pluginParams = params.get('params');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
useEffect(() => {
if (!url) return;
(async () => {
try {
const hex = await getPluginByUrl(url);
if (!hex) {
await installPlugin(url);
}
const config = await getPluginConfigByUrl(url);
const metadata = await getPluginMetadataByUrl(url);
setPluginContent(config);
setPluginMetadata(metadata);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
})();
}, [url]);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!url) return;
try {
const tab = await browser.tabs.create({
active: true,
});
const { promise, resolve } = deferredPromise();
const listener = async (request: any) => {
if (request.type === SidePanelActionTypes.panel_opened) {
browser.runtime.onMessage.removeListener(listener);
resolve();
}
};
browser.runtime.onMessage.addListener(listener);
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
await promise;
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginUrl: url,
pluginParams: pluginParams ? JSON.parse(pluginParams) : undefined,
},
});
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [url]);
return (
<BaseApproval
header={`Execute Plugin`}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
<b className="text-blue-500">{hostname}</b> wants to execute a plugin:
</div>
</div>
{!pluginContent && (
<div className="flex flex-col items-center flex-grow gap-4 border border-slate-300 p-8 mx-8 rounded bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
)}
{pluginContent && (
<div className="flex flex-col gap-4 border border-slate-300 p-4 mx-8 rounded bg-slate-100">
<div className="flex flex-col items-center">
<img
className="w-12 h-12 mb-2"
src={pluginContent.icon}
alt="Plugin Icon"
/>
<span className="text-2xl text-blue-600 font-semibold">
{pluginContent.title}
</span>
<div className="text-slate-500 text-base">
{pluginContent.description}
</div>
</div>
</div>
)}
</BaseApproval>
);
}

View File

@@ -1,161 +0,0 @@
import {
BackgroundActiontype,
RequestHistory,
RequestProgress,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
enum ActionType {
'/history/addRequest' = '/history/addRequest',
'/history/setRequests' = '/history/setRequests',
'/history/deleteRequest' = '/history/deleteRequest',
'/history/addRequestCid' = '/history/addRequestCid',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
map: {
[requestId: string]: RequestHistory;
};
order: string[];
};
const initialState: State = {
map: {},
order: [],
};
export const addRequestHistory = (request?: RequestHistory | null) => {
return {
type: ActionType['/history/addRequest'],
payload: request,
};
};
export const setRequests = (requests: RequestHistory[]) => {
return {
type: ActionType['/history/setRequests'],
payload: requests,
};
};
export const addRequestCid = (requestId: string, cid: string) => {
return {
type: ActionType['/history/addRequestCid'],
payload: { requestId, cid },
};
};
export const deleteRequestHistory = (id: string) => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.delete_prove_request,
data: id,
});
return {
type: ActionType['/history/deleteRequest'],
payload: id,
};
};
export default function history(
state = initialState,
action: Action<any>,
): State {
switch (action.type) {
case ActionType['/history/addRequest']: {
const payload: RequestHistory = action.payload;
if (!payload) return state;
const existing = state.map[payload.id];
if (existing?.progress === RequestProgress.Error) {
return state;
}
const newMap = {
...state.map,
[payload.id]: payload,
};
const newOrder = existing ? state.order : state.order.concat(payload.id);
return {
...state,
map: newMap,
order: newOrder,
};
}
case ActionType['/history/setRequests']: {
const payload: RequestHistory[] = action.payload;
const newMap = payload.reduce(
(map: { [id: string]: RequestHistory }, req) => {
if (state.map[req.id]?.progress === RequestProgress.Error) {
map[req.id] = state.map[req.id];
} else {
map[req.id] = req;
}
return map;
},
{},
);
return {
...state,
map: newMap,
order: payload.map(({ id }) => id),
};
}
case ActionType['/history/deleteRequest']: {
const reqId: string = action.payload;
const newMap = { ...state.map };
delete newMap[reqId];
const newOrder = state.order.filter((id) => id !== reqId);
return {
...state,
map: newMap,
order: newOrder,
};
}
case ActionType['/history/addRequestCid']: {
const { requestId, cid } = action.payload;
if (!state.map[requestId]) return state;
return {
...state,
map: {
...state.map,
[requestId]: {
...state.map[requestId],
cid,
},
},
};
}
default:
return state;
}
}
export const useHistoryOrder = (): string[] => {
return useSelector((state: AppRootState) => {
return state.history.order;
}, deepEqual);
};
export const useAllProofHistory = (): RequestHistory[] => {
return useSelector((state: AppRootState) => {
return state.history.order.map((id) => state.history.map[id]);
}, deepEqual);
};
export const useRequestHistory = (id?: string): RequestHistory | undefined => {
return useSelector((state: AppRootState) => {
if (!id) return undefined;
return state.history.map[id];
}, deepEqual);
};

View File

@@ -1,15 +1,47 @@
import { combineReducers } from 'redux';
import requests from './requests';
import history from './history';
import plugins from './plugins';
import p2p from './p2p';
const rootReducer = combineReducers({
requests,
history,
plugins,
p2p,
// Basic app reducer
interface AppState {
message: string;
count: number;
}
const initialAppState: AppState = {
message: 'Welcome to the extension!',
count: 0,
};
// Action types
const SET_MESSAGE = 'SET_MESSAGE';
const INCREMENT_COUNT = 'INCREMENT_COUNT';
// Action creators
export const setMessage = (message: string) => ({
type: SET_MESSAGE,
payload: message,
});
export type AppRootState = ReturnType<typeof rootReducer>;
export default rootReducer;
export const incrementCount = () => ({
type: INCREMENT_COUNT,
});
// App reducer
const appReducer = (state = initialAppState, action: any): AppState => {
switch (action.type) {
case SET_MESSAGE:
return { ...state, message: action.payload };
case INCREMENT_COUNT:
return { ...state, count: state.count + 1 };
default:
return state;
}
};
// Root reducer
const rootReducer = combineReducers({
app: appReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export type AppRootState = RootState; // For backward compatibility
export default rootReducer;

View File

@@ -1,375 +0,0 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
enum ActionType {
'/p2p/setConnected' = '/p2p/setConnected',
'/p2p/setClientId' = '/p2p/setClientId',
'/p2p/setPairing' = '/p2p/setPairing',
'/p2p/setError' = '/p2p/setError',
'/p2p/appendIncomingPairingRequest' = '/p2p/appendIncomingPairingRequest',
'/p2p/appendOutgoingPairingRequest' = '/p2p/appendOutgoingPairingRequest',
'/p2p/setIncomingPairingRequest' = '/p2p/setIncomingPairingRequest',
'/p2p/setOutgoingPairingRequest' = '/p2p/setOutgoingPairingRequest',
'/p2p/appendIncomingProofRequest' = '/p2p/appendIncomingProofRequest',
'/p2p/appendOutgoingProofRequest' = '/p2p/appendOutgoingProofRequest',
'/p2p/setIncomingProofRequest' = '/p2p/setIncomingProofRequest',
'/p2p/setOutgoingProofRequest' = '/p2p/setOutgoingProofRequest',
'/p2p/setIsProving' = '/p2p/setIsProving',
'/p2p/setIsVerifying' = '/p2p/setIsVerifying',
'/p2p/setPresentation' = '/p2p/setPresentation',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
clientId: string;
pairing: string;
connected: boolean;
error: string;
incomingPairingRequests: string[];
outgoingPairingRequests: string[];
incomingProofRequests: string[];
outgoingProofRequests: string[];
isProving: boolean;
isVerifying: boolean;
presentation: null | {
sent: string;
recv: string;
};
};
export type RequestProofMessage = {
to: string;
from: string;
id: number;
text?: undefined;
};
const initialState: State = {
clientId: '',
pairing: '',
error: '',
connected: false,
incomingPairingRequests: [],
outgoingPairingRequests: [],
incomingProofRequests: [],
outgoingProofRequests: [],
isProving: false,
isVerifying: false,
presentation: null,
};
export const fetchP2PState = async () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_p2p_state,
});
};
export const connectRendezvous = () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.connect_rendezvous,
});
};
export const disconnectRendezvous = () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.disconnect_rendezvous,
});
};
export const setConnected = (connected = false) => ({
type: ActionType['/p2p/setConnected'],
payload: connected,
});
export const setClientId = (clientId: string) => ({
type: ActionType['/p2p/setClientId'],
payload: clientId,
});
export const setPairing = (clientId: string) => ({
type: ActionType['/p2p/setPairing'],
payload: clientId,
});
export const appendIncomingPairingRequests = (peerId: string) => ({
type: ActionType['/p2p/appendIncomingPairingRequest'],
payload: peerId,
});
export const appendIncomingProofRequests = (peerId: string) => ({
type: ActionType['/p2p/appendIncomingProofRequest'],
payload: peerId,
});
export const appendOutgoingPairingRequests = (peerId: string) => ({
type: ActionType['/p2p/appendOutgoingPairingRequest'],
payload: peerId,
});
export const appendOutgoingProofRequest = (peerId: string) => ({
type: ActionType['/p2p/appendOutgoingProofRequest'],
payload: peerId,
});
export const setIncomingPairingRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setIncomingPairingRequest'],
payload: peerIds,
});
export const setOutgoingPairingRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setOutgoingPairingRequest'],
payload: peerIds,
});
export const setIncomingProofRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setIncomingProofRequest'],
payload: peerIds,
});
export const setOutgoingProofRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setOutgoingProofRequest'],
payload: peerIds,
});
export const setP2PError = (error: string) => ({
type: ActionType['/p2p/setError'],
payload: error,
});
export const setIsProving = (proving: boolean) => ({
type: ActionType['/p2p/setIsProving'],
payload: proving,
});
export const setIsVerifying = (verifying: boolean) => ({
type: ActionType['/p2p/setIsVerifying'],
payload: verifying,
});
export const setP2PPresentation = (
presentation: null | { sent: string; recv: string },
) => ({
type: ActionType['/p2p/setPresentation'],
payload: presentation,
});
export const requestProofByHash = (pluginHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.request_p2p_proof_by_hash,
data: pluginHash,
});
};
export const sendPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.send_pair_request,
data: targetId,
});
};
export const cancelPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.cancel_pair_request,
data: targetId,
});
};
export const acceptPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.accept_pair_request,
data: targetId,
});
};
export const rejectPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.reject_pair_request,
data: targetId,
});
};
export const cancelProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.cancel_proof_request,
data: plughinHash,
});
};
export const acceptProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.accept_proof_request,
data: plughinHash,
});
};
export const rejectProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.reject_proof_request,
data: plughinHash,
});
};
export default function p2p(state = initialState, action: Action<any>): State {
switch (action.type) {
case ActionType['/p2p/setConnected']:
return {
...state,
connected: action.payload,
};
case ActionType['/p2p/setClientId']:
return {
...state,
clientId: action.payload,
};
case ActionType['/p2p/setPairing']:
return {
...state,
pairing: action.payload,
};
case ActionType['/p2p/appendIncomingPairingRequest']:
return {
...state,
incomingPairingRequests: [
...new Set(state.incomingPairingRequests.concat(action.payload)),
],
};
case ActionType['/p2p/appendOutgoingPairingRequest']:
return {
...state,
outgoingPairingRequests: [
...new Set(state.outgoingPairingRequests.concat(action.payload)),
],
};
case ActionType['/p2p/setIncomingPairingRequest']:
return {
...state,
incomingPairingRequests: action.payload,
};
case ActionType['/p2p/setOutgoingPairingRequest']:
return {
...state,
outgoingPairingRequests: action.payload,
};
case ActionType['/p2p/appendIncomingProofRequest']:
return {
...state,
incomingProofRequests: [
...new Set(state.incomingProofRequests.concat(action.payload)),
],
};
case ActionType['/p2p/appendOutgoingProofRequest']:
return {
...state,
outgoingProofRequests: [
...new Set(state.outgoingProofRequests.concat(action.payload)),
],
};
case ActionType['/p2p/setIncomingProofRequest']:
return {
...state,
incomingProofRequests: action.payload,
};
case ActionType['/p2p/setOutgoingProofRequest']:
return {
...state,
outgoingProofRequests: action.payload,
};
case ActionType['/p2p/setError']:
return {
...state,
error: action.payload,
};
case ActionType['/p2p/setIsProving']:
return {
...state,
isProving: action.payload,
};
case ActionType['/p2p/setIsVerifying']:
return {
...state,
isVerifying: action.payload,
};
case ActionType['/p2p/setPresentation']:
return {
...state,
presentation: action.payload,
};
default:
return state;
}
}
export function useClientId() {
return useSelector((state: AppRootState) => {
return state.p2p.clientId;
}, deepEqual);
}
export function useConnected() {
return useSelector((state: AppRootState) => {
return state.p2p.connected;
}, deepEqual);
}
export function usePairId(): string {
return useSelector((state: AppRootState) => {
return state.p2p.pairing;
}, deepEqual);
}
export function useIncomingPairingRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.incomingPairingRequests;
}, deepEqual);
}
export function useOutgoingPairingRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.outgoingPairingRequests;
}, deepEqual);
}
export function useIncomingProofRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.incomingProofRequests;
}, deepEqual);
}
export function useOutgoingProofRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.outgoingProofRequests;
}, deepEqual);
}
export function useP2PError(): string {
return useSelector((state: AppRootState) => {
return state.p2p.error;
}, deepEqual);
}
export function useP2PVerifying(): boolean {
return useSelector((state: AppRootState) => {
return state.p2p.isVerifying;
}, deepEqual);
}
export function useP2PProving(): boolean {
return useSelector((state: AppRootState) => {
return state.p2p.isProving;
}, deepEqual);
}
export function useP2PPresentation(): null | { sent: string; recv: string } {
return useSelector((state: AppRootState) => {
return state.p2p.presentation;
}, deepEqual);
}

View File

@@ -1,67 +0,0 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { useEffect, useState } from 'react';
import { getPluginConfigByUrl } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
'/plugin/removePlugin' = '/plugin/removePlugin',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
order: string[];
};
const initState: State = {
order: [],
};
export const addOnePlugin = (hash: string): Action<string> => ({
type: ActionType['/plugin/addPlugin'],
payload: hash,
});
export const removeOnePlugin = (hash: string): Action<string> => ({
type: ActionType['/plugin/removePlugin'],
payload: hash,
});
export default function plugins(state = initState, action: Action<any>): State {
switch (action.type) {
case ActionType['/plugin/addPlugin']:
return {
order: [...new Set(state.order.concat(action.payload))],
};
case ActionType['/plugin/removePlugin']:
return {
order: state.order.filter((h) => h !== action.payload),
};
default:
return state;
}
}
export const usePluginHashes = (): string[] => {
return useSelector((state: AppRootState) => {
return state.plugins.order;
}, deepEqual);
};
export const usePluginConfig = (hash: string) => {
const [config, setConfig] = useState<PluginConfig | null>(null);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByUrl(hash));
})();
}, [hash]);
return config;
};

View File

@@ -1,164 +0,0 @@
import {
type RequestLog,
type RequestHistory,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import {
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../utils/storage';
import { BackgroundActiontype } from '../entries/Background/rpc';
import browser from 'webextension-polyfill';
enum ActionType {
'/requests/setRequests' = '/requests/setRequests',
'/requests/addRequest' = '/requests/addRequest',
'/requests/setActiveTab' = '/requests/setActiveTab',
'/requests/isConnected' = '/requests/isConnected',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
map: {
[requestId: string]: RequestLog;
};
activeTab: chrome.tabs.Tab | null;
isConnected: boolean;
};
const initialState: State = {
map: {},
activeTab: null,
isConnected: false,
};
export const setConnection = (isConnected: boolean): Action<boolean> => ({
type: ActionType['/requests/isConnected'],
payload: isConnected,
});
export const isConnected = (isConnected: boolean) => async () => {
return isConnected;
};
export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
type: ActionType['/requests/setRequests'],
payload: requests,
});
export const notarizeRequest = (options: RequestHistory) => async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.prove_request_start,
data: {
url: options.url,
method: options.method,
headers: options.headers,
body: options.body,
maxSentData,
maxRecvData,
secretHeaders: options.secretHeaders,
secretResps: options.secretResps,
notaryUrl,
websocketProxyUrl,
},
});
};
export const setActiveTab = (
activeTab: browser.Tabs.Tab | null,
): Action<browser.Tabs.Tab | null> => ({
type: ActionType['/requests/setActiveTab'],
payload: activeTab,
});
export const addRequest = (request: RequestLog): Action<RequestLog> => ({
type: ActionType['/requests/addRequest'],
payload: request,
});
export default function requests(
state = initialState,
action: Action<any>,
): State {
switch (action.type) {
case ActionType['/requests/setRequests']:
return {
...state,
map: {
...(action?.payload || []).reduce(
(acc: { [requestId: string]: RequestLog }, req: RequestLog) => {
if (req) {
acc[req.requestId] = req;
}
return acc;
},
{},
),
},
};
case ActionType['/requests/setActiveTab']:
return {
...state,
activeTab: action.payload,
};
case ActionType['/requests/addRequest']:
return {
...state,
map: {
...state.map,
[action.payload.requestId]: action.payload,
},
};
case ActionType['/requests/isConnected']:
return {
...state,
isConnected: action.payload,
};
default:
return state;
}
}
export const useRequests = (): RequestLog[] => {
return useSelector((state: AppRootState) => {
return Object.values(state.requests.map);
}, deepEqual);
};
export const useRequest = (requestId?: string): RequestLog | null => {
return useSelector((state: AppRootState) => {
return requestId ? state.requests.map[requestId] : null;
}, deepEqual);
};
export const useActiveTab = (): chrome.tabs.Tab | null => {
return useSelector((state: AppRootState) => {
return state.requests.activeTab;
}, deepEqual);
};
export const useActiveTabUrl = (): URL | null => {
return useSelector((state: AppRootState) => {
const activeTab = state.requests.activeTab;
return activeTab?.url ? new URL(activeTab.url) : null;
}, deepEqual);
};
export const useIsConnected = (): boolean => {
return useSelector((state: AppRootState) => state.requests.isConnected);
};

View File

@@ -1,6 +0,0 @@
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.12';
export const RENDEZVOUS_API = 'wss://explorer.tlsnotary.org';
export const NOTARY_PROXY = 'wss://notary.pse.dev/proxy';
export const MAX_RECV = 16384;
export const MAX_SENT = 4096;

View File

@@ -1,518 +0,0 @@
import {
BackgroundActiontype,
handleExecP2PPluginProver,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
import { EXPLORER_API } from './constants';
import createPlugin, {
CallContext,
ExtismPluginOptions,
Plugin,
} from '@extism/extism';
import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import {
getCookiesByHost,
getHeadersByHost,
getLocalStorageByHost,
getSessionStorageByHost,
} from '../entries/Background/db';
const charwise = require('charwise');
export function urlify(
text: string,
params?: [string, string, boolean?][],
): URL | null {
try {
const url = new URL(text);
if (params) {
params.forEach(([k, v]) => {
url.searchParams.append(k, v);
});
}
return url;
} catch (e) {
return null;
}
}
export function devlog(...args: any[]) {
if (process.env.NODE_ENV === 'development') {
console.log(...args);
}
}
export function download(filename: string, content: string) {
const element = document.createElement('a');
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
export async function upload(filename: string, content: string) {
const formData = new FormData();
formData.append(
'file',
new Blob([content], { type: 'application/json' }),
filename,
);
const response = await fetch(`${EXPLORER_API}/api/upload`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to upload');
}
const data = await response.json();
return data;
}
export const copyText = async (text: string): Promise<void> => {
try {
await navigator.clipboard.writeText(text);
} catch (e) {
console.error(e);
}
};
export function convertNotaryWsToHttp(notaryWs: string) {
const { protocol, pathname, hostname, port } = new URL(notaryWs);
if (protocol === 'https:' || protocol === 'http:') {
return notaryWs;
}
const p = protocol === 'wss:' ? 'https:' : 'http:';
const pt = port ? `:${port}` : '';
const path = pathname === '/' ? '' : pathname.replace('/notarize', '');
const h = hostname === 'localhost' ? '127.0.0.1' : hostname;
return p + '//' + h + pt + path;
}
export async function replayRequest(req: RequestLog): Promise<string> {
const options = {
method: req.method,
headers: req.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
}
return acc;
},
{},
),
body: req.requestBody,
};
if (req?.formData) {
const formData = new URLSearchParams();
Object.entries(req.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
// @ts-ignore
const resp = await fetch(req.url, options);
return extractBodyFromResponse(resp);
}
export const extractBodyFromResponse = async (
resp: Response,
): Promise<string> => {
const contentType =
resp.headers.get('content-type') || resp.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return resp.text();
} else if (contentType?.includes('text')) {
return resp.text();
} else if (contentType?.includes('image')) {
return resp.blob().then((blob) => blob.text());
} else {
return resp.blob().then((blob) => blob.text());
}
};
export const sha256 = async (data: string) => {
const encoder = new TextEncoder().encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
};
const VALID_HOST_FUNCS: { [name: string]: string } = {
redirect: 'redirect',
notarize: 'notarize',
};
export const makePlugin = async (
arrayBuffer: ArrayBuffer,
config?: PluginConfig,
meta?: {
p2p: boolean;
clientId: string;
},
) => {
const module = await WebAssembly.compile(arrayBuffer);
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
const injectedConfig = {
tabUrl: tab?.url || 'x://x',
tabId: tab?.id,
};
const approvedRequests = config?.requests || [];
const approvedNotary = [await getNotaryApi()].concat(config?.notaryUrls);
const approvedProxy = [await getProxyApi()].concat(config?.proxyUrls);
const HostFunctions: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {
redirect: function (context: CallContext, off: bigint) {
const r = context.read(off);
if (!r) throw new Error('Failed to read context');
const url = r.text();
browser.tabs.update(tab.id, { url });
},
notarize: function (context: CallContext, off: bigint) {
const r = context.read(off);
if (!r) throw new Error('Failed to read context');
const params = JSON.parse(r.text());
const now = Date.now();
const id = charwise.encode(now).toString('hex');
if (
!approvedRequests.find(
({ method, url }) =>
method === params.method && minimatch(params.url, url),
)
) {
throw new Error(`Unapproved request - ${params.method}: ${params.url}`);
}
if (
params.notaryUrl &&
!approvedNotary.find((n) => n === params.notaryUrl)
) {
throw new Error(`Unapproved notary: ${params.notaryUrl}`);
}
if (
params.websocketProxyUrl &&
!approvedProxy.find((w) => w === params.websocketProxyUrl)
) {
throw new Error(`Unapproved proxy: ${params.websocketProxyUrl}`);
}
(async () => {
const {
getSecretResponse,
body: reqBody,
interactive,
verifierPlugin,
} = params;
console.log('interactive', interactive);
console.log('verifierPlugin', verifierPlugin);
console.log('params', params);
if (interactive) {
const pluginHex = Buffer.from(arrayBuffer).toString('hex');
const pluginUrl = await sha256(pluginHex);
handleExecP2PPluginProver({
type: BackgroundActiontype.execute_p2p_plugin_prover,
data: {
...params,
pluginUrl,
pluginHex,
body: reqBody,
now,
verifierPlugin,
},
});
} else {
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
getSecretResponseFn: async (body: string) => {
return new Promise((resolve) => {
setTimeout(async () => {
const out = await plugin.call(getSecretResponse, body);
resolve(JSON.parse(out?.string() || '{}'));
}, 0);
});
},
now,
},
});
}
})();
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
for (const fn of Object.keys(VALID_HOST_FUNCS)) {
funcs[fn] = function (context: CallContext) {
throw new Error(`no permission for ${fn}`);
};
}
if (config?.hostFunctions) {
for (const fn of config.hostFunctions) {
funcs[fn] = HostFunctions[fn];
}
}
if (config?.localStorage) {
const localStorage: { [hostname: string]: { [key: string]: string } } = {};
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await chrome.tabs.sendMessage(tab.id as number, {
type: BackgroundActiontype.get_local_storage,
});
//@ts-ignore
for (const host of config.localStorage) {
const cache = await getLocalStorageByHost(host);
localStorage[host] = cache;
}
//@ts-ignore
injectedConfig.localStorage = JSON.stringify(localStorage);
}
if (config?.sessionStorage) {
const sessionStorage: { [hostname: string]: { [key: string]: string } } =
{};
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await chrome.tabs.sendMessage(tab.id as number, {
type: BackgroundActiontype.get_session_storage,
});
//@ts-ignore
for (const host of config.sessionStorage) {
const cache = await getSessionStorageByHost(host);
sessionStorage[host] = cache;
}
//@ts-ignore
injectedConfig.sessionStorage = JSON.stringify(sessionStorage);
}
if (config?.cookies) {
const cookies: { [link: string]: { [key: string]: string } } = {};
for (const link of config.cookies) {
const cache = await getCookiesByHost(link);
cookies[link] = cache;
}
// @ts-ignore
injectedConfig.cookies = JSON.stringify(cookies);
}
if (config?.headers) {
const headers: { [link: string]: { [key: string]: string } } = {};
for (const link of config.headers) {
const cache = await getHeadersByHost(link);
headers[link] = cache;
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
}
const pluginConfig: ExtismPluginOptions = {
useWasi: true,
config: {
...injectedConfig,
tabId: tab.id?.toString() || '',
},
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
functions: {
'extism:host/user': funcs,
},
};
const plugin = await createPlugin(module, pluginConfig);
return plugin;
};
export type InputFieldConfig = {
name: string; // Unique identifier for the input field
label: string; // Display label for the input
type: 'text' | 'password' | 'email' | 'number' | 'textarea' | 'select'; // Input field type
placeholder?: string; // Optional placeholder text
required?: boolean; // Whether the field is required
defaultValue?: string; // Default value for the field
options?: { value: string; label: string }[]; // Options for select type
};
export type StepConfig = {
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)
inputs?: InputFieldConfig[]; // Input fields for user data collection (optional)
};
export type PluginConfig = {
title: string; // The name of the plugin
description: string; // A description of the plugin 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)
localStorage?: string[]; // LocalStorage the plugin will have access to, cached by the extension from specified hosts (optional)
sessionStorage?: string[]; // SessionStorage 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 = {
origin: string;
filePath: string;
} & { [k: string]: string };
export const getPluginConfig = async (
data: Plugin | ArrayBuffer,
): Promise<PluginConfig> => {
const plugin = data instanceof ArrayBuffer ? await makePlugin(data) : data;
const out = await plugin.call('config');
if (!out) throw new Error('Plugin config call returned null');
const config: PluginConfig = JSON.parse(out.string());
assert(typeof config.title === 'string' && config.title.length);
assert(typeof config.description === 'string' && config.description.length);
assert(!config.icon || typeof config.icon === 'string');
for (const req of config.requests) {
assert(typeof req.method === 'string' && req.method);
assert(typeof req.url === 'string' && req.url);
}
if (config.hostFunctions) {
for (const func of config.hostFunctions) {
assert(typeof func === 'string' && !!VALID_HOST_FUNCS[func]);
}
}
if (config.notaryUrls) {
for (const notaryUrl of config.notaryUrls) {
assert(typeof notaryUrl === 'string' && notaryUrl);
}
}
if (config.proxyUrls) {
for (const proxyUrl of config.proxyUrls) {
assert(typeof proxyUrl === 'string' && proxyUrl);
}
}
if (config.cookies) {
for (const name of config.cookies) {
assert(typeof name === 'string' && name.length);
}
}
if (config.localStorage) {
for (const name of config.localStorage) {
assert(typeof name === 'string' && name.length);
}
}
if (config.sessionStorage) {
for (const name of config.sessionStorage) {
assert(typeof name === 'string' && name.length);
}
}
if (config.headers) {
for (const name of config.headers) {
assert(typeof name === 'string' && name.length);
}
}
if (config.steps) {
for (const step of config.steps) {
assert(typeof step.title === 'string' && step.title.length);
assert(!step.description || typeof step.description);
assert(typeof step.cta === 'string' && step.cta.length);
assert(typeof step.action === 'string' && step.action.length);
assert(!step.prover || typeof step.prover === 'boolean');
if (step.inputs) {
for (const input of step.inputs) {
assert(typeof input.name === 'string' && input.name.length);
assert(typeof input.label === 'string' && input.label.length);
assert(!input.placeholder || typeof input.placeholder === 'string');
assert(!input.required || typeof input.required === 'boolean');
assert(!input.defaultValue || typeof input.defaultValue === 'string');
if (input.type === 'select') {
assert(Array.isArray(input.options) && input.options.length > 0);
for (const option of input.options!) {
assert(typeof option.value === 'string');
assert(typeof option.label === 'string');
}
}
}
}
}
}
return config;
};
export const assert = (expr: any, msg = 'unknown error') => {
if (!expr) throw new Error(msg);
};
export const hexToArrayBuffer = (hex: string) =>
new Uint8Array(Buffer.from(hex, 'hex')).buffer;
export const cacheToMap = (cache: NodeCache) => {
const keys = cache.keys();
return keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
};
export function safeParseJSON(data?: string | null) {
try {
return JSON.parse(data!);
} catch (e) {
return null;
}
}
export function isPopupWindow(): boolean {
return (
!!window.opener || window.matchMedia('(display-mode: standalone)').matches
);
}

View File

@@ -1,33 +0,0 @@
import { HTTPParser } from 'http-parser-js';
export function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0],
headers,
body,
};
}

View File

@@ -1,87 +0,0 @@
import { PluginConfig } from './misc';
import React, { ReactElement, ReactNode } from 'react';
import Icon from '../components/Icon';
export const HostFunctionsDescriptions: {
[key: string]: (pluginContent: PluginConfig) => ReactElement;
} = {
redirect: () => {
return (
<PermissionDescription fa="fa-solid fa-diamond-turn-right">
<span>Redirect your current tab to any URL</span>
</PermissionDescription>
);
},
notarize: ({ notaryUrls, proxyUrls }) => {
const notaries = ['default notary', 'your peer'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (
<>
<PermissionDescription fa="fa-solid fa-route">
<span className="cursor-default">
<span className="mr-1">Proxy notarization requests thru</span>
<MultipleParts parts={proxies} />
</span>
</PermissionDescription>
<PermissionDescription fa="fa-solid fa-stamp">
<span className="cursor-default">
<span className="mr-1">Submit notarization requests to</span>
<MultipleParts parts={notaries} />
</span>
</PermissionDescription>
</>
);
},
};
export function PermissionDescription({
fa,
children,
}: {
fa: string;
children?: ReactNode;
}): ReactElement {
return (
<div className="flex flex-row gap-4 items-start cursor-default">
<Icon className="" size={1.6125} fa={fa} />
<div className="text-sm mt-[0.125rem]">{children}</div>
</div>
);
}
export function MultipleParts({ parts }: { parts: string[] }): ReactElement {
const content = [];
if (parts.length > 1) {
for (let i = 0; i < parts.length; i++) {
content.push(
<span key={i} className="text-blue-600">
{parts[i]}
</span>,
);
if (parts.length - i === 2) {
content.push(
<span key={i + 'separator'} className="inline-block mx-1">
and
</span>,
);
} else if (parts.length - i > 1) {
content.push(
<span key={i + 'separator'} className="inline-block mr-1">
,
</span>,
);
}
}
} else {
content.push(
<span key={0} className="text-blue-600">
{parts[0]}
</span>,
);
}
return <>{content}</>;
}

View File

@@ -1,17 +0,0 @@
export const deferredPromise = (): {
promise: Promise<never>;
resolve: (data?: any) => void;
reject: (reason?: any) => void;
} => {
let resolve: (data?: any) => void, reject: (reason?: any) => void;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-ignore
return { promise, resolve, reject };
};
export type PromiseResolvers = ReturnType<typeof deferredPromise>;

View File

@@ -1,56 +0,0 @@
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
export async function addPlugin(hex: string, url: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,
data: { hex, url },
});
}
export async function removePlugin(hash: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.remove_plugin,
data: hash,
});
}
export async function fetchPluginHashes() {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_hashes,
});
}
export async function fetchPluginByHash(hash: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_by_hash,
data: hash,
});
}
export async function fetchPluginConfigByHash(
hash: string,
): Promise<PluginConfig | null> {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_config_by_hash,
data: hash,
});
}
export async function runPlugin(
hash: string,
method: string,
params?: string,
meta?: any,
) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin,
data: {
hash,
method,
params,
},
meta,
});
}

View File

@@ -1,51 +0,0 @@
import { LoggingLevel } from 'tlsn-js';
import { MAX_RECV, MAX_SENT, NOTARY_API, NOTARY_PROXY } from './constants';
import { RENDEZVOUS_API } from './constants';
export const NOTARY_API_LS_KEY = 'notary-api';
export const PROXY_API_LS_KEY = 'proxy-api';
export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export const RENDEZVOUS_API_LS_KEY = 'rendezvous-api';
export const DEVELOPER_MODE_LS_KEY = 'developer-mode';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
}
export async function get(key: string, defaultValue?: string) {
return chrome.storage.sync
.get(key)
.then((json: any) => json[key] || defaultValue)
.catch(() => defaultValue);
}
export async function getMaxSent() {
return parseInt(await get(MAX_SENT_LS_KEY, MAX_SENT.toString()));
}
export async function getMaxRecv() {
return parseInt(await get(MAX_RECEIVED_LS_KEY, MAX_RECV.toString()));
}
export async function getNotaryApi() {
return await get(NOTARY_API_LS_KEY, NOTARY_API);
}
export async function getProxyApi() {
return await get(PROXY_API_LS_KEY, NOTARY_PROXY);
}
export async function getLoggingFilter(): Promise<LoggingLevel> {
return await get(LOGGING_FILTER_KEY, 'Info');
}
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}
export async function getDeveloperMode(): Promise<boolean> {
const value = await get(DEVELOPER_MODE_LS_KEY, 'false');
return value === 'true';
}

View File

@@ -7,7 +7,6 @@ var webpack = require("webpack"),
TerserPlugin = require("terser-webpack-plugin");
var { CleanWebpackPlugin } = require("clean-webpack-plugin");
var ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
var ExtReloader = require('webpack-ext-reloader');
const ASSET_PATH = process.env.ASSET_PATH || "/";
@@ -37,27 +36,13 @@ const isDevelopment = process.env.NODE_ENV !== "production";
var options = {
mode: process.env.NODE_ENV || "development",
ignoreWarnings: [
/Circular dependency between chunks with runtime/,
/ResizeObserver loop completed with undelivered notifications/,
/Should not import the named export/,
/Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0/,
/Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0./,
/repetitive deprecation warnings omitted/,
],
entry: {
options: path.join(__dirname, "src", "entries", "Options", "index.tsx"),
popup: path.join(__dirname, "src", "entries", "Popup", "index.tsx"),
background: path.join(__dirname, "src", "entries", "Background", "index.ts"),
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
content: path.join(__dirname, "src", "entries", "Content", "content.ts"),
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
sidePanel: path.join(__dirname, "src", "entries", "SidePanel", "index.tsx"),
},
// chromeExtensionBoilerplate: {
// notHotReload: ["background", "contentScript", "devtools"],
// },
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "build"),
@@ -85,9 +70,6 @@ var options = {
loader: "sass-loader",
options: {
sourceMap: true,
sassOptions: {
silenceDeprecations: ["legacy-js-api"],
}
},
},
],
@@ -96,10 +78,6 @@ var options = {
test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
type: "asset/resource",
exclude: /node_modules/,
// loader: 'file-loader',
// options: {
// name: '[name].[ext]',
// },
},
{
test: /\.html$/,
@@ -149,9 +127,6 @@ var options = {
new webpack.ProgressPlugin(),
// expose and write the allowed env vars on the compiled bundle
new webpack.EnvironmentPlugin(["NODE_ENV"]),
// new ExtReloader({
// manifest: path.resolve(__dirname, "src/manifest.json")
// }),
new CopyWebpackPlugin({
patterns: [
{
@@ -198,21 +173,6 @@ var options = {
},
],
}),
new CopyWebpackPlugin({
patterns: [
{
from: "node_modules/tlsn-js/build",
to: path.join(__dirname, "build"),
force: true,
},
],
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "entries", "Options", "index.html"),
filename: "options.html",
chunks: ["options"],
cache: false,
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "entries", "Popup", "index.html"),
filename: "popup.html",
@@ -225,29 +185,10 @@ var options = {
chunks: ["offscreen"],
cache: false,
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "entries", "SidePanel", "index.html"),
filename: "sidePanel.html",
chunks: ["sidePanel"],
cache: false,
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
].filter(Boolean),
infrastructureLogging: {
level: "info",
},
// Required by wasm-bindgen-rayon, in order to use SharedArrayBuffer on the Web
// Ref:
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
// - https://web.dev/i18n/en/coop-coep/
devServer: {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
}
},
};
if (env.NODE_ENV === "development") {
@@ -263,4 +204,4 @@ if (env.NODE_ENV === "development") {
};
}
module.exports = options;
module.exports = options;