Compare commits

...

58 Commits

Author SHA1 Message Date
Hendrik Eeckhaut
753e0490f1 Bump version to 1202 (#204) 2025-09-18 16:14:15 +02:00
Hendrik Eeckhaut
30249bb2d3 Fixed formatting (#203) 2025-09-18 16:06:00 +02:00
Kayanski
e497dcae27 Removing initiator url on requestId as well (#200)
* Removing initiator url on requestId as well

* Removing format

* Another format removal
2025-09-16 21:26:50 +08:00
Kayanski
1cac2f79e9 Added close panel listener (#202) 2025-09-16 21:26:41 +08:00
Hendrik Eeckhaut
650553e793 Removed bundled plugins and bump version to 1201
* Removed bundled plugins

* bump version to 1201
2025-08-20 14:32:20 +02:00
dylan1951
de676eb498 fix: cookie parsing logic (again) (#194)
Co-authored-by: dylan <dylanbradshaw107@hotmail.com>
2025-08-19 16:48:14 +02:00
Tanner
d334286cbd Sidepanel supports <input> fields for user input through plugins (#195)
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-08-18 19:23:23 +02:00
baptistapedro
538331c847 remove defaultPluginsInstalled as it blocks testing with tlsn-plugin-boilerplate (#198) 2025-08-18 10:43:15 +02:00
Hendrik Eeckhaut
df1af592b6 Interactive verifier support in browser extension (#196)
* refactor: turn p2p prover into interactive verifier plugin prover
2025-08-18 10:01:59 +02:00
Tanner
7db5b12629 Network Tab moved to Dev Mode (#197) 2025-08-14 10:45:26 +02:00
Tanner
cc3264f058 Alpha.12 (#193) 2025-06-27 17:58:36 +01:00
Tanner
ea12322686 Developer Mode (#190)
* feat: adding developer mode toggle to add and display plugins

* chore: lint

* chore: cleanup
2025-06-26 17:54:22 +01:00
tsukino
5a9e5bc77d fix: query for headers and cookies (#192) 2025-06-26 17:38:30 +01:00
tsukino
f489800663 fix: refactor cache layer (#189) 2025-06-19 10:54:08 -04:00
yuroitaki
49056dc605 Correct url. (#191) 2025-06-12 15:30:56 +02:00
Tanner
9567590e47 Data: no longer duplicated in meta for downloaded attestations (#188)
* fix: data no longer duplicated in meta in downloads

* fix: cleanup
2025-06-10 08:24:38 -04:00
Tanner
008cb10b30 Display WSS errors in Sidepanel (#187)
* feat: display websocket proxy errors in sidepanel

* chore: cleanup
2025-06-10 08:23:35 -04:00
tsukino
6b8e9a3580 feat: clear db button (#186)
* feat: clear db

* add size calc
2025-06-10 08:23:13 -04:00
Brendan A. Miller
6aab10b7d0 Improve error handling for webpack build (#183) 2025-06-03 16:44:00 +02:00
Hendrik Eeckhaut
7de46bf590 chore: type cleanup (#185) 2025-06-02 21:30:52 +02:00
Hendrik Eeckhaut
c67b794f40 ci: do not unpack the zip file (#184) 2025-06-02 10:51:31 +02:00
Tanner
9872a376c1 Alpha.11 (#182)
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-06-02 10:32:34 +02:00
Hendrik Eeckhaut
f34718f352 ci: Add missing checkout step in release job (#179) 2025-05-14 14:17:05 +02:00
tsukino
08520a90ff chore: update to alpha.10 (#177) 2025-04-30 16:18:51 +02:00
tsukino
264128d1fb feat: 1-click plugin (#175) 2025-04-30 03:01:54 -04:00
Hendrik Eeckhaut
dbb617f516 Update extism to v2.0.0-rc11 (#174) 2025-04-25 22:27:07 +02:00
Hendrik Eeckhaut
0ba4a71bba Added link to chrome web store (#173) 2025-04-16 17:22:26 +02:00
Tanner
ade6d7e575 Added Errors to Notarization Progress (#147)
* feat: added Error for RequestProgress

* feat: added logic to handle errors depending on progress

* chore: linting errors

* fix: linting build error

* Avoid magic numbers

* lint

* feat: alpha.8

* fix: package.json path

* chore: update lockfiles

* fix: notary url

* feat: notarization timeouts

* fix: notarization timeouts

* feat: fixed comments + errors are properly thrown and status changes

* fix: notarization timeouts work properly now

* fix: timeout error message fix

* fix: lint

* fix: lint

* fix: errors should keep state after being thrown now

* chore: lint

* fix: fixing metadata with new progress status

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
Co-authored-by: tsukino <0xtsukino@gmail.com>
2025-04-15 10:00:57 -07:00
Hendrik Eeckhaut
3f37c1aee8 Update extism to v1.0.3 (#166) 2025-04-10 10:02:48 +02:00
tsukino
5545d7abed refactor: remove unused versions (#167) 2025-03-31 16:57:41 +02:00
Hendrik Eeckhaut
b674f5c579 Correct package version (#165) 2025-03-28 13:52:26 +01:00
Hendrik Eeckhaut
49a94e1bbc build: Update to alpha.9 (#162) 2025-03-28 13:44:22 +01:00
Hendrik Eeckhaut
64d572d663 ci: Merge GH workflows into one ci flow + add release job 2025-03-28 11:32:24 +01:00
Hendrik Eeckhaut
2f35278235 ci: use npm to build (instead of pnpm) (#160) 2025-03-27 16:02:36 +01:00
tsukino
5af783e48c fix: notaryUrl and websoccketProxyUrl types in proof viewer (#159) 2025-03-14 04:47:56 -04:00
tsukino
1ea69d0574 feat: alpha.8 (#156)
* feat: alpha.8

* fix: package.json path

* chore: update lockfiles

* fix: notary url
2025-03-14 04:05:11 -04:00
Hendrik Eeckhaut
8bfcf8c8d5 fix: correct github workflow name (#144) 2025-03-14 03:42:10 -04:00
dylan1951
fe825d7ebb Add support for passing params to plugin (#152)
* Add support for passing params to plugin start() method.

* Remove GUI parameter inputs for now

---------

Co-authored-by: dylan <dylanbradshaw107@hotmail.com>
2025-03-14 03:41:19 -04:00
tsukino
ca4986c3bb fix: parse raw buffers from transcript (#154) 2025-03-12 10:43:16 -04:00
Tanner
da756721d4 UI changes (#142)
* feat: adding metadata to proofviewer

* feat: X button conditionally displays on how the proof viewer is opened

* feat: added confirmation modal to deleting history

* chore: clean up

* feat: added verifierKey and notaryKey to metadata in proofviewer

* feat: UI fixes and adding RemoveHistory modal to ProofViewer
2025-03-05 18:59:59 -08:00
Tanner
38852620d2 feat: sidepanel now refreshes on subsequent plugin runs if still open (#153) 2025-03-05 18:52:45 -08:00
Hendrik Eeckhaut
1c9f340add Use GitHub ci to release extension to the Chrome webstore (#133) 2025-02-07 03:30:35 -05:00
tsukino
423be796f6 chore: version 0.1.0.704 (#143) 2025-02-07 01:12:51 -05:00
dylan1951
7631baf939 fix: cookie parsing logic (#141) 2025-02-05 14:28:38 +01:00
Tanner
8733b26e12 feat: fix Install Plugin button in extension menu
#131
2025-01-30 16:04:41 +01:00
tsukino
846bc1ef29 feat: add progress for notarization request and resurface error message (#119)
#119
2025-01-28 10:29:21 +01:00
tsukino
efb4386d1b chore: update default twitter plugin and bump version (#132) 2025-01-21 20:00:11 -05:00
mac
06dc4fac83 chore: add domains to websockify_config (#121)
chore: Remove stale websockify_config

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-01-21 11:12:27 +01:00
tsukino
3b98fc3b2f fix: allow querying with both url and hostname when caching headers and cookies (#123) 2025-01-21 05:00:19 -05:00
tsukino
e3166d62a0 refactor: offscreen and ws event handlers (#115)
* refactor: offscreen rpc code

* refactor: ws message sender
2024-12-11 04:50:56 -05:00
Tanner
ca382f3532 fix: unintended sendResponse removed (#120) 2024-12-04 22:46:25 -05:00
Tanner
b8d068b828 feat: Add plugin context to get browser storage (#101) 2024-12-03 07:08:26 -05:00
tsukino
4c908d0611 chore: version pump (#117) 2024-11-08 02:56:57 -05:00
tsukino
ed3e797bad fix: send correct message on plugin execution using content script (#116)
* fix: send correct message on plugin execution using content script

* fix: get plugins filter logic and run plugin message timing on content script
2024-11-08 02:55:31 -05:00
tsukino
398950598e feat: implement p2p prover and verifier (#114) 2024-11-07 04:12:41 -05:00
tsukino
f3b8cc1066 fix: use notarized transcript instead of pre-fetched response for redaction (#113)
* wip

* fix: use transcript for redaction

* refactor: add map secret to ranges utility

* fix: body selector

* fix
2024-11-05 07:32:02 -05:00
tsukino
bd8e58c042 feat: dispatch event when cs is loaded (#112) 2024-10-29 05:39:59 -04:00
tsukino
fb4c81b851 fix: throw error when installilng duplicate pluign (#111) 2024-10-29 05:08:55 -04:00
65 changed files with 8757 additions and 23372 deletions

View File

@@ -1,46 +0,0 @@
name: build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: npm run build

72
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: ci
on:
pull_request:
release:
types: [published]
jobs:
build-lint-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: npm run lint
- name: Test Webpack Build
run: npm run build:webpack
- name: Save extension zip file for releases
if: github.event_name == 'release'
uses: actions/upload-artifact@v4
with:
name: tlsn-extension-${{ github.ref_name }}.zip
path: ./zip/tlsn-extension-${{ github.ref_name }}.zip
if-no-files-found: error
release:
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: build-lint-test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download extension from build-lint-test job
uses: actions/download-artifact@v4
with:
name: tlsn-extension-${{ github.ref_name }}.zip
path: .
- name: 📦 Add extension zip file to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ github.event.release.tag_name }}" \
./tlsn-extension-${{ github.ref_name }}.zip \
--clobber
# Get tokens as documented on
# * https://developer.chrome.com/docs/webstore/using-api#beforeyoubegin
# * https://github.com/fregante/chrome-webstore-upload-keys?tab=readme-ov-file
- name: 💨 Publish to chrome store
uses: browser-actions/release-chrome-extension@latest # https://github.com/browser-actions/release-chrome-extension/tree/latest/
with:
extension-id: "gcfkkledipjbgdbimfpijgbkhajiaaph"
extension-path: tlsn-extension-${{ github.ref_name }}.zip
oauth-client-id: ${{ secrets.OAUTH_CLIENT_ID }}
oauth-client-secret: ${{ secrets.OAUTH_CLIENT_SECRET }}
oauth-refresh-token: ${{ secrets.OAUTH_REFRESH_TOKEN }}

View File

@@ -1,46 +0,0 @@
name: lint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: npm run lint

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ bin/
build
tlsn/
zip
.vscode

View File

@@ -12,7 +12,7 @@
# Chrome Extension (MV3) for TLSNotary
> [!IMPORTANT]
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/main/crates/notary/server), please ensure that the server's version is the same as the version of this extension
## License
This repository is licensed under either of
@@ -25,7 +25,11 @@ at your option.
## Installing and Running
### Procedures:
The easiest way to install the TLSN browser extension is to use the [Chrome Web Store](https://chromewebstore.google.com/detail/tlsn-extension/gcfkkledipjbgdbimfpijgbkhajiaaph).
You can also build and run it locally as explained in the following steps.
### Procedure:
1. Check if your [Node.js](https://nodejs.org/) version is >= **18**.
2. Clone this repository.

16030
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-extension",
"version": "0.1.0.700",
"version": "0.1.0.1202",
"license": "MIT",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^1.0.2",
"@extism/extism": "^2.0.0-rc11",
"@fortawesome/fontawesome-free": "^6.4.2",
"async-mutex": "^0.4.0",
"buffer": "^6.0.3",
@@ -24,8 +24,10 @@
"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",
@@ -38,8 +40,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.7.1",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
"tlsn-js": "0.1.0-alpha.12.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -93,4 +94,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

8645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" id="Glyph" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M16,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S17.654,13,16,13z" id="XMLID_287_"/><path d="M6,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S7.654,13,6,13z" id="XMLID_289_"/><path d="M26,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S27.654,13,26,13z" id="XMLID_291_"/></svg>

After

Width:  |  Height:  |  Size: 621 B

BIN
src/assets/img/notarize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

View File

@@ -9,7 +9,6 @@ import Icon from '../Icon';
import browser from 'webextension-polyfill';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import PluginUploadInfo from '../PluginInfo';
export function MenuIcon(): ReactElement {
const [opened, setOpen] = useState(false);
@@ -54,24 +53,24 @@ export default function Menu(props: {
<div className="absolute top-[100%] right-0 rounded-md z-20">
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
<MenuRow
fa="fa-solid fa-plus"
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');
}}
>
<PluginUploadInfo />
<span>Install Plugin</span>
</MenuRow>
<MenuRow
fa="fa-solid fa-toolbox"
className="border-b border-slate-300"
onClick={() => {
props.setOpen(false);
navigate('/plugins');
}}
>
Plugins
Verify
</MenuRow>
<MenuRow
className="lg:hidden"

View File

@@ -1,14 +1,4 @@
import React, {
ChangeEvent,
Children,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import React, { Children, MouseEventHandler, ReactNode } from 'react';
import Modal, {
ModalHeader,
ModalContent,
@@ -22,69 +12,9 @@ import {
MultipleParts,
PermissionDescription,
} from '../../utils/plugins';
import { ErrorModal } from '../ErrorModal';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function PluginUploadInfo(): ReactElement {
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onAddPlugin = useCallback(
async (evt: React.MouseEvent<HTMLButtonElement>) => {
try {
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
setPluginContent(null);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
},
[pluginContent, pluginBuffer],
);
const onPluginInfo = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
} finally {
evt.target.value = '';
}
},
[setPluginContent, setPluginBuffer],
);
const onClose = useCallback(() => {
setPluginContent(null);
setPluginBuffer(null);
}, []);
return (
<>
<input
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
type="file"
onChange={onPluginInfo}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
<PluginInfoModal
pluginContent={pluginContent}
onClose={onClose}
onAddPlugin={onAddPlugin}
/>
)}
</>
);
}
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];
@@ -193,6 +123,22 @@ export function PluginPermissions({
</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">

View File

@@ -6,7 +6,7 @@
padding: 0;
overflow: hidden;
transition: 200ms opacity;
transition-delay: 200ms;
}
&:hover {

View File

@@ -5,9 +5,18 @@ import React, {
useEffect,
useState,
} from 'react';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import {
fetchPluginHashes,
removePlugin,
runPlugin,
addPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import {
getPluginConfig,
hexToArrayBuffer,
PluginConfig,
} from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import classNames from 'classnames';
import Icon from '../Icon';
@@ -19,75 +28,163 @@ import {
PluginInfoModalContent,
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { getPluginConfigByUrl } from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { openSidePanel } from '../../entries/utils';
export function PluginList(props: { className?: string }): ReactElement {
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', props.className)}
>
{!hashes.length && (
<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} />
<Plugin
key={hash}
hash={hash}
unremovable={unremovable}
onClick={onClick}
/>
))}
</div>
);
}
export function Plugin(props: {
export function Plugin({
hash,
hex,
unremovable,
onClick,
className,
}: {
hash: string;
onClick?: () => void;
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 onClick = useCallback(async () => {
const onRunPlugin = useCallback(async () => {
if (!config || remove) return;
try {
await runPlugin(props.hash, 'start');
if (onClick) {
onClick(hash);
return;
}
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
try {
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
},
});
await browser.storage.local.set({ plugin_hash: props.hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
await runPlugin(hash, 'start');
window.close();
} catch (e: any) {
showError(e.message);
}
}, [props.hash, config, remove]);
}, [hash, config, remove, onClick]);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(props.hash));
if (hex) {
setConfig(await getPluginConfig(hexToArrayBuffer(hex)));
} else {
setConfig(await getPluginConfigByUrl(hash));
}
})();
}, [props.hash]);
}, [hash, hex]);
const onRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
removePlugin(props.hash);
removePlugin(hash);
showRemove(false);
},
[props.hash, remove],
[hash, remove],
);
const onConfirmRemove: MouseEventHandler = useCallback(
@@ -95,7 +192,7 @@ export function Plugin(props: {
e.stopPropagation();
showRemove(true);
},
[props.hash, remove],
[hash, remove],
);
const onPluginInfo: MouseEventHandler = useCallback(
@@ -103,7 +200,7 @@ export function Plugin(props: {
e.stopPropagation();
showPluginInfo(true);
},
[props.hash, pluginInfo],
[hash, pluginInfo],
);
if (!config) return <></>;
@@ -113,8 +210,9 @@ export function Plugin(props: {
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={onClick}
onClick={onRunPlugin}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
{!remove ? (
@@ -129,11 +227,13 @@ export function Plugin(props: {
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
onClick={onPluginInfo}
/>
<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}
/>
{!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>

View File

@@ -24,6 +24,7 @@ import {
MAX_RECEIVED_LS_KEY,
getMaxRecv,
getMaxSent,
getDeveloperMode,
} from '../../utils/storage';
import { MAX_RECV, MAX_SENT } from '../../utils/constants';

View File

@@ -1,28 +0,0 @@
import NodeCache from 'node-cache';
let RequestsLogs: {
[tabId: string]: NodeCache;
} = {};
export const deleteCacheByTabId = (tabId: number) => {
delete RequestsLogs[tabId];
};
export const getCacheByTabId = (tabId: number): NodeCache => {
RequestsLogs[tabId] =
RequestsLogs[tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
return RequestsLogs[tabId];
};
export const clearRequestCache = () => {
RequestsLogs = {};
};
export const clearCache = () => {
clearRequestCache();
};

View File

@@ -1,7 +1,14 @@
import { Level } from 'level';
import type { RequestHistory } from './rpc';
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
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', {
@@ -22,17 +29,94 @@ const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
});
const cookiesDb = db.sublevel<string, boolean>('cookies', {
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
valueEncoding: 'json',
});
const headersDb = db.sublevel<string, boolean>('headers', {
const sessionStorageDb = db.sublevel<string, any>('localStorage', {
valueEncoding: 'json',
});
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
enum AppDatabaseKey {
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
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(
@@ -68,6 +152,17 @@ export async function addNotaryRequestProofs(
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',
@@ -105,9 +200,33 @@ export async function setNotaryRequestError(
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 },
verification: {
sent: string;
recv: string;
verifierKey: string;
notaryKey?: string;
},
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
@@ -157,41 +276,44 @@ export async function getPluginHashes(): Promise<string[]> {
return retVal;
}
export async function getPluginByHash(hash: string): Promise<string | null> {
export async function getPluginByUrl(url: string): Promise<string | null> {
try {
const plugin = await pluginDb.get(hash);
const plugin = await pluginDb.get(url);
return plugin;
} catch (e) {
return null;
}
}
export async function addPlugin(hex: string): Promise<string | null> {
export async function addPlugin(
hex: string,
url: string,
): Promise<string | null> {
const hash = await sha256(hex);
if (await getPluginByHash(hash)) {
return null;
if (await getPluginByUrl(url)) {
return url;
}
await pluginDb.put(hash, hex);
await pluginDb.put(url, hex);
return hash;
}
export async function removePlugin(hash: string): Promise<string | null> {
const existing = await pluginDb.get(hash);
export async function removePlugin(url: string): Promise<string | null> {
const existing = await pluginDb.get(url);
if (!existing) return null;
await pluginDb.del(hash);
await pluginDb.del(url);
return hash;
return url;
}
export async function getPluginConfigByHash(
hash: string,
export async function getPluginConfigByUrl(
url: string,
): Promise<PluginConfig | null> {
try {
const config = await pluginConfigDb.get(hash);
const config = await pluginConfigDb.get(url);
return config;
} catch (e) {
return null;
@@ -199,25 +321,25 @@ export async function getPluginConfigByHash(
}
export async function addPluginConfig(
hash: string,
url: string,
config: PluginConfig,
): Promise<PluginConfig | null> {
if (await getPluginConfigByHash(hash)) {
if (await getPluginConfigByUrl(url)) {
return null;
}
await pluginConfigDb.put(hash, config);
await pluginConfigDb.put(url, config);
return config;
}
export async function removePluginConfig(
hash: string,
url: string,
): Promise<PluginConfig | null> {
const existing = await pluginConfigDb.get(hash);
const existing = await pluginConfigDb.get(url);
if (!existing) return null;
await pluginConfigDb.del(hash);
await pluginConfigDb.del(url);
return existing;
}
@@ -228,27 +350,33 @@ export async function getPlugins(): Promise<
const hashes = await getPluginHashes();
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
for (const hash of hashes) {
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
const config = await getPluginConfigByUrl(hash);
const metadata = await getPluginMetadataByUrl(hash);
if (config) {
ret.push({
...config,
hash,
metadata: metadata || {
filePath: '',
origin: '',
},
metadata: metadata
? {
...metadata,
hash,
}
: {
filePath: '',
origin: '',
hash,
},
});
}
}
return ret;
}
export async function getPluginMetadataByHash(
hash: string,
export async function getPluginMetadataByUrl(
url: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(hash);
const metadata = await pluginMetadataDb.get(url);
return metadata;
} catch (e) {
return null;
@@ -256,21 +384,21 @@ export async function getPluginMetadataByHash(
}
export async function addPluginMetadata(
hash: string,
url: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(hash, metadata);
await pluginMetadataDb.put(url, metadata);
return metadata;
}
export async function removePluginMetadata(
hash: string,
url: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(hash);
const existing = await pluginMetadataDb.get(url);
if (!existing) return null;
await pluginMetadataDb.del(hash);
await pluginMetadataDb.del(url);
return existing;
}
@@ -299,34 +427,46 @@ export async function setConnection(origin: string) {
return true;
}
export async function setCookies(host: string, name: string, value: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearCookies(host: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).clear();
return true;
});
}
export async function getCookies(host: string, name: string) {
try {
const existing = await cookiesDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(host: string) {
export async function getCookiesByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of cookiesDb.sublevel(host).iterator()) {
ret[key] = value;
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;
}
@@ -346,51 +486,150 @@ export async function getConnection(origin: string) {
return null;
}
}
export async function setHeaders(host: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearHeaders(host: string) {
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).clear();
return true;
});
}
export async function getHeaders(host: string, name: string) {
try {
const existing = await headersDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getHeadersByHost(host: string) {
export async function getHeadersByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
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;
}
async function getDefaultPluginsInstalled(): Promise<boolean> {
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
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 setDefaultPluginsInstalled(installed = false) {
export async function resetDB() {
return mutex.runExclusive(async () => {
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
return Promise.all([
localStorageDb.clear(),
sessionStorageDb.clear(),
requestDb.clear(),
]);
});
}
export async function getAppState() {
return {
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
};
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,10 +1,9 @@
import { getCacheByTabId } from './cache';
import { BackgroundActiontype, RequestLog } from './rpc';
import { BackgroundActiontype } from './rpc';
import mutex from './mutex';
import browser from 'webextension-polyfill';
import { addRequest } from '../../reducers/requests';
import { urlify } from '../../utils/misc';
import { setCookies, setHeaders } from './db';
import { getRequestLog, upsertRequestLog } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
@@ -13,36 +12,22 @@ export const onSendHeaders = (
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const { hostname } = urlify(details.url) || {};
const { origin, pathname } = urlify(details.url) || {};
if (hostname && details.requestHeaders) {
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
value
.split(';')
.map((v) => v.split('='))
.forEach((cookie) => {
setCookies(hostname, cookie[0].trim(), cookie[1]);
});
} else {
setHeaders(hostname, name, value);
}
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(),
});
}
cache.set(requestId, {
...existing,
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
});
}
});
};
@@ -56,24 +41,30 @@ export const onBeforeRequest = (
if (method === 'OPTIONS') return;
if (requestBody) {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
if (requestBody.raw && requestBody.raw[0]?.bytes) {
try {
cache.set(requestId, {
...existing,
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) {
cache.set(requestId, {
...existing,
formData: 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(),
});
}
}
@@ -88,12 +79,7 @@ export const onResponseStarted = (
if (method === 'OPTIONS') return;
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],
...existing,
await upsertRequestLog({
method: details.method,
type: details.type,
url: details.url,
@@ -101,9 +87,15 @@ export const onResponseStarted = (
tabId: tabId,
requestId: requestId,
responseHeaders,
};
updatedAt: Date.now(),
});
cache.set(requestId, newLog);
const newLog = await getRequestLog(requestId);
if (!newLog) {
console.error('Request log not found', requestId);
return;
}
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,

View File

@@ -1,7 +1,6 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import { getAppState, setDefaultPluginsInstalled } from './db';
import { removePlugin, removeRequestLogsByTabId } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
@@ -30,22 +29,9 @@ import { installPlugin } from './plugins/utils';
);
browser.tabs.onRemoved.addListener((tabId) => {
deleteCacheByTabId(tabId);
removeRequestLogsByTabId(tabId);
});
const { defaultPluginsInstalled } = await getAppState();
if (!defaultPluginsInstalled) {
try {
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
await installPlugin(twitterProfileUrl);
await installPlugin(discordDmUrl);
} finally {
await setDefaultPluginsInstalled(true);
}
}
const { initRPC } = await import('./rpc');
await createOffscreenDocument();
initRPC();

View File

@@ -2,28 +2,37 @@ import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
import { getPluginConfig } from '../../../utils/misc';
export async function installPlugin(
urlOrBuffer: ArrayBuffer | string,
url: string,
origin = '',
filePath = '',
metadata: {[key: string]: string} = {},
) {
let arrayBuffer;
if (typeof urlOrBuffer === 'string') {
const resp = await fetch(urlOrBuffer);
arrayBuffer = await resp.arrayBuffer();
} else {
arrayBuffer = urlOrBuffer;
}
const 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);
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
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

@@ -0,0 +1,481 @@
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,40 +1,9 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { RequestHistory } from '../Background/rpc';
import { PluginConfig, PluginMetadata } from '../../utils/misc';
import { PresentationJSON } from '../../utils/types';
import { PresentationJSON } from 'tlsn-js/build/types';
const client = new RPCClient();
class TLSN {
async getHistory(
method: string,
url: string,
metadata?: {
[key: string]: string;
},
): Promise<
(Pick<
RequestHistory,
'id' | 'method' | 'notaryUrl' | 'url' | 'websocketProxyUrl'
> & { time: Date })[]
> {
const resp = await client.call(ContentScriptTypes.get_history, {
method,
url,
metadata,
});
return resp || [];
}
async getProof(id: string): Promise<PresentationJSON | null> {
const resp = await client.call(ContentScriptTypes.get_proof, {
id,
});
return resp || null;
}
async notarize(
url: string,
requestOptions?: {
@@ -67,37 +36,10 @@ class TLSN {
return resp;
}
async installPlugin(
url: string,
metadata?: { [k: string]: string },
): Promise<string> {
const resp = await client.call(ContentScriptTypes.install_plugin, {
async runPlugin(url: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin_by_url, {
url,
metadata,
});
return resp;
}
async getPlugins(
url: string,
origin?: string,
metadata?: {
[key: string]: string;
},
): Promise<(PluginConfig & { hash: string; metadata: PluginMetadata })[]> {
const resp = await client.call(ContentScriptTypes.get_plugins, {
url,
origin,
metadata,
});
return resp;
}
async runPlugin(hash: string) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
params,
});
return resp;
@@ -105,14 +47,12 @@ class TLSN {
}
const connect = async () => {
const resp = await client.call(ContentScriptTypes.connect);
if (resp) {
return new TLSN();
}
return new TLSN();
};
// @ts-ignore
window.tlsn = {
connect,
};
window.dispatchEvent(new CustomEvent('tlsn_loaded'));

View File

@@ -1,4 +1,4 @@
import browser from 'webextension-polyfill';
import browser, { browserAction } from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
@@ -7,69 +7,23 @@ import { urlify } from '../../utils/misc';
loadScript('content.bundle.js');
const server = new RPCServer();
server.on(ContentScriptTypes.connect, async () => {
const connected = await browser.runtime.sendMessage({
type: BackgroundActiontype.connect_request,
data: {
...getPopupData(),
},
});
if (!connected) throw new Error('user rejected.');
return connected;
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_local_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_local_storage,
data: { ...localStorage },
});
}
});
server.on(
ContentScriptTypes.get_history,
async (
request: ContentScriptRequest<{
method: string;
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
method: filterMethod,
url: filterUrl,
metadata,
} = request.params || {};
if (!filterMethod || !filterUrl)
throw new Error('params must include method and url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_request,
data: {
...getPopupData(),
method: filterMethod,
url: filterUrl,
metadata,
},
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_session_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_session_storage,
data: { ...sessionStorage },
});
return response;
},
);
server.on(
ContentScriptTypes.get_proof,
async (request: ContentScriptRequest<{ id: string }>) => {
const { id } = request.params || {};
if (!id) throw new Error('params must include id.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_request,
data: {
...getPopupData(),
id,
},
});
return proof;
},
);
}
});
server.on(
ContentScriptTypes.notarize,
@@ -121,73 +75,23 @@ import { urlify } from '../../utils/misc';
);
server.on(
ContentScriptTypes.install_plugin,
ContentScriptTypes.run_plugin_by_url,
async (
request: ContentScriptRequest<{
url: string;
metadata?: { [k: string]: string };
params?: Record<string, string>;
}>,
) => {
const { url, metadata } = request.params || {};
const { url, params } = request.params || {};
if (!url) throw new Error('params must include url.');
if (!url) throw new Error('params must include url');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_request,
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_request,
data: {
...getPopupData(),
url,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_plugins,
async (
request: ContentScriptRequest<{
url: string;
origin?: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
url: filterUrl,
origin: filterOrigin,
metadata,
} = request.params || {};
if (!filterUrl) throw new Error('params must include url.');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_request,
data: {
...getPopupData(),
url: filterUrl,
origin: filterOrigin,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.run_plugin,
async (request: ContentScriptRequest<{ hash: string }>) => {
const { hash } = request.params || {};
if (!hash) throw new Error('params must include hash');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_request,
data: {
...getPopupData(),
hash,
params,
},
});

View File

@@ -1,13 +1,8 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise';
export enum ContentScriptTypes {
connect = 'tlsn/cs/connect',
get_history = 'tlsn/cs/get_history',
get_proof = 'tlsn/cs/get_proof',
notarize = 'tlsn/cs/notarize',
install_plugin = 'tlsn/cs/install_plugin',
get_plugins = 'tlsn/cs/get_plugins',
run_plugin = 'tlsn/cs/run_plugin',
run_plugin_by_url = 'tlsn/cs/run_plugin_by_url',
}
export type ContentScriptRequest<params> = {

View File

@@ -1,132 +1,56 @@
import React, { useEffect } from 'react';
import * as Comlink from 'comlink';
import { OffscreenActionTypes } from './types';
import {
NotaryServer,
Prover as TProver,
Presentation as TPresentation,
Transcript,
} from 'tlsn-js';
import { verify } from 'tlsn-js-v5';
import { urlify } from '../../utils/misc';
import { BackgroundActiontype } from '../Background/rpc';
import browser from 'webextension-polyfill';
import { PresentationJSON } from '../../utils/types';
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
import { Method } from 'tlsn-js/wasm/pkg';
const { init, Prover, Presentation }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
import {
initThreads,
onCreatePresentationRequest,
onCreateProverRequest,
onNotarizationRequest,
onProcessProveRequest,
onVerifyProof,
onVerifyProofRequest,
startP2PProver,
startP2PVerifier,
} from './rpc';
const Offscreen = () => {
useEffect(() => {
(async () => {
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
});
await init({ loggingLevel });
await initThreads();
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case OffscreenActionTypes.notarization_request: {
const { id } = request.data;
(async () => {
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) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
error,
},
});
}
})();
onNotarizationRequest(request);
break;
}
case OffscreenActionTypes.create_prover_request: {
onCreateProverRequest(request);
break;
}
case OffscreenActionTypes.create_presentation_request: {
onCreatePresentationRequest(request);
break;
}
case BackgroundActiontype.process_prove_request: {
const { id } = request.data;
(async () => {
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
})();
onProcessProveRequest(request);
break;
}
case BackgroundActiontype.verify_proof: {
(async () => {
const result = await verifyProof(request.data);
sendResponse(result);
})();
onVerifyProof(request, sendResponse);
return true;
}
case BackgroundActiontype.verify_prove_request: {
(async () => {
const proof: PresentationJSON = request.data.proof;
const result: { sent: string; recv: 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,
},
},
});
})();
onVerifyProofRequest(request);
break;
}
case OffscreenActionTypes.start_p2p_verifier: {
startP2PVerifier(request);
break;
}
case OffscreenActionTypes.start_p2p_prover: {
startP2PProver(request);
break;
}
default:
@@ -140,164 +64,3 @@ const Offscreen = () => {
};
export default Offscreen;
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;
}
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<PresentationJSONa7> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders = [],
secretResps = [],
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
const transcript = await prover.transcript();
const commit = {
sent: subtractRanges(
transcript.ranges.sent.all,
secretHeaders
.map((secret: string) => {
const index = transcript.sent.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
recv: subtractRanges(
transcript.ranges.recv.all,
secretResps
.map((secret: string) => {
const index = transcript.recv.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
};
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,
})) as TPresentation;
const presentationJSON = await presentation.json();
return presentationJSON;
}
async function verifyProof(
proof: PresentationJSON,
): Promise<{ sent: string; recv: string }> {
let result: { sent: string; recv: string };
switch (proof.version) {
case undefined: {
result = await verify(proof);
break;
}
case '0.1.0-alpha.7': {
const presentation: TPresentation = await new Presentation(proof.data);
const verifierOutput = await presentation.verify();
const transcript = new Transcript({
sent: verifierOutput.transcript.sent,
recv: verifierOutput.transcript.recv,
});
result = {
sent: transcript.sent(),
recv: transcript.recv(),
};
break;
}
}
return result;
}

View File

@@ -0,0 +1,699 @@
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,4 +1,15 @@
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

@@ -0,0 +1,39 @@
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,8 +1,9 @@
import * as Comlink from 'comlink';
import init, { Prover, Presentation } from 'tlsn-js';
import init, { Prover, Presentation, Verifier } from 'tlsn-js';
Comlink.expose({
init,
Prover,
Presentation,
Verifier,
});

View File

@@ -1,14 +1,8 @@
import React, { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import {
setActiveTab,
setRequests,
useActiveTab,
useActiveTabUrl,
} from '../../reducers/requests';
import { setActiveTab, setRequests } from '../../reducers/requests';
import { BackgroundActiontype } from '../Background/rpc';
import Requests from '../../pages/Requests';
import Options from '../../pages/Options';
import Request from '../../pages/Requests/Request';
import Home from '../../pages/Home';
@@ -16,29 +10,23 @@ import logo from '../../assets/img/icon-128.png';
import RequestBuilder from '../../pages/RequestBuilder';
import Notarize from '../../pages/Notarize';
import ProofViewer from '../../pages/ProofViewer';
import History from '../../pages/History';
import ProofUploader from '../../pages/ProofUploader';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import PluginUploadInfo from '../../components/PluginInfo';
import ConnectionDetailsModal from '../../components/ConnectionDetailsModal';
import { ConnectionApproval } from '../../pages/ConnectionApproval';
import { GetHistoryApproval } from '../../pages/GetHistoryApproval';
import { GetProofApproval } from '../../pages/GetProofApproval';
import { isPopupWindow } from '../../utils/misc';
import { NotarizeApproval } from '../../pages/NotarizeApproval';
import { InstallPluginApproval } from '../../pages/InstallPluginApproval';
import { GetPluginsApproval } from '../../pages/GetPluginsApproval';
import { RunPluginApproval } from '../../pages/RunPluginApproval';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { getConnection } from '../Background/db';
import { useIsConnected, setConnection } from '../../reducers/requests';
import { MenuIcon } from '../../components/Menu';
import Plugins from '../../pages/Plugins';
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();
}, []);
useEffect(() => {
(async () => {
@@ -95,8 +83,11 @@ const Popup = () => {
onClick={() => navigate('/')}
/>
<div className="flex flex-row flex-grow items-center justify-end gap-4">
<AppConnectionLogo />
<MenuIcon />
{!isPopup && (
<>
<MenuIcon />
</>
)}
</div>
</div>
<Routes>
@@ -108,19 +99,13 @@ const Popup = () => {
<Route path="/requests" element={<Home tab="network" />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/plugins" element={<Plugins />} />
<Route path="/home" element={<Home />} />
<Route path="/plugininfo" element={<PluginUploadInfo />} />
<Route path="/connection-approval" element={<ConnectionApproval />} />
<Route path="/get-history-approval" element={<GetHistoryApproval />} />
<Route path="/get-proof-approval" element={<GetProofApproval />} />
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
<Route
path="/install-plugin-approval"
element={<InstallPluginApproval />}
path="/run-plugin-approval"
element={<RunPluginByUrlApproval />}
/>
<Route path="/p2p" element={<P2PHome />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
@@ -128,58 +113,3 @@ const Popup = () => {
};
export default Popup;
function AppConnectionLogo() {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (url) {
const isConnected: boolean | null = await getConnection(url?.origin);
dispatch(setConnection(!!isConnected));
}
})();
}, [url]);
return (
<div
className="flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
onClick={() => setShowConnectionDetails(true)}
>
<div className="flex flex-row relative bg-black border-[1px] border-black rounded-full">
{!!activeTab?.favIconUrl ? (
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
className="bg-white text-slate-400 rounded-full"
size={1.25}
/>
)}
<div
className={classNames(
'absolute right-[-2px] bottom-[-2px] rounded-full h-[10px] w-[10px] border-[2px]',
{
'bg-green-500': connected,
'bg-slate-500': !connected,
},
)}
/>
</div>
{showConnectionDetails && (
<ConnectionDetailsModal
showConnectionDetails={showConnectionDetails}
setShowConnectionDetails={setShowConnectionDetails}
/>
)}
</div>
);
}

View File

@@ -4,6 +4,11 @@ import { HashRouter } from 'react-router-dom';
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');

View File

@@ -1,44 +1,97 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import {
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
StepConfig,
InputFieldConfig,
} from '../../utils/misc';
import { PluginList } from '../../components/PluginList';
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 } from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import type { Plugin } from '@extism/extism';
import { OffscreenActionTypes } from '../Offscreen/types';
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 [hash, setHash] = useState('');
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(() => {
(async function () {
const result = await browser.storage.local.get('plugin_hash');
const { plugin_hash } = result;
const config = await getPluginConfigByHash(plugin_hash);
setHash(plugin_hash);
setConfig(config);
// await browser.storage.local.set({ plugin_hash: '' });
})();
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 relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
<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"
@@ -47,18 +100,38 @@ export default function SidePanel(): ReactElement {
Close
</button>
</div>
{!config && <PluginList />}
{config && <PluginBody hash={hash} config={config} />}
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
url={url}
hex={hex}
config={config}
p2p={p2p}
clientId={clientId}
presetParameterValues={params}
/>
)}
</div>
);
}
function PluginBody(props: {
function PluginBody({
url,
hex,
config,
p2p,
clientId,
presetParameterValues,
}: {
config: PluginConfig;
hash: string;
url: string;
hex?: string;
clientId?: string;
p2p?: boolean;
presetParameterValues?: Record<string, string>;
}): ReactElement {
const { hash } = props;
const { title, description, icon, steps } = props.config;
const { title, description, icon, steps } = config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
@@ -72,7 +145,7 @@ function PluginBody(props: {
setNotarizationId(response);
}
},
[hash, responses],
[url, responses],
);
useEffect(() => {
@@ -80,20 +153,31 @@ function PluginBody(props: {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
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: {
hash,
error: notaryRequest.error,
url,
error:
notaryRequest.errorMessage ||
notaryRequest.error ||
'Notarization failed',
},
});
}
}, [hash, notaryRequest?.status]);
}, [url, notaryRequest?.status, notaryRequest?.sessionId]);
return (
<div className="flex flex-col p-4">
@@ -109,11 +193,17 @@ function PluginBody(props: {
<div className="flex flex-col items-start gap-8 mt-8">
{steps?.map((step, i) => (
<StepContent
hash={hash}
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}
/>
))}
@@ -124,11 +214,16 @@ function PluginBody(props: {
function StepContent(
props: StepConfig & {
hash: string;
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 {
@@ -140,32 +235,70 @@ function StepContent(
setResponse,
lastResponse,
prover,
hash,
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 getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
const hex = (await getPluginByUrl(url)) || _hex;
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config!);
}, [hash]);
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 {
const out = await plugin.call(action, JSON.stringify(lastResponse));
const val = JSON.parse(out.string());
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 {
@@ -174,11 +307,20 @@ function StepContent(
setResponse(val, index);
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unkonwn error');
setError(e?.message || 'Unknown error');
} finally {
setPending(false);
}
}, [action, index, lastResponse, prover, getPlugin]);
}, [
action,
index,
lastResponse,
prover,
getPlugin,
inputs,
inputValues,
parameterValues,
]);
const onClick = useCallback(() => {
if (
@@ -209,13 +351,42 @@ function StepContent(
});
}, [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(() => {
processStep();
}, [processStep]);
// only auto-progress if this step does need inputs
if (!inputs || inputs.length === 0) {
processStep();
}
}, [processStep, inputs]);
let btnContent = null;
if (completed) {
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(
@@ -240,10 +411,33 @@ function StepContent(
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
btnContent = (
<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">{cta}</span>
</button>
<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 = (
@@ -273,9 +467,117 @@ function StepContent(
{!!description && (
<div className="text-slate-500 text-sm">{description}</div>
)}
{!!error && <div className="text-red-500 text-sm">{error}</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,4 +1,11 @@
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',
}

74
src/entries/utils.ts Normal file
View File

@@ -0,0 +1,74 @@
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from './Background/rpc';
import { SidePanelActionTypes } from './SidePanel/types';
import { deferredPromise } from '../utils/promise';
import { devlog } from '../utils/misc';
export const pushToRedux = async (action: {
type: string;
payload?: any;
error?: boolean;
meta?: any;
}) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action,
});
};
export const openSidePanel = async () => {
const { promise, resolve, reject } = deferredPromise();
try {
const response = await browser.runtime.sendMessage({
type: SidePanelActionTypes.is_panel_open,
});
if (response?.isOpen) {
await browser.runtime.sendMessage({
type: SidePanelActionTypes.reset_panel,
});
resolve();
return promise;
}
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
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 });
} catch (e) {
reject(e);
}
return promise;
};
export const waitForEvent = async (event: string) => {
const { promise, resolve } = deferredPromise();
const listener = async (request: any) => {
if (request.type === event) {
devlog('received event:', event);
browser.runtime.onMessage.removeListener(listener);
resolve(request);
}
};
browser.runtime.onMessage.addListener(listener);
return promise;
};

View File

@@ -28,7 +28,7 @@
],
"web_accessible_resources": [
{
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "discord_dm.wasm", "twitter_profile.wasm"],
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
}
],

View File

@@ -1,62 +0,0 @@
import React, { ReactElement, useCallback } from 'react';
import Icon from '../../components/Icon';
import logo from '../../assets/img/icon-128.png';
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';
export function ConnectionApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hostname = urlify(origin || '')?.hostname;
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.connect_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.connect_response,
data: true,
});
}, []);
return (
<BaseApproval
header={`Connecting to ${hostname}`}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 border border-slate-200 bg-slate-200 rounded-full"
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-sm font-semibold">{hostname}</div>
</div>
<div className="text-lg font-bold text-center">Connect to this site?</div>
<div className="text-sm px-8 text-center text-slate-500 flex-grow">
Do you trust this site? By granting this permission, you're allowing
this site to view your installed plugins, suggest requests to notarize,
suggest plugins to install, ask you to share proofs metadata{' '}
<i>(method, url, notary url, and proxy url)</i>, and ask to view a
specific proof.
</div>
</BaseApproval>
);
}

View File

@@ -1,139 +0,0 @@
import React, { ReactElement, useCallback, useEffect } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { safeParseJSON, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { minimatch } from 'minimatch';
import { useAllProofHistory } from '../../reducers/history';
import classNames from 'classnames';
export function GetHistoryApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const method = params.get('method');
const url = params.get('url');
const rawMetadata = params.get('metadata');
const metadata = safeParseJSON(rawMetadata);
const hostname = urlify(origin || '')?.hostname;
const proofs = useAllProofHistory();
useEffect(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_prove_requests,
});
}, []);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_response,
data: true,
});
}, []);
const result = proofs.filter((proof) => {
let matchedMetadata = true;
if (metadata) {
matchedMetadata = Object.entries(
metadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(proof.metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(proof.method, method!, { nocase: true }) &&
minimatch(proof.url, url!) &&
matchedMetadata
);
});
return (
<BaseApproval
header="Requesting Proof History"
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">
Do you want to share proof history with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
All proofs matching the following patterns with be shared:
</div>
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<tr>
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Method
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono text-left">
{method?.toUpperCase()}
</td>
</tr>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
URL
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{url}
</td>
</tr>
{rawMetadata && (
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Metadata
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{rawMetadata}
</td>
</tr>
)}
</tbody>
</table>
<div
className={classNames('border rounded font-semibold px-2 py-1', {
'text-green-500 bg-green-200 border-green-300': result.length,
'text-slate-500 bg-slate-200 border-slate-300': !result.length,
})}
>
{result.length} results found
</div>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
Only certain metadata will be shared with the app, such as <i>id</i>,{' '}
<i>method</i>, <i>url</i>, <i>notary</i>, <i>proxy</i>, and{' '}
<i>timestamp</i>.
</div>
</BaseApproval>
);
}

View File

@@ -1,138 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { safeParseJSON, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { getPlugins } from '../../entries/Background/db';
import { minimatch } from 'minimatch';
import classNames from 'classnames';
export function GetPluginsApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const filterOrigin = params.get('filterOrigin');
const rawMetadata = params.get('metadata');
const filterMetadata = safeParseJSON(rawMetadata);
const hostname = urlify(origin || '')?.hostname;
const [result, setResult] = useState<any[]>([]);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_response,
data: true,
});
}, []);
useEffect(() => {
(async () => {
const response = await getPlugins();
const res = response.filter(({ metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(metadata.filePath, url || '**') &&
minimatch(metadata.origin, filterOrigin || '**') &&
matchedMetadata
);
});
setResult(res);
})();
}, [url, filterMetadata]);
return (
<BaseApproval
header="Requesting Plugins"
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">
Do you want to share installed plugins with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
All plugins matching the following patterns with be shared:
</div>
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
URL
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{url}
</td>
</tr>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Origin
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{filterOrigin}
</td>
</tr>
{rawMetadata && (
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Metadata
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{rawMetadata}
</td>
</tr>
)}
</tbody>
</table>
<div
className={classNames('border rounded font-semibold px-2 py-1', {
'text-green-500 bg-green-200 border-green-300': result.length,
'text-slate-500 bg-slate-200 border-slate-300': !result.length,
})}
>
{result.length} results found
</div>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
Only certain metadata will be shared with the app, such as <i>id</i>,{' '}
<i>method</i>, <i>url</i>, <i>notary</i>, <i>proxy</i>, and{' '}
<i>timestamp</i>.
</div>
</BaseApproval>
);
}

View File

@@ -1,68 +0,0 @@
import React, { ReactElement, useCallback, useEffect } 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 { OneRequestHistory } from '../History';
export function GetProofApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const id = params.get('id');
const hostname = urlify(origin || '')?.hostname;
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_response,
data: true,
});
}, []);
return (
<BaseApproval
header="Requesting Proof History"
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">
Do you want to share proof data with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
The following proof will be shared:
</div>
<OneRequestHistory
className="w-full !cursor-default hover:bg-white text-xs"
requestId={id!}
hideActions={['share', 'delete', 'retry']}
/>
</div>
</BaseApproval>
);
}

View File

@@ -1,30 +1,27 @@
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} from '../../reducers/history';
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { urlify, download, upload } from '../../utils/misc';
import { BackgroundActiontype } from '../../entries/Background/rpc';
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 copy from 'copy-to-clipboard';
import { EXPLORER_API } from '../../utils/constants';
import {
getNotaryRequest,
setNotaryRequestCid,
} from '../../entries/Background/db';
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">
<div className="flex flex-col flex-nowrap overflow-y-auto pb-36">
{history
.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
@@ -43,42 +40,11 @@ export function OneRequestHistory(props: {
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
const [uploadError, setUploadError] = useState('');
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [cid, setCid] = useState<{ [key: string]: string }>({});
const [uploading, setUploading] = useState(false);
const [showingMenu, showMenu] = useState(false);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
useEffect(() => {
const fetchData = async () => {
try {
const request = await getNotaryRequest(props.requestId);
if (request && request.cid) {
setCid({ [props.requestId]: request.cid });
}
} catch (e) {
console.error('Error fetching data', e);
}
};
fetchData();
}, []);
const onRetry = useCallback(async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
id: props.requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [props.requestId]);
const onView = useCallback(() => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
@@ -87,151 +53,97 @@ export function OneRequestHistory(props: {
navigate('/verify/' + request?.id);
}, [request]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(props.requestId));
}, [props.requestId]);
const onShowError = useCallback(async () => {
showError(true);
}, [request?.error, showError]);
const closeAllModal = useCallback(() => {
setShowingShareConfirmation(false);
showError(false);
}, [setShowingShareConfirmation, showError]);
}, [showError]);
const handleUpload = useCallback(async () => {
setUploading(true);
try {
const data = await upload(
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
setCid((prevCid) => ({ ...prevCid, [props.requestId]: data }));
await setNotaryRequestCid(props.requestId, data);
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [props.requestId, request, cid]);
const day = dayjs(charwise.decode(props.requestId, 'hex'));
return (
<div
className={classNames(
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
'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();
}}
>
<ShareConfirmationModal />
<ErrorModal />
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
<div className="flex flex-row items-center text-xs">
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
{request?.method}
</div>
<div className="text-black font-bold px-2 py-1 rounded-md overflow-hidden text-ellipsis">
{requestUrl?.pathname}
</div>
<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="flex flex-row">
<div className="font-bold text-slate-400">Time:</div>
<div className="ml-2 text-slate-800">
{new Date(charwise.decode(props.requestId, 'hex')).toISOString()}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Host:</div>
<div className="ml-2 text-slate-800">{requestUrl?.host}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Notary API:</div>
<div className="ml-2 text-slate-800">{request?.notaryUrl}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">TLS Proxy API:</div>
<div className="ml-2 text-slate-800">
{request?.websocketProxyUrl}
</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 gap-1">
{status === 'success' && (
<>
<ActionButton
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
onClick={onView}
fa="fa-solid fa-receipt"
ctaText="View"
hidden={hideActions.includes('view')}
/>
<ActionButton
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
onClick={() =>
download(`${request?.id}.json`, JSON.stringify(request?.proof))
}
fa="fa-solid fa-download"
ctaText="Download"
hidden={hideActions.includes('download')}
/>
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={() => setShowingShareConfirmation(true)}
fa="fa-solid fa-upload"
ctaText="Share"
hidden={hideActions.includes('share')}
/>
</>
)}
{status === 'error' && !!request?.error && (
<ErrorButton hidden={hideActions.includes('error')} />
)}
{(!status || status === 'error') && (
<RetryButton hidden={hideActions.includes('retry')} />
)}
{status === 'pending' && (
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-xs font-bold">Pending</span>
</button>
)}
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
onClick={onDelete}
fa="fa-solid fa-trash"
ctaText="Delete"
hidden={hideActions.includes('delete')}
/>
<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 RetryButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={onRetry}
>
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
<span className="text-xs font-bold">Retry</span>
</button>
);
}
function ErrorButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
onClick={onShowError}
>
<Icon fa="fa-solid fa-circle-exclamation" size={1} />
<span className="text-xs font-bold">Error</span>
</button>
);
}
function ErrorModal(): ReactElement {
const msg = typeof request?.error === 'string' && request?.error;
return !showingError ? (
@@ -242,7 +154,7 @@ export function OneRequestHistory(props: {
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || 'Something went wrong :('}
{msg || request?.errorMessage}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
@@ -253,99 +165,4 @@ export function OneRequestHistory(props: {
</Modal>
);
}
function ShareConfirmationModal(): ReactElement {
return !showingShareConfirmation ? (
<></>
) : (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={closeAllModal}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!cid[props.requestId] ? (
<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/${cid[props.requestId]}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!cid[props.requestId] ? (
<>
{!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={closeAllModal}
>
Close
</button>
</>
) : (
<>
<button
onClick={() =>
copy(`${EXPLORER_API}/ipfs/${cid[props.requestId]}`)
}
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={closeAllModal}
>
Close
</button>
</>
)}
</div>
</Modal>
);
}
}
function ActionButton(props: {
onClick: () => void;
fa: string;
ctaText: string;
className?: string;
hidden?: boolean;
}): ReactElement {
if (props.hidden) return <></>;
return (
<button
className={classNames(
'flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 hover:font-bold',
props.className,
)}
onClick={props.onClick}
>
<Icon className="" fa={props.fa} size={1} />
<span className="text-xs font-bold">{props.ctaText}</span>
</button>
);
}

View File

@@ -0,0 +1,307 @@
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,61 +1,50 @@
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { ErrorModal } from '../../components/ErrorModal';
import History from '../History';
import './index.scss';
import Requests from '../Requests';
import PluginUploadInfo from '../../components/PluginInfo';
import {
useOnPluginClick,
usePluginConfig,
usePluginHashes,
} from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { fetchPluginHashes } from '../../utils/rpc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
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'>(props.tab || 'history');
const [tab, setTab] = useState<'history' | 'network' | 'plugins'>(
props.tab || 'history',
);
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [actionPanelElement, setActionPanelElement] =
useState<HTMLDivElement | null>(null);
const [scrollTop, setScrollTop] = useState(0);
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;
if (!actionPanelElement) return;
let timer = Date.now();
const onScroll = () => {
const now = Date.now();
if (now - timer > 20) {
timer = now;
setScrollTop(element.scrollTop);
if (element.scrollTop >= actionPanelElement.clientHeight) {
setFix(true);
} else {
setFix(false);
}
if (element.scrollTop > 0) {
setFix(true);
} else {
setFix(false);
}
};
@@ -64,7 +53,7 @@ export default function Home(props: {
return () => {
element.removeEventListener('scroll', onScroll);
};
}, [scrollableContent, actionPanelElement]);
}, [scrollableContent]);
return (
<div
@@ -73,165 +62,54 @@ export default function Home(props: {
className="flex flex-col flex-grow overflow-y-auto"
>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<ActionPanel
setActionPanelElement={setActionPanelElement}
scrollTop={scrollTop}
/>
<div
className={classNames('flex flex-row justify-center items-center', {
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40': shouldFix,
})}
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,
},
)}
>
<TabSelector
onClick={() => setTab('network')}
selected={tab === 'network'}
>
Network
</TabSelector>
{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' && <Requests shouldFix={shouldFix} />}
{tab === 'network' && developerMode && (
<Requests shouldFix={shouldFix} />
)}
{tab === 'plugins' && (
<PluginList
className="p-2 overflow-y-auto"
showAddButton={developerMode}
/>
)}
</div>
</div>
);
}
function ActionPanel({
setActionPanelElement,
scrollTop,
}: {
scrollTop: number;
setActionPanelElement: (el: HTMLDivElement) => void;
}) {
const pluginHashes = usePluginHashes();
const navigate = useNavigate();
const container = useRef<HTMLDivElement | null>(null);
const [isOverflow, setOverflow] = useState(false);
const [expanded, setExpand] = useState(false);
useEffect(() => {
const element = container.current;
if (!element) return;
setActionPanelElement(element);
const onCheckSize = () => {
if (element.scrollWidth > element.clientWidth) {
setOverflow(true);
} else {
setOverflow(false);
}
};
onCheckSize();
window.addEventListener('resize', onCheckSize);
return () => {
window.removeEventListener('resize', onCheckSize);
};
}, [container, pluginHashes]);
useEffect(() => {
const element = container.current;
if (!element) return;
if (scrollTop >= element.clientHeight) {
setExpand(false);
}
}, [container, scrollTop]);
return (
<div
ref={container}
className={classNames(
'flex flex-row justify-start items-center gap-4 p-4 border-b relative',
{
'flex-wrap': expanded,
'flex-nowrap': !expanded,
},
)}
>
<NavButton
fa="fa-solid fa-hammer"
onClick={() => navigate('/custom')}
title="Build a custom request"
>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
title="Visualize an attestation"
>
Verify
</NavButton>
{pluginHashes.map((hash) => (
<PluginIcon hash={hash} />
))}
<button
className={
'flex flex-row items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
}
title="Install a plugin"
>
<PluginUploadInfo />
<Icon fa="fa-solid fa-plus" />
</button>
<button
className={classNames(
'absolute right-0 top-0 w-6 h-full bg-slate-100 hover:bg-slate-200 font-semibold',
'flex flex-row items-center justify-center gap-2 text-slate-500 hover:text-slate-700',
{
hidden: !isOverflow || expanded,
},
)}
onClick={() => setExpand(true)}
>
<Icon fa="fa-solid fa-caret-down" size={0.875} />
</button>
</div>
);
}
function PluginIcon({ hash }: { hash: string }) {
const config = usePluginConfig(hash);
const onPluginClick = useOnPluginClick(hash);
const onClick = useCallback(() => {
if (!config) return;
onPluginClick();
}, [onPluginClick, config]);
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
)}
onClick={onClick}
>
<Icon
className="rounded-full flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
url={config?.icon || DefaultPluginIcon}
size={2}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
{config?.title}
</span>
</button>
);
}
function TabSelector(props: {
children: string;
className?: string;
@@ -254,34 +132,3 @@ function TabSelector(props: {
</button>
);
}
function NavButton(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
title?: string;
disabled?: boolean;
}): ReactElement {
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
props.className,
)}
onClick={props.onClick}
disabled={props.disabled}
title={props.title}
>
<Icon
className="w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
fa={props.fa}
size={0.875}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
{props.children}
</span>
</button>
);
}

View File

@@ -1,108 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import {
getPluginConfig,
makePlugin,
type PluginConfig,
urlify,
} from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
export function InstallPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const rawMetadata = params.get('metadata');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_response,
data: true,
});
}, []);
useEffect(() => {
(async () => {
try {
const resp = await fetch(url!);
const arrayBuffer = await resp.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
})();
}, [url]);
return (
<BaseApproval
header={`Installing 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 install 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 flex-grow gap-4 border border-slate-300 p-8 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-3xl text-blue-600 font-semibold">
{pluginContent.title}
</span>
<div className="text-slate-500 text-lg">
{pluginContent.description}
</div>
</div>
<PluginPermissions className="w-full" pluginContent={pluginContent} />
</div>
)}
</BaseApproval>
);
}

View File

@@ -17,6 +17,10 @@ import {
getProxyApi,
getLoggingFilter,
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
getDeveloperMode,
DEVELOPER_MODE_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -24,11 +28,13 @@ import {
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);
@@ -36,19 +42,38 @@ export default function Options(): ReactElement {
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 () => {
setNotary((await getNotaryApi()) || NOTARY_API);
setProxy((await getProxyApi()) || NOTARY_PROXY);
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || 'Info');
setRendezvous((await getRendezvousApi()) || RENDEZVOUS_API);
})();
}, [advanced]);
@@ -63,9 +88,20 @@ export default function Options(): ReactElement {
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, shouldReload],
[
notary,
proxy,
maxSent,
maxReceived,
loggingLevel,
rendezvous,
developerMode,
shouldReload,
],
);
const onSaveAndReload = useCallback(
@@ -84,8 +120,15 @@ export default function Options(): ReactElement {
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">
<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"
@@ -120,6 +163,8 @@ export default function Options(): ReactElement {
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}>
@@ -145,6 +190,8 @@ export default function Options(): ReactElement {
loggingLevel={loggingLevel}
setLoggingLevel={setLoggingLevel}
setShouldReload={setShouldReload}
rendezvous={rendezvous}
setRendezvous={setRendezvous}
/>
)}
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
@@ -171,6 +218,15 @@ export default function Options(): ReactElement {
>
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>
);
@@ -207,8 +263,18 @@ function NormalOptions(props: {
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
developerMode: boolean;
setDeveloperMode: (value: boolean) => void;
}) {
const { notary, setNotary, proxy, setProxy, setDirty } = props;
const {
notary,
setNotary,
proxy,
setProxy,
setDirty,
developerMode,
setDeveloperMode,
} = props;
return (
<div>
@@ -240,6 +306,33 @@ function NormalOptions(props: {
<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>
);
}
@@ -248,11 +341,13 @@ 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,
@@ -263,6 +358,8 @@ function AdvancedOptions(props: {
setLoggingLevel,
loggingLevel,
setShouldReload,
rendezvous,
setRendezvous,
} = props;
return (

View File

@@ -0,0 +1,497 @@
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

@@ -7,14 +7,17 @@ import 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
@@ -26,8 +29,19 @@ export default function ProofUploader(): ReactElement {
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 }>({
.sendMessage<
any,
{
recv: string;
sent: string;
verifierKey?: string;
notaryKey?: string;
}
>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
@@ -48,7 +62,15 @@ export default function ProofUploader(): ReactElement {
);
if (proof) {
return <ProofViewer recv={proof.recv} sent={proof.sent} />;
return (
<ProofViewer
recv={proof.recv}
sent={proof.sent}
verifierKey={proof.verifierKey}
notaryKey={proof.notaryKey}
info={metadata}
/>
);
}
return (

View File

@@ -2,41 +2,99 @@ import React, {
ReactNode,
ReactElement,
useState,
useEffect,
MouseEventHandler,
useCallback,
} from 'react';
import { useParams, useNavigate } from 'react-router';
import c from 'classnames';
import { useRequestHistory } from '../../reducers/history';
import {
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import { download } from '../../utils/misc';
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="flex flex-col w-full py-2 gap-2 flex-grow">
<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">
<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"
/>
{!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
@@ -49,6 +107,12 @@ export default function ProofViewer(props?: {
Download
</button>
)}
<button
className="button !text-red-500"
onClick={() => setShowRemoveModal(true)}
>
Delete
</button>
</div>
</div>
</div>
@@ -67,11 +131,70 @@ export default function ProofViewer(props?: {
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;
@@ -90,3 +213,20 @@ function TabLabel(props: {
</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

@@ -7,62 +7,91 @@ import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
import {
getPluginConfigByHash,
getPluginMetadataByHash,
getPluginConfigByUrl,
getPluginMetadataByUrl,
getPluginByUrl,
} from '../../entries/Background/db';
import { runPlugin } from '../../utils/rpc';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { deferredPromise } from '../../utils/promise';
import { installPlugin } from '../../entries/Background/plugins/utils';
export function RunPluginApproval(): ReactElement {
export function RunPluginByUrlApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hash = params.get('hash');
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);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!hash) return;
try {
const tab = await browser.tabs.create({
active: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [hash]);
useEffect(() => {
if (!url) return;
(async () => {
if (!hash) return;
try {
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
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');
}
})();
}, [hash]);
}, [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

View File

@@ -1,6 +1,7 @@
import {
BackgroundActiontype,
RequestHistory,
RequestProgress,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
@@ -10,6 +11,7 @@ enum ActionType {
'/history/addRequest' = '/history/addRequest',
'/history/setRequests' = '/history/setRequests',
'/history/deleteRequest' = '/history/deleteRequest',
'/history/addRequestCid' = '/history/addRequestCid',
}
type Action<payload> = {
@@ -45,6 +47,13 @@ export const setRequests = (requests: RequestHistory[]) => {
};
};
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,
@@ -68,6 +77,9 @@ export default function history(
if (!payload) return state;
const existing = state.map[payload.id];
if (existing?.progress === RequestProgress.Error) {
return state;
}
const newMap = {
...state.map,
[payload.id]: payload,
@@ -82,13 +94,20 @@ export default function history(
}
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: payload.reduce((map: { [id: string]: RequestHistory }, req) => {
map[req.id] = req;
return map;
}, {}),
map: newMap,
order: payload.map(({ id }) => id),
};
}
@@ -103,6 +122,20 @@ export default function history(
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;
}

View File

@@ -2,11 +2,13 @@ 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,
});
export type AppRootState = ReturnType<typeof rootReducer>;

375
src/reducers/p2p.ts Normal file
View File

@@ -0,0 +1,375 @@
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,11 +1,9 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { useCallback, useEffect, useState } from 'react';
import { getPluginConfigByHash } from '../entries/Background/db';
import { useEffect, useState } from 'react';
import { getPluginConfigByUrl } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
import { runPlugin } from '../utils/rpc';
import browser from 'webextension-polyfill';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
@@ -62,26 +60,8 @@ export const usePluginConfig = (hash: string) => {
const [config, setConfig] = useState<PluginConfig | null>(null);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(hash));
setConfig(await getPluginConfigByUrl(hash));
})();
}, [hash]);
return config;
};
export const useOnPluginClick = (hash: string) => {
return useCallback(async () => {
await runPlugin(hash, 'start');
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
window.close();
}, [hash]);
};

View File

@@ -1,5 +1,6 @@
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.7';
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,5 +1,6 @@
import {
BackgroundActiontype,
handleExecP2PPluginProver,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
@@ -13,8 +14,12 @@ import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
import {
getCookiesByHost,
getHeadersByHost,
getLocalStorageByHost,
getSessionStorageByHost,
} from '../entries/Background/db';
const charwise = require('charwise');
export function urlify(
@@ -84,6 +89,19 @@ export const copyText = async (text: string): Promise<void> => {
}
};
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,
@@ -148,6 +166,10 @@ const VALID_HOST_FUNCS: { [name: string]: string } = {
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 });
@@ -166,11 +188,13 @@ export const makePlugin = async (
} = {
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');
@@ -200,40 +224,53 @@ export const makePlugin = async (
(async () => {
const {
url,
method,
headers,
getSecretResponse,
body: reqBody,
interactive,
verifierPlugin,
} = params;
let secretResps;
const resp = await fetch(url, {
method,
headers,
body: reqBody,
});
const body = await extractBodyFromResponse(resp);
if (getSecretResponse) {
const out = await plugin.call(getSecretResponse, body);
secretResps = JSON.parse(out.string());
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,
},
});
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
secretResps,
now,
},
});
})();
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
@@ -250,21 +287,61 @@ export const makePlugin = async (
}
}
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: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = await getCookiesByHost(host);
cookies[host] = cache;
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: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.headers) {
const cache = await getHeadersByHost(host);
headers[host] = cache;
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);
@@ -272,7 +349,10 @@ export const makePlugin = async (
const pluginConfig: ExtismPluginOptions = {
useWasi: true,
config: injectedConfig,
config: {
...injectedConfig,
tabId: tab.id?.toString() || '',
},
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
functions: {
'extism:host/user': funcs,
@@ -283,22 +363,35 @@ export const makePlugin = async (
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's purpose
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)
@@ -314,6 +407,7 @@ export const getPluginConfig = async (
): 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);
@@ -348,7 +442,16 @@ export const getPluginConfig = async (
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);
@@ -362,6 +465,23 @@ export const getPluginConfig = async (
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');
}
}
}
}
}
}
@@ -390,3 +510,9 @@ export function safeParseJSON(data?: string | null) {
return null;
}
}
export function isPopupWindow(): boolean {
return (
!!window.opener || window.matchMedia('(display-mode: standalone)').matches
);
}

33
src/utils/parser.ts Normal file
View File

@@ -0,0 +1,33 @@
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

@@ -13,7 +13,7 @@ export const HostFunctionsDescriptions: {
);
},
notarize: ({ notaryUrls, proxyUrls }) => {
const notaries = ['default notary'].concat(notaryUrls || []);
const notaries = ['default notary', 'your peer'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (

View File

@@ -2,10 +2,10 @@ import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
export async function addPlugin(hex: string) {
export async function addPlugin(hex: string, url: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,
data: hex,
data: { hex, url },
});
}
@@ -38,7 +38,12 @@ export async function fetchPluginConfigByHash(
});
}
export async function runPlugin(hash: string, method: string, params?: string) {
export async function runPlugin(
hash: string,
method: string,
params?: string,
meta?: any,
) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin,
data: {
@@ -46,5 +51,6 @@ export async function runPlugin(hash: string, method: string, params?: string) {
method,
params,
},
meta,
});
}

View File

@@ -1,11 +1,14 @@
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 });
@@ -37,3 +40,12 @@ export async function getProxyApi() {
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

@@ -1,10 +0,0 @@
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
export type PresentationJSON = PresentationJSONa5 | PresentationJSONa7;
export type PresentationJSONa5 = {
version?: undefined;
session: any;
substrings: any;
notaryUrl: string;
};

View File

@@ -22,6 +22,28 @@ config.plugins = (config.plugins || []).concat(
}),
);
webpack(config, function (err) {
if (err) throw err;
webpack(config, function (err, stats) {
if (err) {
console.error('Webpack error:', err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error('Build failed with errors:');
const info = stats.toJson();
console.error(info.errors.map((e) => e.message).join('\n\n'));
process.exit(1);
}
if (stats.hasWarnings()) {
console.warn('Build completed with warnings:');
const info = stats.toJson();
console.warn(info.warnings.map((w) => w.message).join('\n\n'));
}
console.log('Build completed successfully!');
console.log(`Output: ${path.join(__dirname, '../', 'build')}`);
console.log(
`Zip: ${path.join(__dirname, '../', 'zip', `${packageInfo.name}-${packageInfo.version}.zip`)}`,
);
});

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 ReactRefreshTypeScript = require("react-refresh-typescript");
var ExtReloader = require('webpack-ext-reloader');
const ASSET_PATH = process.env.ASSET_PATH || "/";
@@ -42,6 +41,9 @@ var options = {
/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: {
@@ -111,11 +113,6 @@ var options = {
{
loader: require.resolve("ts-loader"),
options: {
getCustomTransformers: () => ({
before: [isDevelopment && ReactRefreshTypeScript()].filter(
Boolean
),
}),
transpileOnly: isDevelopment,
},
},
@@ -208,16 +205,6 @@ var options = {
to: path.join(__dirname, "build"),
force: true,
},
{
from: "src/assets/plugins/discord_dm.wasm",
to: path.join(__dirname, "build"),
force: true,
},
{
from: "src/assets/plugins/twitter_profile.wasm",
to: path.join(__dirname, "build"),
force: true,
},
],
}),
new HtmlWebpackPlugin({

View File

@@ -1,6 +0,0 @@
api.twitter.com: api.twitter.com:443
twitter.com: twitter.com:443
api.reddit.com: api.reddit.com:443
reddit.com: reddit.com:443
gateway.reddit.com: gateway.reddit.com:443
swapi.dev: swapi.dev:443