mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-09 13:08:04 -05:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
303448a3c1 | ||
|
|
46674491c6 | ||
|
|
0ba4a71bba | ||
|
|
ade6d7e575 | ||
|
|
3f37c1aee8 | ||
|
|
5545d7abed | ||
|
|
b674f5c579 | ||
|
|
49a94e1bbc | ||
|
|
64d572d663 | ||
|
|
2f35278235 | ||
|
|
5af783e48c | ||
|
|
1ea69d0574 | ||
|
|
8bfcf8c8d5 | ||
|
|
fe825d7ebb | ||
|
|
ca4986c3bb | ||
|
|
da756721d4 | ||
|
|
38852620d2 | ||
|
|
1c9f340add | ||
|
|
423be796f6 | ||
|
|
7631baf939 | ||
|
|
8733b26e12 | ||
|
|
846bc1ef29 | ||
|
|
efb4386d1b | ||
|
|
06dc4fac83 | ||
|
|
3b98fc3b2f | ||
|
|
e3166d62a0 | ||
|
|
ca382f3532 | ||
|
|
b8d068b828 | ||
|
|
4c908d0611 | ||
|
|
ed3e797bad | ||
|
|
398950598e | ||
|
|
f3b8cc1066 | ||
|
|
bd8e58c042 | ||
|
|
fb4c81b851 | ||
|
|
2db735e04f | ||
|
|
f6e582016b | ||
|
|
21ebcd1a11 | ||
|
|
5063d6cb45 | ||
|
|
3d5e3ce4ac | ||
|
|
d47cf0d8ea | ||
|
|
25f35d0051 | ||
|
|
5ccdd9b06a | ||
|
|
186f77d3cb | ||
|
|
08c4f74479 | ||
|
|
737cc10af7 | ||
|
|
d1cbc34126 | ||
|
|
30a7b7b36b | ||
|
|
4c78625f12 | ||
|
|
d15d021b4a | ||
|
|
be27560631 | ||
|
|
a018acb7bf | ||
|
|
d9dacdfb14 | ||
|
|
998c9c091e | ||
|
|
c04556620c | ||
|
|
4b473273dc | ||
|
|
585a8f2d3d | ||
|
|
1c29fee920 | ||
|
|
884e55dccf | ||
|
|
87e96c0f50 | ||
|
|
3b8cd0fba3 | ||
|
|
c41b4ff401 | ||
|
|
047eb673f3 | ||
|
|
b8d2ba06d7 | ||
|
|
ca1ea2b34e | ||
|
|
53ba6f69b8 | ||
|
|
217824f2bf | ||
|
|
869fa5eeaa | ||
|
|
92f9d65c23 | ||
|
|
c481ee6bbf | ||
|
|
810e7bf415 | ||
|
|
8348756f0a | ||
|
|
9890604391 | ||
|
|
763335b0b1 | ||
|
|
71cef56356 | ||
|
|
25689017b0 | ||
|
|
c68e2e1548 | ||
|
|
8bb76ad969 | ||
|
|
d55279501e | ||
|
|
2331074c9c | ||
|
|
a42bb2eabd | ||
|
|
76c6acd998 | ||
|
|
d0024077f9 | ||
|
|
42ab67eb24 | ||
|
|
028a3b5444 | ||
|
|
d60d6a3ff3 | ||
|
|
33ad9acca5 | ||
|
|
6503281d75 | ||
|
|
5f7bc6dae0 | ||
|
|
bc4e77b8f1 | ||
|
|
8ec3e37c92 | ||
|
|
220c138290 | ||
|
|
eb505cf234 | ||
|
|
04485168e1 | ||
|
|
fec058fd7c | ||
|
|
042fba9c09 | ||
|
|
1da4f45564 | ||
|
|
c8f2b541d6 | ||
|
|
610b07f4b3 | ||
|
|
217a18ea9d | ||
|
|
a9e0924a1d | ||
|
|
c2b3b9dcfa | ||
|
|
72efee69ff | ||
|
|
821f2734f9 | ||
|
|
51dcb29e7e | ||
|
|
629a519112 | ||
|
|
99d1922aa8 | ||
|
|
bb204533db | ||
|
|
3be3b5997c | ||
|
|
0379540ca8 | ||
|
|
5acf70686e | ||
|
|
d09aeca74b | ||
|
|
03b54a53d6 | ||
|
|
d9e8e813b0 | ||
|
|
33ff881ec8 | ||
|
|
7c95e9e6f8 |
@@ -1,10 +0,0 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+atomics,+bulk-memory,+mutable-globals",
|
||||
"-C",
|
||||
"link-arg=--max-memory=4294967296"
|
||||
]
|
||||
|
||||
[unstable]
|
||||
build-std = ["panic_abort", "std"]
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
root: true,
|
||||
"root": true,
|
||||
"extends": ["prettier", "plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["prettier", "@typescript-eslint"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
@@ -7,6 +7,7 @@
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-explicit-any": 1,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"no-undef": "error",
|
||||
"padding-line-between-statements": "error"
|
||||
},
|
||||
@@ -30,6 +31,7 @@
|
||||
"wasm",
|
||||
"tlsn",
|
||||
"util",
|
||||
"plugins",
|
||||
"webpack.config.js"
|
||||
]
|
||||
}
|
||||
46
.github/workflows/build.yaml
vendored
46
.github/workflows/build.yaml
vendored
@@ -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
|
||||
70
.github/workflows/ci.yaml
vendored
Normal file
70
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
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: Download extension from build-lint-test job
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tlsn-extension-${{ github.ref_name }}.zip
|
||||
path: ./tlsn-extension-${{ github.ref_name }}.zip
|
||||
|
||||
- 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 }}
|
||||
46
.github/workflows/lint.yaml
vendored
46
.github/workflows/lint.yaml
vendored
@@ -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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,11 +2,10 @@
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
wasm-pack.log
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
.idea
|
||||
build
|
||||
tlsn/
|
||||
zip
|
||||
wasm/prover/target/
|
||||
.vscode
|
||||
53
README.md
53
README.md
@@ -1,31 +1,58 @@
|
||||
![MIT licensed][mit-badge]
|
||||
![Apache licensed][apache-badge]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[apache-badge]: https://img.shields.io/github/license/saltstack/salt
|
||||
[actions-badge]: https://github.com/tlsnotary/tlsn-extension/actions/workflows/build.yaml/badge.svg
|
||||
[actions-url]: https://github.com/tlsnotary/tlsn-extension/actions?query=workflow%3Abuild+branch%3Amain++
|
||||
|
||||
<img src="src/assets/img/icon-128.png" width="64"/>
|
||||
|
||||
# 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
|
||||
|
||||
## License
|
||||
This repository is licensed under either of
|
||||
|
||||
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [MIT license](http://opensource.org/licenses/MIT)
|
||||
|
||||
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.
|
||||
3. Run `npm install` to install the dependencies.
|
||||
4. Run `npm run build:wasm`
|
||||
5. Run `npm run dev`
|
||||
6. Load your extension on Chrome following:
|
||||
4. Run `npm run dev`
|
||||
5. Load your extension on Chrome following:
|
||||
1. Access `chrome://extensions/`
|
||||
2. Check `Developer mode`
|
||||
3. Click on `Load unpacked extension`
|
||||
4. Select the `build` folder.
|
||||
7. Happy hacking.
|
||||
|
||||
## Webpack auto-reload and HRM
|
||||
|
||||
To make your workflow much more efficient this boilerplate uses the [webpack server](https://webpack.github.io/docs/webpack-dev-server.html) to development (started with `npm start`) with auto reload feature that reloads the browser automatically every time that you save some file in your editor.
|
||||
|
||||
You can run the dev mode on other port if you want. Just specify the env var `port` like this:
|
||||
6. Happy hacking.
|
||||
|
||||
## Building Websockify Docker Image
|
||||
```
|
||||
$ PORT=6002 npm run start
|
||||
$ git clone https://github.com/novnc/websockify && cd websockify
|
||||
$ ./docker/build.sh
|
||||
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.x.com:443
|
||||
```
|
||||
|
||||
## Running Websockify Docker Image
|
||||
```
|
||||
$ cd tlsn-extension
|
||||
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.twitter.com:443
|
||||
```
|
||||
|
||||
## Packing
|
||||
@@ -42,5 +69,3 @@ Now, the content of `build` folder will be the extension ready to be submitted t
|
||||
|
||||
- [Webpack documentation](https://webpack.js.org/concepts/)
|
||||
- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)
|
||||
- [wasm-bindgen-rayon](https://github.com/GoogleChromeLabs/wasm-bindgen-rayon)
|
||||
- [wasm-unsafe-eval CSP](https://bugs.chromium.org/p/chromium/issues/detail?id=1173354#c60)
|
||||
|
||||
33
cog.toml
Normal file
33
cog.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
branch_whitelist = []
|
||||
disable_changelog = false
|
||||
from_latest_tag = false
|
||||
generate_mono_repository_global_tag = true
|
||||
ignore_merge_commits = false
|
||||
post_bump_hooks = []
|
||||
post_package_bump_hooks = []
|
||||
pre_bump_hooks = [
|
||||
"echo {{version}}",
|
||||
]
|
||||
pre_package_bump_hooks = []
|
||||
skip_ci = "[skip ci]"
|
||||
skip_untracked = false
|
||||
|
||||
[git_hooks]
|
||||
|
||||
[commit_types]
|
||||
|
||||
[changelog]
|
||||
authors = [
|
||||
{username = "0xtsukino", signature = "tsukino"},
|
||||
{username = "heeckhau", signature = "Hendrik Eeckhaut"},
|
||||
{username = "mhchia", signature = "Kevin Mai-Husan Chia"},
|
||||
]
|
||||
owner = "TLSNotary"
|
||||
path = "CHANGELOG.md"
|
||||
remote = "github.com"
|
||||
repository = "tlsn-extension"
|
||||
template = "remote"
|
||||
|
||||
[bump_profiles]
|
||||
|
||||
[packages]
|
||||
15508
package-lock.json
generated
Normal file
15508
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0.1000",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,22 +8,28 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clone:tlsn": "bash ./utils/download-tlsn.sh",
|
||||
"build:wasm": "wasm-pack build --target web wasm/prover",
|
||||
"build": "NODE_ENV=production node utils/build.js",
|
||||
"build:webpack": "NODE_ENV=production webpack --config webpack.config.js",
|
||||
"websockify": "docker run -it --rm -p 55688:80 -v $(pwd):/app novnc/websockify 80 --target-config /app/websockify_config",
|
||||
"dev": "NODE_ENV=development node utils/webserver.js",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extism/extism": "^1.0.3",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"async-mutex": "^0.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"charwise": "^3.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"comlink": "^4.4.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"http-parser-js": "^0.5.9",
|
||||
"level": "^8.0.0",
|
||||
"minimatch": "^9.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -33,7 +39,8 @@
|
||||
"redux": "^4.2.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3"
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
@@ -47,6 +54,7 @@
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/webextension-polyfill": "^0.10.7",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
@@ -77,12 +85,13 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ttlcache": "link:@types/@isaacs/ttlcache",
|
||||
"type-fest": "^3.5.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-ext-reloader": "^1.1.12",
|
||||
"zip-webpack-plugin": "^4.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
7821
pnpm-lock.yaml
generated
7821
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
[toolchain]
|
||||
# channel = "nightly-2022-12-12"
|
||||
# channel = "stable"
|
||||
# channel = "nightly-x86_64-apple-darwin"
|
||||
channel = "nightly"
|
||||
BIN
src/assets/img/default-plugin-icon.png
Normal file
BIN
src/assets/img/default-plugin-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
2
src/assets/img/dot-menu.svg
Normal file
2
src/assets/img/dot-menu.svg
Normal 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
BIN
src/assets/img/notarize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/plugins/discord_dm.wasm
Normal file
BIN
src/assets/plugins/discord_dm.wasm
Normal file
Binary file not shown.
BIN
src/assets/plugins/twitter_profile.wasm
Normal file
BIN
src/assets/plugins/twitter_profile.wasm
Normal file
Binary file not shown.
70
src/components/ConnectionDetailsModal/index.tsx
Normal file
70
src/components/ConnectionDetailsModal/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
useActiveTabUrl,
|
||||
setConnection,
|
||||
useIsConnected,
|
||||
} from '../../reducers/requests';
|
||||
import Modal, { ModalHeader, ModalContent } from '../../components/Modal/Modal';
|
||||
import { deleteConnection, getConnection } from '../../entries/Background/db';
|
||||
|
||||
const ConnectionDetailsModal = (props: {
|
||||
showConnectionDetails: boolean;
|
||||
setShowConnectionDetails: (show: boolean) => void;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const activeTabOrigin = useActiveTabUrl();
|
||||
const connected = useIsConnected();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (activeTabOrigin) {
|
||||
const isConnected: boolean | null = await getConnection(
|
||||
activeTabOrigin.origin,
|
||||
);
|
||||
dispatch(setConnection(!!isConnected));
|
||||
}
|
||||
})();
|
||||
}, [activeTabOrigin, dispatch]);
|
||||
|
||||
const handleDisconnect = useCallback(async () => {
|
||||
if (activeTabOrigin?.origin) {
|
||||
await deleteConnection(activeTabOrigin.origin);
|
||||
props.setShowConnectionDetails(false);
|
||||
dispatch(setConnection(false));
|
||||
}
|
||||
}, [activeTabOrigin?.origin, dispatch, props]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => props.setShowConnectionDetails(false)}
|
||||
className="flex flex-col gap-2 items-center text-base cursor-default justify-center mx-4 min-h-24"
|
||||
>
|
||||
<ModalHeader
|
||||
className="w-full rounded-t-lg pb-0 border-b-0"
|
||||
onClose={() => props.setShowConnectionDetails(false)}
|
||||
>
|
||||
<span className="text-lg font-semibold">
|
||||
{activeTabOrigin?.hostname || 'Connections'}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<ModalContent className="w-full gap-2 flex-grow flex flex-col items-center justify-between px-4 pt-0 pb-4">
|
||||
<div className="flex flex-row gap-2 items-start w-full text-xs font-semibold text-slate-800">
|
||||
{connected
|
||||
? 'TLSN Extension is connected to this site.'
|
||||
: 'TLSN Extension is not connected to this site. To connect to this site, find and click the connect button.'}
|
||||
</div>
|
||||
{connected && (
|
||||
<button
|
||||
className="button disabled:opacity-50 self-end"
|
||||
onClick={handleDisconnect}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionDetailsModal;
|
||||
26
src/components/ErrorModal/index.tsx
Normal file
26
src/components/ErrorModal/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import Modal, { ModalContent } from '../Modal/Modal';
|
||||
|
||||
export function ErrorModal(props: {
|
||||
onClose: () => void;
|
||||
message: string;
|
||||
}): ReactElement {
|
||||
const { onClose, message } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500 !bg-red-100"
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-red-500">
|
||||
{message || 'Something went wrong :('}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-200 text-red-400 hover:bg-red-200 hover:text-red-500"
|
||||
onClick={onClose}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
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 Icon from '../../components/Icon';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
|
||||
import { urlify, download } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../../pages/Background/actionTypes';
|
||||
|
||||
export default function History(): ReactElement {
|
||||
const history = useHistoryOrder();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap overflow-y-auto">
|
||||
{history.map((id) => {
|
||||
return <OneRequestHistory key={id} requestId={id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const request = useRequestHistory(props.requestId);
|
||||
const navigate = useNavigate();
|
||||
const { status } = request || {};
|
||||
const requestUrl = urlify(request?.url || '');
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
const notaryUrl = await get(NOTARY_API_LS_KEY);
|
||||
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
|
||||
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,
|
||||
data: request,
|
||||
});
|
||||
navigate('/verify/' + request?.id);
|
||||
}, [request]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
dispatch(deleteRequestHistory(props.requestId));
|
||||
}, [props.requestId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<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">
|
||||
{requestUrl?.pathname}
|
||||
</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>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold"
|
||||
onClick={onView}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-receipt" size={1} />
|
||||
<span className="text-xs font-bold">View Proof</span>
|
||||
</div>
|
||||
<div
|
||||
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={() => download(`${request?.id}.json`, JSON.stringify(request?.proof))}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-download" size={1} />
|
||||
<span className="text-xs font-bold">Download</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(!status || status === 'error') && (
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
{status === 'pending' && (
|
||||
<div 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>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
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}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-trash" size={1} />
|
||||
<span className="text-xs font-bold">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
src/components/Menu/index.tsx
Normal file
115
src/components/Menu/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../Icon';
|
||||
import browser from 'webextension-polyfill';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import PluginUploadInfo from '../PluginInfo';
|
||||
|
||||
export function MenuIcon(): ReactElement {
|
||||
const [opened, setOpen] = useState(false);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setOpen(!opened);
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{opened && (
|
||||
<>
|
||||
<div
|
||||
className="fixed top-0 left-0 w-screen h-screen z-10"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
<Menu opened={opened} setOpen={setOpen} />
|
||||
</>
|
||||
)}
|
||||
<Icon
|
||||
fa="fa-solid fa-bars"
|
||||
className="text-slate-500 hover:text-slate-700 active:text-slate-900 cursor-pointer z-20"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Menu(props: {
|
||||
opened: boolean;
|
||||
setOpen: (opened: boolean) => void;
|
||||
}): ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const openExtensionInPage = () => {
|
||||
props.setOpen(false);
|
||||
browser.tabs.create({
|
||||
url: `chrome-extension://${chrome.runtime.id}/popup.html`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-[100%] right-0 rounded-md z-20">
|
||||
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
|
||||
<MenuRow
|
||||
fa="fa-solid fa-plus"
|
||||
className="relative"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
}}
|
||||
>
|
||||
<PluginUploadInfo onPluginInstalled={() => props.setOpen(false)} />
|
||||
<span>Install Plugin</span>
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
fa="fa-solid fa-toolbox"
|
||||
className="border-b border-slate-300"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
navigate('/plugins');
|
||||
}}
|
||||
>
|
||||
Plugins
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
className="lg:hidden"
|
||||
fa="fa-solid fa-up-right-and-down-left-from-center"
|
||||
onClick={openExtensionInPage}
|
||||
>
|
||||
Expand
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
fa="fa-solid fa-gear"
|
||||
onClick={() => {
|
||||
props.setOpen(false);
|
||||
navigate('/options');
|
||||
}}
|
||||
>
|
||||
Options
|
||||
</MenuRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuRow(props: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
onClick?: MouseEventHandler;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center py-3 px-4 gap-2 hover:bg-slate-300 cursor-pointer text-slate-800 hover:text-slate-900 font-semibold',
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon size={0.875} fa={props.fa} />
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,13 +37,19 @@ export default function Modal(props: Props): ReactElement {
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function ModalHeader(props: HeaderProps): ReactElement {
|
||||
return (
|
||||
<div className={classNames('border-b modal__header border-gray-100')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'border-b modal__header border-gray-100',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="modal__header__title">{props.children}</div>
|
||||
<div className="modal__header__content">
|
||||
{props.onClose && (
|
||||
@@ -84,7 +90,7 @@ export function ModalFooter(props: FooterProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'border-t modal__footer border-gray-100',
|
||||
'border-t modal__footer border-gray-100 w-full',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import RequestBuilder from '../../pages/RequestBuilder';
|
||||
|
||||
export default function Notarize(): ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const request = useRequestHistory(params.requestId);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
{request?.id}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
set,
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
} from '../../utils/storage';
|
||||
|
||||
export default function Options(): ReactElement {
|
||||
const [notary, setNotary] = useState('http://localhost:7047');
|
||||
const [proxy, setProxy] = useState('ws://127.0.0.1:55688');
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setNotary(await get(NOTARY_API_LS_KEY));
|
||||
setProxy(await get(PROXY_API_LS_KEY));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(async () => {
|
||||
await set(NOTARY_API_LS_KEY, notary);
|
||||
await set(PROXY_API_LS_KEY, proxy);
|
||||
setDirty(false);
|
||||
}, [notary, proxy]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<div className="flex flex-row flex-nowrap py-1 px-2 gap-2 font-bold text-base">
|
||||
Settings
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Notary API</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input border"
|
||||
placeholder="http://localhost:7047"
|
||||
onChange={(e) => {
|
||||
setNotary(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={notary}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Proxy API</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input border"
|
||||
placeholder="ws://127.0.0.1:55688"
|
||||
onChange={(e) => {
|
||||
setProxy(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={proxy}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
disabled={!dirty}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src/components/PluginInfo/index.scss
Normal file
20
src/components/PluginInfo/index.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.custom-modal {
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
flex-grow: 2;
|
||||
overflow-y: auto;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
232
src/components/PluginInfo/index.tsx
Normal file
232
src/components/PluginInfo/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Children,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { makePlugin, getPluginConfig } from '../../utils/misc';
|
||||
import { addPlugin } from '../../utils/rpc';
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '../../components/Modal/Modal';
|
||||
import type { PluginConfig } from '../../utils/misc';
|
||||
import './index.scss';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
import {
|
||||
HostFunctionsDescriptions,
|
||||
MultipleParts,
|
||||
PermissionDescription,
|
||||
} from '../../utils/plugins';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import classNames from 'classnames';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
|
||||
export default function PluginUploadInfo({
|
||||
onPluginInstalled,
|
||||
}: {
|
||||
onPluginInstalled?: () => void;
|
||||
}): 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);
|
||||
onPluginInstalled?.();
|
||||
} 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}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
{pluginContent && (
|
||||
<PluginInfoModal
|
||||
pluginContent={pluginContent}
|
||||
onClose={onClose}
|
||||
onAddPlugin={onAddPlugin}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginInfoModalHeader(props: {
|
||||
className?: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
return <div className={props.className}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function PluginInfoModalContent(props: {
|
||||
className?: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
return <div className={props.className}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function PluginInfoModal(props: {
|
||||
pluginContent: PluginConfig;
|
||||
onClose: () => void;
|
||||
onAddPlugin?: MouseEventHandler;
|
||||
children?: ReactNode | ReactNode[];
|
||||
}) {
|
||||
const { pluginContent, onClose, onAddPlugin, children } = props;
|
||||
|
||||
const header = Children.toArray(children).filter(
|
||||
(c: any) => c.type.name === 'PluginInfoModalHeader',
|
||||
)[0];
|
||||
|
||||
const content = Children.toArray(children).filter(
|
||||
(c: any) => c.type.name === 'PluginInfoModalContent',
|
||||
)[0];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
className="custom-modal !rounded-none flex items-center justify-center gap-4 cursor-default"
|
||||
>
|
||||
<ModalHeader className="w-full p-2 border-gray-200 text-gray-500">
|
||||
{header || (
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<img className="h-5" src={logo || DefaultPluginIcon} alt="logo" />
|
||||
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalContent className="flex flex-col flex-grow-0 flex-shrink-0 items-center px-8 py-2 gap-2 w-full max-h-none">
|
||||
{content || (
|
||||
<>
|
||||
<img
|
||||
className="w-12 h-12"
|
||||
src={pluginContent.icon || DefaultPluginIcon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-center">
|
||||
<span>
|
||||
<span className="text-blue-600 font-semibold">
|
||||
{pluginContent.title}
|
||||
</span>{' '}
|
||||
wants access to your browser
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
<div className="flex-grow flex-shrink overflow-y-auto w-full px-8">
|
||||
<PluginPermissions pluginContent={pluginContent} />
|
||||
</div>
|
||||
<ModalFooter className="flex justify-end gap-2 p-4">
|
||||
<button className="button" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
{onAddPlugin && (
|
||||
<button className="button button--primary" onClick={onAddPlugin}>
|
||||
Allow
|
||||
</button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginPermissions({
|
||||
pluginContent,
|
||||
className,
|
||||
}: {
|
||||
pluginContent: PluginConfig;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={classNames('flex flex-col p-2 gap-5', className)}>
|
||||
{pluginContent.hostFunctions?.map((hostFunction: string) => {
|
||||
const HFComponent = HostFunctionsDescriptions[hostFunction];
|
||||
return <HFComponent key={hostFunction} {...pluginContent} />;
|
||||
})}
|
||||
{pluginContent.cookies && (
|
||||
<PermissionDescription fa="fa-solid fa-cookie-bite">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access cookies from</span>
|
||||
<MultipleParts parts={pluginContent.cookies} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.headers && (
|
||||
<PermissionDescription fa="fa-solid fa-envelope">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access headers from</span>
|
||||
<MultipleParts parts={pluginContent.headers} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.localStorage && (
|
||||
<PermissionDescription fa="fa-solid fa-database">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access local storage storage from</span>
|
||||
<MultipleParts parts={pluginContent.localStorage} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.sessionStorage && (
|
||||
<PermissionDescription fa="fa-solid fa-database">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access session storage from</span>
|
||||
<MultipleParts parts={pluginContent.sessionStorage} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.requests && (
|
||||
<PermissionDescription fa="fa-solid fa-globe">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Submit network requests to</span>
|
||||
<MultipleParts
|
||||
parts={pluginContent?.requests.map(({ url }) => url)}
|
||||
/>
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/PluginList/index.scss
Normal file
45
src/components/PluginList/index.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.plugin-box {
|
||||
&__remove-icon {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transition: 200ms opacity;
|
||||
transition-delay: 200ms;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.plugin-box__remove-icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
padding: .5rem;
|
||||
opacity: .5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-modal {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
margin: 1rem auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
flex-grow: 2;
|
||||
overflow-y: auto;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
249
src/components/PluginList/index.tsx
Normal file
249
src/components/PluginList/index.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
|
||||
import { usePluginHashes } from '../../reducers/plugins';
|
||||
import {
|
||||
getPluginConfig,
|
||||
hexToArrayBuffer,
|
||||
PluginConfig,
|
||||
} from '../../utils/misc';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
import './index.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import {
|
||||
PluginInfoModal,
|
||||
PluginInfoModalContent,
|
||||
PluginInfoModalHeader,
|
||||
} from '../PluginInfo';
|
||||
import { getPluginConfigByHash } from '../../entries/Background/db';
|
||||
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
|
||||
import { openSidePanel } from '../../entries/utils';
|
||||
|
||||
export function PluginList({
|
||||
className,
|
||||
unremovable,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
unremovable?: boolean;
|
||||
onClick?: (hash: string) => void;
|
||||
}): ReactElement {
|
||||
const hashes = usePluginHashes();
|
||||
|
||||
useEffect(() => {
|
||||
fetchPluginHashes();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col flex-nowrap gap-1', className)}>
|
||||
{!hashes.length && (
|
||||
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
|
||||
<div>No available plugins</div>
|
||||
</div>
|
||||
)}
|
||||
{hashes.map((hash) => (
|
||||
<Plugin
|
||||
key={hash}
|
||||
hash={hash}
|
||||
unremovable={unremovable}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Plugin({
|
||||
hash,
|
||||
hex,
|
||||
unremovable,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
hash: string;
|
||||
hex?: string;
|
||||
className?: string;
|
||||
onClick?: (hash: string) => void;
|
||||
unremovable?: boolean;
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
const [pluginInfo, showPluginInfo] = useState(false);
|
||||
const [remove, showRemove] = useState(false);
|
||||
|
||||
const onRunPlugin = useCallback(async () => {
|
||||
if (!config || remove) return;
|
||||
|
||||
if (onClick) {
|
||||
onClick(hash);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await openSidePanel();
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: SidePanelActionTypes.execute_plugin_request,
|
||||
data: {
|
||||
pluginHash: hash,
|
||||
},
|
||||
});
|
||||
|
||||
await runPlugin(hash, 'start');
|
||||
|
||||
window.close();
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [hash, config, remove, onClick]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (hex) {
|
||||
setConfig(await getPluginConfig(hexToArrayBuffer(hex)));
|
||||
} else {
|
||||
setConfig(await getPluginConfigByHash(hash));
|
||||
}
|
||||
})();
|
||||
}, [hash, hex]);
|
||||
|
||||
const onRemove: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
removePlugin(hash);
|
||||
showRemove(false);
|
||||
},
|
||||
[hash, remove],
|
||||
);
|
||||
|
||||
const onConfirmRemove: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showRemove(true);
|
||||
},
|
||||
[hash, remove],
|
||||
);
|
||||
|
||||
const onPluginInfo: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showPluginInfo(true);
|
||||
},
|
||||
[hash, pluginInfo],
|
||||
);
|
||||
|
||||
if (!config) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row justify-center border rounded border-slate-300 p-2 gap-2 plugin-box',
|
||||
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
|
||||
className,
|
||||
)}
|
||||
onClick={onRunPlugin}
|
||||
>
|
||||
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
{!remove ? (
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
|
||||
<div className="flex flex-col w-full items-start">
|
||||
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
|
||||
{config.title}
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<Icon
|
||||
fa="fa-solid fa-circle-info"
|
||||
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
|
||||
onClick={onPluginInfo}
|
||||
/>
|
||||
{!unremovable && (
|
||||
<Icon
|
||||
fa="fa-solid fa-xmark"
|
||||
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
|
||||
onClick={onConfirmRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{config.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RemovePlugin
|
||||
onRemove={onRemove}
|
||||
showRemove={showRemove}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
{pluginInfo && (
|
||||
<PluginInfoModal
|
||||
pluginContent={config}
|
||||
onClose={() => showPluginInfo(false)}
|
||||
>
|
||||
<PluginInfoModalHeader>
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<Icon
|
||||
className="text-slate-500 hover:text-slate-700 cursor-pointer"
|
||||
size={1}
|
||||
fa="fa-solid fa-caret-left"
|
||||
onClick={() => showPluginInfo(false)}
|
||||
/>
|
||||
</div>
|
||||
</PluginInfoModalHeader>
|
||||
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
|
||||
<img
|
||||
className="w-12 h-12 mb-2"
|
||||
src={config.icon || DefaultPluginIcon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-blue-600 font-semibold">
|
||||
{config.title}
|
||||
</span>
|
||||
<div className="text-slate-500 text-lg">{config.description}</div>
|
||||
</PluginInfoModalContent>
|
||||
</PluginInfoModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemovePlugin(props: {
|
||||
onRemove: MouseEventHandler;
|
||||
showRemove: (show: boolean) => void;
|
||||
config: PluginConfig;
|
||||
}): ReactElement {
|
||||
const { onRemove, showRemove, config } = props;
|
||||
|
||||
const onCancel: MouseEventHandler = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
showRemove(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full gap-1">
|
||||
<div className="font-bold text-red-700">
|
||||
{`Are you sure you want to remove "${config.title}" plugin?`}
|
||||
</div>
|
||||
<div className="mb-1">Warning: this cannot be undone.</div>
|
||||
<div className="flex flex-row w-full gap-1">
|
||||
<button className="flex-grow button p-1" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="flex-grow font-bold bg-red-500 hover:bg-red-600 text-white rounded p-1"
|
||||
onClick={onRemove}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { ReactNode, ReactElement, useState } from 'react';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import c from 'classnames';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import RequestBuilder from '../../pages/RequestBuilder';
|
||||
import Icon from '../../components/Icon';
|
||||
import { download } from '../../utils/misc';
|
||||
|
||||
export default function ProofViewer(): ReactElement {
|
||||
const {requestId} = useParams<{ requestId: string }>();
|
||||
const request = useRequestHistory(requestId);
|
||||
const navigate = useNavigate();
|
||||
const [ tab, setTab ] = useState('sent');
|
||||
const loc = useLocation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
|
||||
<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"
|
||||
/>
|
||||
<TabLabel
|
||||
onClick={() => setTab('sent')}
|
||||
active={tab === 'sent'}
|
||||
>
|
||||
Sent
|
||||
</TabLabel>
|
||||
<TabLabel
|
||||
onClick={() => setTab('recv')}
|
||||
active={tab === 'recv'}
|
||||
>
|
||||
Recv
|
||||
</TabLabel>
|
||||
<div className="flex flex-row flex-grow items-center justify-end">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => download(request.id, JSON.stringify(request.proof))}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow px-2">
|
||||
{tab === 'sent' && (
|
||||
<textarea
|
||||
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
|
||||
value={request.verification?.sent}
|
||||
></textarea>
|
||||
)}
|
||||
{tab === 'recv' && (
|
||||
<textarea
|
||||
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
|
||||
value={request.verification?.recv}
|
||||
></textarea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function TabLabel(props: {
|
||||
children: ReactNode;
|
||||
onClick: MouseEventHandler;
|
||||
active?: boolean;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className={c('px-1 select-none cursor-pointer font-bold', {
|
||||
'text-slate-800 border-b-2 border-green-500': props.active,
|
||||
'text-slate-400 border-b-2 border-transparent hover:text-slate-500': !props.active,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
154
src/components/RequestBuilder/index.tsx
Normal file
154
src/components/RequestBuilder/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import c from 'classnames';
|
||||
|
||||
export function InputBody(props: {
|
||||
body: string;
|
||||
setBody: (body: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<textarea
|
||||
className="textarea h-[90%] w-full resize-none"
|
||||
value={props.body}
|
||||
onChange={(e) => props.setBody(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormBodyTable(props: {
|
||||
formBody: [string, string, boolean?][];
|
||||
setFormBody: (formBody: [string, string, boolean?][]) => void;
|
||||
}) {
|
||||
const toggleKV = useCallback(
|
||||
(index: number) => {
|
||||
const newFormBody = [...props.formBody];
|
||||
newFormBody[index][2] = !newFormBody[index][2];
|
||||
props.setFormBody(newFormBody);
|
||||
},
|
||||
[props.formBody],
|
||||
);
|
||||
|
||||
const setKV = useCallback(
|
||||
(index: number, key: string, value: string) => {
|
||||
const newFormBody = [...props.formBody];
|
||||
newFormBody[index] = [key, value];
|
||||
props.setFormBody(newFormBody);
|
||||
|
||||
if (index === props.formBody.length - 1 && (key || value)) {
|
||||
props.setFormBody([...newFormBody, ['', '', true]]);
|
||||
}
|
||||
},
|
||||
[props.formBody],
|
||||
);
|
||||
|
||||
const last = props.formBody.length - 1;
|
||||
|
||||
return (
|
||||
<table className="border border-slate-300 border-collapse table-fixed w-full">
|
||||
<tbody>
|
||||
{props.formBody.map(([key, value, silent], i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={c('border-b border-slate-200', {
|
||||
'opacity-30': !!silent,
|
||||
})}
|
||||
>
|
||||
<td className="w-8 text-center pt-2">
|
||||
{last !== i && (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={() => toggleKV(i)}
|
||||
checked={!silent}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="border border-slate-300 font-bold align-top break-all w-fit">
|
||||
<input
|
||||
className="input py-1 px-2 w-full"
|
||||
type="text"
|
||||
value={key}
|
||||
placeholder="Key"
|
||||
onChange={(e) => {
|
||||
setKV(i, e.target.value, value);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-slate-300 break-all align-top">
|
||||
<input
|
||||
className="input py-1 px-2 w-full"
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder="Value"
|
||||
onChange={(e) => {
|
||||
setKV(i, key, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatForRequest(
|
||||
input: string | [string, string, boolean?][],
|
||||
type: string,
|
||||
): string {
|
||||
try {
|
||||
let pairs: [string, string][] = [];
|
||||
|
||||
if (typeof input === 'string') {
|
||||
const lines = input.split('\n').filter((line) => line.trim() !== '');
|
||||
pairs = lines.map((line) => {
|
||||
const [key, value] = line.split('=').map((part) => part.trim());
|
||||
return [key, value];
|
||||
});
|
||||
} else {
|
||||
pairs = input
|
||||
.filter(([, , silent]) => silent !== true)
|
||||
.map(([key, value]) => [key, value]);
|
||||
}
|
||||
if (type === 'text/plain') {
|
||||
return JSON.stringify(input as string);
|
||||
}
|
||||
if (type === 'application/json') {
|
||||
const jsonObject = JSON.parse(input as string);
|
||||
return JSON.stringify(jsonObject);
|
||||
}
|
||||
|
||||
if (type === 'application/x-www-form-urlencoded') {
|
||||
const searchParams = new URLSearchParams();
|
||||
pairs.forEach(([key, value]) => {
|
||||
searchParams.append(key, value);
|
||||
});
|
||||
return searchParams.toString();
|
||||
}
|
||||
|
||||
return pairs.map(([key, value]) => `${key}=${value}`).join('&');
|
||||
} catch (e) {
|
||||
console.error('Error formatting for request:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseResponse(contentType: string, res: Response) {
|
||||
const parsedResponseData = {
|
||||
json: '',
|
||||
text: '',
|
||||
img: '',
|
||||
headers: Array.from(res.headers.entries()),
|
||||
};
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
parsedResponseData.json = await res.json();
|
||||
} else if (contentType?.includes('text')) {
|
||||
parsedResponseData.text = await res.text();
|
||||
} else if (contentType?.includes('image')) {
|
||||
const blob = await res.blob();
|
||||
parsedResponseData.img = URL.createObjectURL(blob);
|
||||
} else {
|
||||
parsedResponseData.text = await res.text();
|
||||
}
|
||||
|
||||
return parsedResponseData;
|
||||
}
|
||||
@@ -5,18 +5,10 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
RequestLog,
|
||||
} from '../../pages/Background/actionTypes';
|
||||
import {
|
||||
notarizeRequest,
|
||||
useRequest,
|
||||
} from '../../reducers/requests';
|
||||
import { notarizeRequest, useRequest } from '../../reducers/requests';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
@@ -25,61 +17,35 @@ import {
|
||||
} from 'react-router';
|
||||
import Icon from '../Icon';
|
||||
import NavigateWithParams from '../NavigateWithParams';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import {
|
||||
set,
|
||||
get,
|
||||
MAX_SENT_LS_KEY,
|
||||
MAX_RECEIVED_LS_KEY,
|
||||
getMaxRecv,
|
||||
getMaxSent,
|
||||
} from '../../utils/storage';
|
||||
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
const maxTranscriptSize = 16384;
|
||||
|
||||
const authToken = 'a28cae3969369c26c1410f5bded83c3f4f914fbc';
|
||||
const accessToken =
|
||||
'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
||||
const csrfToken =
|
||||
'b73b3488687683372af2ea77486a444ccaa5327bbabad709df1b5161a6b83c8d7ec19106a82cb8dd5f8569632ee95ab4c6dc2abf5ad2ed7fa11b8340fcbe86a8fc00df28db6c4109a807f7cb12dd19da';
|
||||
const userAgent =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
|
||||
|
||||
|
||||
export default function RequestDetail(props: Props): ReactElement {
|
||||
const request = useRequest(props.requestId);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const notarize = useCallback(async () => {
|
||||
const notaryUrl = await get(NOTARY_API_LS_KEY);
|
||||
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
|
||||
const headers = request
|
||||
.requestHeaders.reduce((acc, h) => {
|
||||
if (!(/^(origin|referer|Accept-Language|Accept-EncodingAccept)$|^(sec-|x-twitter-)/i.test(h.name))) {
|
||||
acc[h.name] = h.value;
|
||||
}
|
||||
return acc;
|
||||
}, { Host: urlify(request.url)?.hostname });
|
||||
if (!request) return;
|
||||
|
||||
//TODO: for some reason, these needs to be override for twitter to work
|
||||
headers['Accept-Encoding'] = 'identity';
|
||||
headers['Connection'] = 'close';
|
||||
|
||||
dispatch(notarizeRequest({
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers,
|
||||
body: request.body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
}))
|
||||
navigate(`/history`);
|
||||
}, [request]);
|
||||
navigate('/notary/' + request.requestId);
|
||||
}, [request, props.requestId]);
|
||||
|
||||
if (!request) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-1 px-2 gap-2">
|
||||
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
|
||||
<Icon
|
||||
className="cursor-point text-slate-400 hover:text-slate-700"
|
||||
fa="fa-solid fa-xmark"
|
||||
@@ -94,6 +60,9 @@ export default function RequestDetail(props: Props): ReactElement {
|
||||
<RequestDetailsHeaderTab path="/response">
|
||||
Response
|
||||
</RequestDetailsHeaderTab>
|
||||
<RequestDetailsHeaderTab path="/advanced">
|
||||
Advanced
|
||||
</RequestDetailsHeaderTab>
|
||||
<button
|
||||
className="absolute right-2 bg-primary/[0.9] text-white font-bold px-2 py-0.5 hover:bg-primary/[0.8] active:bg-primary"
|
||||
onClick={notarize}
|
||||
@@ -102,9 +71,19 @@ export default function RequestDetail(props: Props): ReactElement {
|
||||
</button>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="headers" element={<RequestHeaders requestId={props.requestId} />} />
|
||||
<Route path="payloads" element={<RequestPayload requestId={props.requestId} />} />
|
||||
<Route path="response" element={<WebResponse requestId={props.requestId} />} />
|
||||
<Route
|
||||
path="headers"
|
||||
element={<RequestHeaders requestId={props.requestId} />}
|
||||
/>
|
||||
<Route
|
||||
path="payloads"
|
||||
element={<RequestPayload requestId={props.requestId} />}
|
||||
/>
|
||||
<Route
|
||||
path="response"
|
||||
element={<WebResponse requestId={props.requestId} />}
|
||||
/>
|
||||
<Route path="advanced" element={<AdvancedOptions />} />
|
||||
<Route path="/" element={<NavigateWithParams to="/headers" />} />
|
||||
</Routes>
|
||||
</>
|
||||
@@ -132,6 +111,62 @@ function RequestDetailsHeaderTab(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedOptions(): ReactElement {
|
||||
const [maxSent, setMaxSent] = useState(MAX_SENT);
|
||||
const [maxRecv, setMaxRecv] = useState(MAX_RECV);
|
||||
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setMaxRecv((await getMaxRecv()) || MAX_RECV);
|
||||
setMaxSent((await getMaxSent()) || MAX_SENT);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(async () => {
|
||||
await set(MAX_RECEIVED_LS_KEY, maxRecv.toString());
|
||||
await set(MAX_SENT_LS_KEY, maxSent.toString());
|
||||
setDirty(false);
|
||||
}, [maxSent, maxRecv]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Max Sent Data</div>
|
||||
<input
|
||||
type="number"
|
||||
className="input border"
|
||||
value={maxSent}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxSent(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="font-semibold">Max Received Data</div>
|
||||
<input
|
||||
type="number"
|
||||
className="input border"
|
||||
value={maxRecv}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxRecv(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
disabled={!dirty}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestPayload(props: Props): ReactElement {
|
||||
const data = useRequest(props.requestId);
|
||||
const [url, setUrl] = useState<URL | null>();
|
||||
@@ -280,6 +315,7 @@ function WebResponse(props: Props): ReactElement {
|
||||
const options = {
|
||||
method: data.method,
|
||||
headers: data.requestHeaders.reduce(
|
||||
// @ts-ignore
|
||||
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
|
||||
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
|
||||
acc[h.name] = h.value;
|
||||
@@ -295,6 +331,7 @@ function WebResponse(props: Props): ReactElement {
|
||||
options.body = formData.toString();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const resp = await fetch(data.url, options);
|
||||
setResponse(resp);
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
RequestLog,
|
||||
} from '../../pages/Background/actionTypes';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { BackgroundActiontype, RequestLog } from '../../entries/Background/rpc';
|
||||
import { useNavigate } from 'react-router';
|
||||
import Fuse from 'fuse.js';
|
||||
import Icon from '../Icon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setRequests } from '../../reducers/requests';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
requests: RequestLog[];
|
||||
shouldFix?: boolean;
|
||||
};
|
||||
|
||||
export default function RequestTable(props: Props): ReactElement {
|
||||
@@ -50,7 +55,14 @@ export default function RequestTable(props: Props): ReactElement {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<div className="flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2',
|
||||
{
|
||||
'fixed top-[4.5rem] w-full shadow': props.shouldFix,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className="input w-full"
|
||||
type="text"
|
||||
@@ -64,7 +76,7 @@ export default function RequestTable(props: Props): ReactElement {
|
||||
onClick={reset}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto h-0">
|
||||
<div className="flex-grow">
|
||||
<table className="border border-slate-300 border-collapse table-fixed w-full">
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
|
||||
@@ -1,63 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export default function ResponseDetail(props: {
|
||||
response: Response | null;
|
||||
responseData: {
|
||||
json: any | null;
|
||||
text: string | null;
|
||||
img: string | null;
|
||||
headers: [string, string][] | null;
|
||||
} | null;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
const [json, setJSON] = useState<any | null>(null);
|
||||
const [text, setText] = useState<string | null>(null);
|
||||
const [img, setImg] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<URLSearchParams | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const resp = props.response;
|
||||
|
||||
if (!resp) return;
|
||||
|
||||
const contentType =
|
||||
resp.headers.get('content-type') || resp.headers.get('Content-Type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
resp
|
||||
.json()
|
||||
.then((json) => {
|
||||
if (json) {
|
||||
setJSON(json);
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
} else if (contentType?.includes('text')) {
|
||||
resp
|
||||
.text()
|
||||
.then((_text) => {
|
||||
if (_text) {
|
||||
setText(_text);
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
} else if (contentType?.includes('image')) {
|
||||
resp
|
||||
.blob()
|
||||
.then((blob) => {
|
||||
if (blob) {
|
||||
setImg(URL.createObjectURL(blob));
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
} else {
|
||||
resp
|
||||
.blob()
|
||||
.then((blob) => blob.text())
|
||||
.then((_text) => {
|
||||
if (_text) {
|
||||
setText(_text);
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
}, [props.response]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -66,7 +18,7 @@ export default function ResponseDetail(props: {
|
||||
)}
|
||||
>
|
||||
<table className="border border-slate-300 border-collapse table-fixed w-full">
|
||||
{!!json && (
|
||||
{!!props.responseData?.json && (
|
||||
<>
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
@@ -80,13 +32,13 @@ export default function ResponseDetail(props: {
|
||||
<textarea
|
||||
rows={16}
|
||||
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
|
||||
value={JSON.stringify(json, null, 2)}
|
||||
value={JSON.stringify(props.responseData.json, null, 2)}
|
||||
></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
{!!text && (
|
||||
{!!props.responseData?.text && (
|
||||
<>
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
@@ -100,13 +52,13 @@ export default function ResponseDetail(props: {
|
||||
<textarea
|
||||
rows={16}
|
||||
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
|
||||
value={text}
|
||||
value={props.responseData.text}
|
||||
></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
{!!img && (
|
||||
{!!props.responseData?.img && (
|
||||
<>
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
@@ -117,12 +69,12 @@ export default function ResponseDetail(props: {
|
||||
</thead>
|
||||
<tr>
|
||||
<td className="bg-slate-100" colSpan={2}>
|
||||
<img src={img} />
|
||||
<img src={props.responseData.img} />
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
{!!props.response?.headers && (
|
||||
{!!props.responseData?.headers && (
|
||||
<>
|
||||
<thead className="bg-slate-200">
|
||||
<tr>
|
||||
@@ -132,20 +84,18 @@ export default function ResponseDetail(props: {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from(props.response.headers.entries()).map(
|
||||
([name, value]) => {
|
||||
return (
|
||||
<tr className="border-b border-slate-200">
|
||||
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
|
||||
{name}
|
||||
</td>
|
||||
<td className="border border-slate-300 break-all align-top py-1 px-2">
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{props.responseData?.headers.map(([name, value]) => {
|
||||
return (
|
||||
<tr className="border-b border-slate-200">
|
||||
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
|
||||
{name}
|
||||
</td>
|
||||
<td className="border border-slate-300 break-all align-top py-1 px-2">
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</>
|
||||
)}
|
||||
|
||||
28
src/entries/Background/cache.ts
Normal file
28
src/entries/Background/cache.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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();
|
||||
};
|
||||
514
src/entries/Background/db.ts
Normal file
514
src/entries/Background/db.ts
Normal file
@@ -0,0 +1,514 @@
|
||||
import { Level } from 'level';
|
||||
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
|
||||
import { RequestHistory, RequestProgress } from './rpc';
|
||||
import mutex from './mutex';
|
||||
import { minimatch } from 'minimatch';
|
||||
const charwise = require('charwise');
|
||||
|
||||
export const db = new Level('./ext-db', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const historyDb = db.sublevel<string, RequestHistory>('history', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const pluginDb = db.sublevel<string, string>('plugin', {
|
||||
valueEncoding: 'hex',
|
||||
});
|
||||
const pluginConfigDb = db.sublevel<string, PluginConfig>('pluginConfig', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const connectionDb = db.sublevel<string, boolean>('connections', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const cookiesDb = db.sublevel<string, boolean>('cookies', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const headersDb = db.sublevel<string, boolean>('headers', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const sessionStorageDb = db.sublevel<string, any>('localStorage', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const appDb = db.sublevel<string, any>('app', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
enum AppDatabaseKey {
|
||||
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
|
||||
}
|
||||
|
||||
export async function addNotaryRequest(
|
||||
now = Date.now(),
|
||||
request: Omit<RequestHistory, 'status' | 'id'>,
|
||||
): Promise<RequestHistory> {
|
||||
const id = charwise.encode(now).toString('hex');
|
||||
const newReq: RequestHistory = {
|
||||
...request,
|
||||
id,
|
||||
status: '',
|
||||
};
|
||||
await historyDb.put(id, newReq);
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function addNotaryRequestProofs(
|
||||
id: string,
|
||||
proof: { session: any; substrings: any },
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq: RequestHistory = {
|
||||
...existing,
|
||||
proof,
|
||||
status: 'success',
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestStatus(
|
||||
id: string,
|
||||
status: '' | 'pending' | 'success' | 'error',
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
status,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestError(
|
||||
id: string,
|
||||
error: any,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq: RequestHistory = {
|
||||
...existing,
|
||||
error,
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestProgress(
|
||||
id: string,
|
||||
progress: RequestProgress,
|
||||
errorMessage?: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq: RequestHistory = {
|
||||
...existing,
|
||||
progress,
|
||||
errorMessage,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestVerification(
|
||||
id: string,
|
||||
verification: {
|
||||
sent: string;
|
||||
recv: string;
|
||||
verifierKey: string;
|
||||
notaryKey?: string;
|
||||
},
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
verification,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function removeNotaryRequest(
|
||||
id: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await historyDb.del(id);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
export async function getNotaryRequests(): Promise<RequestHistory[]> {
|
||||
const retVal = [];
|
||||
for await (const [key, value] of historyDb.iterator()) {
|
||||
retVal.push(value);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export async function getNotaryRequest(
|
||||
id: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
return historyDb.get(id).catch(() => null);
|
||||
}
|
||||
|
||||
export async function getPluginHashes(): Promise<string[]> {
|
||||
const retVal: string[] = [];
|
||||
for await (const [key] of pluginDb.iterator()) {
|
||||
retVal.push(key);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export async function getPluginByHash(hash: string): Promise<string | null> {
|
||||
try {
|
||||
const plugin = await pluginDb.get(hash);
|
||||
return plugin;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPlugin(hex: string): Promise<string | null> {
|
||||
const hash = await sha256(hex);
|
||||
|
||||
if (await getPluginByHash(hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pluginDb.put(hash, hex);
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function removePlugin(hash: string): Promise<string | null> {
|
||||
const existing = await pluginDb.get(hash);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await pluginDb.del(hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function getPluginConfigByHash(
|
||||
hash: string,
|
||||
): Promise<PluginConfig | null> {
|
||||
try {
|
||||
const config = await pluginConfigDb.get(hash);
|
||||
return config;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPluginConfig(
|
||||
hash: string,
|
||||
config: PluginConfig,
|
||||
): Promise<PluginConfig | null> {
|
||||
if (await getPluginConfigByHash(hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pluginConfigDb.put(hash, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function removePluginConfig(
|
||||
hash: string,
|
||||
): Promise<PluginConfig | null> {
|
||||
const existing = await pluginConfigDb.get(hash);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await pluginConfigDb.del(hash);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
export async function getPlugins(): Promise<
|
||||
(PluginConfig & { hash: string; metadata: PluginMetadata })[]
|
||||
> {
|
||||
const hashes = await getPluginHashes();
|
||||
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
|
||||
for (const hash of hashes) {
|
||||
const config = await getPluginConfigByHash(hash);
|
||||
const metadata = await getPluginMetadataByHash(hash);
|
||||
if (config) {
|
||||
ret.push({
|
||||
...config,
|
||||
hash,
|
||||
metadata: metadata
|
||||
? {
|
||||
...metadata,
|
||||
hash,
|
||||
}
|
||||
: {
|
||||
filePath: '',
|
||||
origin: '',
|
||||
hash,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getPluginMetadataByHash(
|
||||
hash: string,
|
||||
): Promise<PluginMetadata | null> {
|
||||
try {
|
||||
const metadata = await pluginMetadataDb.get(hash);
|
||||
return metadata;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPluginMetadata(
|
||||
hash: string,
|
||||
metadata: PluginMetadata,
|
||||
): Promise<PluginMetadata | null> {
|
||||
await pluginMetadataDb.put(hash, metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function removePluginMetadata(
|
||||
hash: string,
|
||||
): Promise<PluginMetadata | null> {
|
||||
const existing = await pluginMetadataDb.get(hash);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await pluginMetadataDb.del(hash);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestCid(
|
||||
id: string,
|
||||
cid: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
cid,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
export async function setConnection(origin: string) {
|
||||
if (await getConnection(origin)) return null;
|
||||
await connectionDb.put(origin, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function 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(link: string, name: string) {
|
||||
try {
|
||||
const existing = await cookiesDb.sublevel(link).get(name);
|
||||
return existing;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCookiesByHost(link: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
const links: { [k: string]: boolean } = {};
|
||||
const url = urlify(link);
|
||||
|
||||
for await (const sublevel of cookiesDb.keys({ keyEncoding: 'utf8' })) {
|
||||
const l = sublevel.split('!')[1];
|
||||
links[l] = true;
|
||||
}
|
||||
|
||||
const cookieLink = url
|
||||
? Object.keys(links).filter((l) => minimatch(l, link))[0]
|
||||
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
|
||||
|
||||
if (!cookieLink) return ret;
|
||||
|
||||
for await (const [key, value] of cookiesDb.sublevel(cookieLink).iterator()) {
|
||||
ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function deleteConnection(origin: string) {
|
||||
return mutex.runExclusive(async () => {
|
||||
if (await getConnection(origin)) {
|
||||
await connectionDb.del(origin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function getConnection(origin: string) {
|
||||
try {
|
||||
const existing = await connectionDb.get(origin);
|
||||
return existing;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setHeaders(link: string, name: string, value?: string) {
|
||||
if (!value) return null;
|
||||
return mutex.runExclusive(async () => {
|
||||
await headersDb.sublevel(link).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(link: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
const url = urlify(link);
|
||||
|
||||
const links: { [k: string]: boolean } = {};
|
||||
for await (const sublevel of headersDb.keys({ keyEncoding: 'utf8' })) {
|
||||
const l = sublevel.split('!')[1];
|
||||
links[l] = true;
|
||||
}
|
||||
|
||||
const headerLink = url
|
||||
? Object.keys(links).filter((l) => minimatch(l, link))[0]
|
||||
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
|
||||
|
||||
if (!headerLink) return ret;
|
||||
|
||||
for await (const [key, value] of headersDb.sublevel(headerLink).iterator()) {
|
||||
ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function setLocalStorage(
|
||||
host: string,
|
||||
name: string,
|
||||
value: string,
|
||||
) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await localStorageDb.sublevel(host).put(name, value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function setSessionStorage(
|
||||
host: string,
|
||||
name: string,
|
||||
value: string,
|
||||
) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await sessionStorageDb.sublevel(host).put(name, value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearLocalStorage(host: string) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await localStorageDb.sublevel(host).clear();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSessionStorage(host: string) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await sessionStorageDb.sublevel(host).clear();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLocalStorageByHost(host: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
for await (const [key, value] of localStorageDb.sublevel(host).iterator()) {
|
||||
ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getSessionStorageByHost(host: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
for await (const [key, value] of sessionStorageDb.sublevel(host).iterator()) {
|
||||
ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function getDefaultPluginsInstalled(): Promise<string | boolean> {
|
||||
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
|
||||
}
|
||||
|
||||
export async function setDefaultPluginsInstalled(
|
||||
installed: string | boolean = false,
|
||||
) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAppState() {
|
||||
return {
|
||||
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
|
||||
};
|
||||
}
|
||||
120
src/entries/Background/handlers.ts
Normal file
120
src/entries/Background/handlers.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { getCacheByTabId } from './cache';
|
||||
import { BackgroundActiontype, RequestLog } from './rpc';
|
||||
import mutex from './mutex';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { addRequest } from '../../reducers/requests';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import { getHeadersByHost, setCookies, setHeaders } from './db';
|
||||
export const onSendHeaders = (
|
||||
details: browser.WebRequest.OnSendHeadersDetailsType,
|
||||
) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const { method, tabId, requestId } = details;
|
||||
|
||||
if (method !== 'OPTIONS') {
|
||||
const cache = getCacheByTabId(tabId);
|
||||
const existing = cache.get<RequestLog>(requestId);
|
||||
const { origin, pathname } = urlify(details.url) || {};
|
||||
|
||||
const link = [origin, pathname].join('');
|
||||
|
||||
if (link && details.requestHeaders) {
|
||||
details.requestHeaders.forEach((header) => {
|
||||
const { name, value } = header;
|
||||
if (/^cookie$/i.test(name) && value) {
|
||||
value.split(';').forEach((cookieStr) => {
|
||||
const index = cookieStr.indexOf('=');
|
||||
if (index !== -1) {
|
||||
const cookieName = cookieStr.slice(0, index).trim();
|
||||
const cookieValue = cookieStr.slice(index + 1);
|
||||
setCookies(link, cookieName, cookieValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setHeaders(link, name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const onBeforeRequest = (
|
||||
details: browser.WebRequest.OnBeforeRequestDetailsType,
|
||||
) => {
|
||||
mutex.runExclusive(async () => {
|
||||
const { method, requestBody, tabId, requestId } = details;
|
||||
|
||||
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,
|
||||
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
|
||||
'utf-8',
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (requestBody.formData) {
|
||||
cache.set(requestId, {
|
||||
...existing,
|
||||
formData: requestBody.formData,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const onResponseStarted = (
|
||||
details: browser.WebRequest.OnResponseStartedDetailsType,
|
||||
) => {
|
||||
mutex.runExclusive(async () => {
|
||||
const { method, responseHeaders, tabId, requestId } = details;
|
||||
|
||||
if (method === 'OPTIONS') return;
|
||||
|
||||
const cache = getCacheByTabId(tabId);
|
||||
|
||||
const existing = cache.get<RequestLog>(requestId);
|
||||
const newLog: RequestLog = {
|
||||
requestHeaders: [],
|
||||
...existing,
|
||||
method: details.method,
|
||||
type: details.type,
|
||||
url: details.url,
|
||||
initiator: details.initiator || null,
|
||||
tabId: tabId,
|
||||
requestId: requestId,
|
||||
responseHeaders,
|
||||
};
|
||||
|
||||
cache.set(requestId, newLog);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: details.tabId,
|
||||
request: newLog,
|
||||
},
|
||||
action: addRequest(newLog),
|
||||
});
|
||||
});
|
||||
};
|
||||
99
src/entries/Background/index.ts
Normal file
99
src/entries/Background/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
|
||||
import { deleteCacheByTabId } from './cache';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { getAppState, removePlugin, setDefaultPluginsInstalled } from './db';
|
||||
import { installPlugin } from './plugins/utils';
|
||||
|
||||
(async () => {
|
||||
browser.webRequest.onSendHeaders.addListener(
|
||||
onSendHeaders,
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['requestHeaders', 'extraHeaders'],
|
||||
);
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
onBeforeRequest,
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['requestBody'],
|
||||
);
|
||||
|
||||
browser.webRequest.onResponseStarted.addListener(
|
||||
onResponseStarted,
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['responseHeaders', 'extraHeaders'],
|
||||
);
|
||||
|
||||
browser.tabs.onRemoved.addListener((tabId) => {
|
||||
deleteCacheByTabId(tabId);
|
||||
});
|
||||
|
||||
const { defaultPluginsInstalled } = await getAppState();
|
||||
|
||||
switch (defaultPluginsInstalled) {
|
||||
case false: {
|
||||
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('0.1.0.703');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case true: {
|
||||
try {
|
||||
await removePlugin(
|
||||
'6931d2ad63340d3a1fb1a5c1e3f4454c5a518164d6de5ad272e744832355ee02',
|
||||
);
|
||||
const twitterProfileUrl = browser.runtime.getURL(
|
||||
'twitter_profile.wasm',
|
||||
);
|
||||
await installPlugin(twitterProfileUrl);
|
||||
} finally {
|
||||
await setDefaultPluginsInstalled('0.1.0.703');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '0.1.0.703':
|
||||
break;
|
||||
}
|
||||
|
||||
const { initRPC } = await import('./rpc');
|
||||
await createOffscreenDocument();
|
||||
initRPC();
|
||||
})();
|
||||
|
||||
let creatingOffscreen: any;
|
||||
async function createOffscreenDocument() {
|
||||
const offscreenUrl = browser.runtime.getURL('offscreen.html');
|
||||
// @ts-ignore
|
||||
const existingContexts = await browser.runtime.getContexts({
|
||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
||||
documentUrls: [offscreenUrl],
|
||||
});
|
||||
|
||||
if (existingContexts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (creatingOffscreen) {
|
||||
await creatingOffscreen;
|
||||
} else {
|
||||
creatingOffscreen = (chrome as any).offscreen.createDocument({
|
||||
url: 'offscreen.html',
|
||||
reasons: ['WORKERS'],
|
||||
justification: 'workers for multithreading',
|
||||
});
|
||||
await creatingOffscreen;
|
||||
creatingOffscreen = null;
|
||||
}
|
||||
}
|
||||
5
src/entries/Background/mutex.ts
Normal file
5
src/entries/Background/mutex.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
export default mutex;
|
||||
43
src/entries/Background/plugins/utils.ts
Normal file
43
src/entries/Background/plugins/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
|
||||
import { getPluginConfig } from '../../../utils/misc';
|
||||
|
||||
export async function installPlugin(
|
||||
urlOrBuffer: ArrayBuffer | string,
|
||||
origin = '',
|
||||
filePath = '',
|
||||
metadata: {[key: string]: string} = {},
|
||||
) {
|
||||
let arrayBuffer;
|
||||
|
||||
if (typeof urlOrBuffer === 'string') {
|
||||
const resp = await fetch(urlOrBuffer);
|
||||
arrayBuffer = await resp.arrayBuffer();
|
||||
} else {
|
||||
arrayBuffer = urlOrBuffer;
|
||||
}
|
||||
|
||||
const config = await getPluginConfig(arrayBuffer);
|
||||
const hex = Buffer.from(arrayBuffer).toString('hex');
|
||||
const hash = await addPlugin(hex);
|
||||
await addPluginConfig(hash!, config);
|
||||
await addPluginMetadata(hash!, {
|
||||
...metadata,
|
||||
origin,
|
||||
filePath,
|
||||
});
|
||||
return hash;
|
||||
}
|
||||
|
||||
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 }[]
|
||||
}
|
||||
1426
src/entries/Background/rpc.ts
Normal file
1426
src/entries/Background/rpc.ts
Normal file
File diff suppressed because it is too large
Load Diff
463
src/entries/Background/ws.ts
Normal file
463
src/entries/Background/ws.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
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 { getPluginByHash } 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 getPluginByHash(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 = () => {
|
||||
console.error('Error connecting to websocket');
|
||||
pushToRedux(setConnected(false));
|
||||
};
|
||||
};
|
||||
|
||||
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 getPluginByHash(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 getPluginByHash(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));
|
||||
}
|
||||
121
src/entries/Content/content.ts
Normal file
121
src/entries/Content/content.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ContentScriptTypes, RPCClient } from './rpc';
|
||||
import { RequestHistory } from '../Background/rpc';
|
||||
import { PluginConfig, PluginMetadata } from '../../utils/misc';
|
||||
import { PresentationJSON } from '../../utils/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?: {
|
||||
method?: string;
|
||||
headers?: { [key: string]: string };
|
||||
body?: string;
|
||||
},
|
||||
proofOptions?: {
|
||||
notaryUrl?: string;
|
||||
websocketProxyUrl?: string;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
metadata?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
},
|
||||
): Promise<PresentationJSON> {
|
||||
const resp = await client.call(ContentScriptTypes.notarize, {
|
||||
url,
|
||||
method: requestOptions?.method,
|
||||
headers: requestOptions?.headers,
|
||||
body: requestOptions?.body,
|
||||
maxSentData: proofOptions?.maxSentData,
|
||||
maxRecvData: proofOptions?.maxRecvData,
|
||||
notaryUrl: proofOptions?.notaryUrl,
|
||||
websocketProxyUrl: proofOptions?.websocketProxyUrl,
|
||||
metadata: proofOptions?.metadata,
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
async installPlugin(
|
||||
url: string,
|
||||
metadata?: { [k: string]: string },
|
||||
): Promise<string> {
|
||||
const resp = await client.call(ContentScriptTypes.install_plugin, {
|
||||
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, params?: Record<string, string>) {
|
||||
const resp = await client.call(ContentScriptTypes.run_plugin, {
|
||||
hash,
|
||||
params,
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
const resp = await client.call(ContentScriptTypes.connect);
|
||||
|
||||
if (resp) {
|
||||
return new TLSN();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
window.tlsn = {
|
||||
connect,
|
||||
};
|
||||
|
||||
window.dispatchEvent(new CustomEvent('tlsn_loaded'));
|
||||
239
src/entries/Content/index.ts
Normal file
239
src/entries/Content/index.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import browser, { browserAction } from 'webextension-polyfill';
|
||||
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
|
||||
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
|
||||
import { urlify } from '../../utils/misc';
|
||||
|
||||
(async () => {
|
||||
loadScript('content.bundle.js');
|
||||
const server = new RPCServer();
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === BackgroundActiontype.get_local_storage) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.set_local_storage,
|
||||
data: { ...localStorage },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === BackgroundActiontype.get_session_storage) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.set_session_storage,
|
||||
data: { ...sessionStorage },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.on(ContentScriptTypes.connect, async () => {
|
||||
const connected = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.connect_request,
|
||||
data: {
|
||||
...getPopupData(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!connected) throw new Error('user rejected.');
|
||||
|
||||
return connected;
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
async (
|
||||
request: ContentScriptRequest<{
|
||||
url: string;
|
||||
method?: string;
|
||||
headers?: { [key: string]: string };
|
||||
metadata?: { [key: string]: string };
|
||||
body?: string;
|
||||
notaryUrl?: string;
|
||||
websocketProxyUrl?: string;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
}>,
|
||||
) => {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
} = request.params || {};
|
||||
|
||||
if (!url || !urlify(url)) throw new Error('invalid url.');
|
||||
|
||||
const proof = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.notarize_request,
|
||||
data: {
|
||||
...getPopupData(),
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
},
|
||||
});
|
||||
|
||||
return proof;
|
||||
},
|
||||
);
|
||||
|
||||
server.on(
|
||||
ContentScriptTypes.install_plugin,
|
||||
async (
|
||||
request: ContentScriptRequest<{
|
||||
url: string;
|
||||
metadata?: { [k: string]: string };
|
||||
}>,
|
||||
) => {
|
||||
const { url, metadata } = request.params || {};
|
||||
|
||||
if (!url) throw new Error('params must include url.');
|
||||
|
||||
const response: RequestHistory[] = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.install_plugin_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;
|
||||
params?: Record<string, string>;
|
||||
}>,
|
||||
) => {
|
||||
const { hash, params } = 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,
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
||||
function loadScript(filename: string) {
|
||||
const url = browser.runtime.getURL(filename);
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
script.setAttribute('src', url);
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
function getPopupData() {
|
||||
return {
|
||||
origin: window.origin,
|
||||
position: {
|
||||
left: window.screen.width / 2 - 240,
|
||||
top: window.screen.height / 2 - 300,
|
||||
},
|
||||
};
|
||||
}
|
||||
118
src/entries/Content/rpc.ts
Normal file
118
src/entries/Content/rpc.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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',
|
||||
}
|
||||
|
||||
export type ContentScriptRequest<params> = {
|
||||
tlsnrpc: string;
|
||||
} & RPCRequest<ContentScriptTypes, params>;
|
||||
|
||||
export type ContentScriptResponse = {
|
||||
tlsnrpc: string;
|
||||
} & RPCResponse;
|
||||
|
||||
export type RPCRequest<method, params> = {
|
||||
id: number;
|
||||
method: method;
|
||||
params?: params;
|
||||
};
|
||||
|
||||
export type RPCResponse = {
|
||||
id: number;
|
||||
result?: never;
|
||||
error?: never;
|
||||
};
|
||||
|
||||
export class RPCServer {
|
||||
#handlers: Map<
|
||||
ContentScriptTypes,
|
||||
(message: ContentScriptRequest<any>) => Promise<any>
|
||||
> = new Map();
|
||||
|
||||
constructor() {
|
||||
window.addEventListener(
|
||||
'message',
|
||||
async (event: MessageEvent<ContentScriptRequest<never>>) => {
|
||||
const data = event.data;
|
||||
|
||||
if (data.tlsnrpc !== '1.0') return;
|
||||
if (!data.method) return;
|
||||
|
||||
const handler = this.#handlers.get(data.method);
|
||||
|
||||
if (handler) {
|
||||
try {
|
||||
const result = await handler(data);
|
||||
window.postMessage({
|
||||
tlsnrpc: '1.0',
|
||||
id: data.id,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
tlsnrpc: '1.0',
|
||||
id: data.id,
|
||||
error,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`unknown method - ${data.method}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
on(
|
||||
method: ContentScriptTypes,
|
||||
handler: (message: ContentScriptRequest<any>) => Promise<any>,
|
||||
) {
|
||||
this.#handlers.set(method, handler);
|
||||
}
|
||||
}
|
||||
|
||||
export class RPCClient {
|
||||
#requests: Map<number, PromiseResolvers> = new Map();
|
||||
#id = 0;
|
||||
|
||||
get id() {
|
||||
return this.#id++;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
window.addEventListener(
|
||||
'message',
|
||||
(event: MessageEvent<ContentScriptResponse>) => {
|
||||
const data = event.data;
|
||||
|
||||
if (data.tlsnrpc !== '1.0') return;
|
||||
|
||||
const promise = this.#requests.get(data.id);
|
||||
|
||||
if (promise) {
|
||||
if (typeof data.result !== 'undefined') {
|
||||
promise.resolve(data.result);
|
||||
this.#requests.delete(data.id);
|
||||
} else if (typeof data.error !== 'undefined') {
|
||||
promise.reject(data.error);
|
||||
this.#requests.delete(data.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async call(method: ContentScriptTypes, params?: any): Promise<never> {
|
||||
const request = { tlsnrpc: '1.0', id: this.id, method, params };
|
||||
const defer = deferredPromise();
|
||||
this.#requests.set(request.id, defer);
|
||||
window.postMessage(request, '*');
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
||||
66
src/entries/Offscreen/Offscreen.tsx
Normal file
66
src/entries/Offscreen/Offscreen.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { OffscreenActionTypes } from './types';
|
||||
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import {
|
||||
initThreads,
|
||||
onCreatePresentationRequest,
|
||||
onCreateProverRequest,
|
||||
onNotarizationRequest,
|
||||
onProcessProveRequest,
|
||||
onVerifyProof,
|
||||
onVerifyProofRequest,
|
||||
startP2PProver,
|
||||
startP2PVerifier,
|
||||
} from './rpc';
|
||||
|
||||
const Offscreen = () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await initThreads();
|
||||
// @ts-ignore
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
switch (request.type) {
|
||||
case OffscreenActionTypes.notarization_request: {
|
||||
onNotarizationRequest(request);
|
||||
break;
|
||||
}
|
||||
case OffscreenActionTypes.create_prover_request: {
|
||||
onCreateProverRequest(request);
|
||||
break;
|
||||
}
|
||||
case OffscreenActionTypes.create_presentation_request: {
|
||||
onCreatePresentationRequest(request);
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.process_prove_request: {
|
||||
onProcessProveRequest(request);
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.verify_proof: {
|
||||
onVerifyProof(request, sendResponse);
|
||||
return true;
|
||||
}
|
||||
case BackgroundActiontype.verify_prove_request: {
|
||||
onVerifyProofRequest(request);
|
||||
break;
|
||||
}
|
||||
case OffscreenActionTypes.start_p2p_verifier: {
|
||||
startP2PVerifier(request);
|
||||
break;
|
||||
}
|
||||
case OffscreenActionTypes.start_p2p_prover: {
|
||||
startP2PProver(request);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div className="App" />;
|
||||
};
|
||||
|
||||
export default Offscreen;
|
||||
623
src/entries/Offscreen/rpc.ts
Normal file
623
src/entries/Offscreen/rpc.ts
Normal file
@@ -0,0 +1,623 @@
|
||||
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 { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
|
||||
import { OffscreenActionTypes } from './types';
|
||||
import { PresentationJSON } from '../../utils/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,
|
||||
})) as TPresentation;
|
||||
const json = await presentation.json();
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
proof: {
|
||||
...json,
|
||||
meta: {
|
||||
...json.meta,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 {
|
||||
pluginHash,
|
||||
pluginHex,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
proverUrl,
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
maxSentData,
|
||||
secretHeaders,
|
||||
getSecretResponse,
|
||||
} = request.data;
|
||||
|
||||
const hostname = urlify(url)?.hostname || '';
|
||||
|
||||
const prover: TProver = await new Prover({
|
||||
id: pluginHash,
|
||||
serverDns: hostname,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
});
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.prover_instantiated,
|
||||
data: {
|
||||
pluginHash,
|
||||
},
|
||||
});
|
||||
|
||||
const proofRequestStart = waitForEvent(
|
||||
OffscreenActionTypes.start_p2p_proof_request,
|
||||
);
|
||||
|
||||
const proverSetup = prover.setup(proverUrl);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.prover_setup,
|
||||
data: {
|
||||
pluginHash,
|
||||
},
|
||||
});
|
||||
|
||||
await proverSetup;
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.prover_started,
|
||||
data: {
|
||||
pluginHash,
|
||||
},
|
||||
});
|
||||
await proofRequestStart;
|
||||
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const transcript = await prover.transcript();
|
||||
|
||||
let secretResps: string[] = [];
|
||||
|
||||
if (getSecretResponse) {
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_secrets_from_transcript,
|
||||
data: {
|
||||
pluginHash,
|
||||
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'),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const endRequest = waitForEvent(OffscreenActionTypes.end_p2p_proof_request);
|
||||
await prover.reveal(commit);
|
||||
await endRequest;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
updateRequestProgress(id, RequestProgress.SendingRequest);
|
||||
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
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,
|
||||
})) as TPresentation;
|
||||
|
||||
const json = await presentation.json();
|
||||
return {
|
||||
...json,
|
||||
meta: {
|
||||
...json,
|
||||
notaryUrl: notaryUrl,
|
||||
websocketProxyUrl: websocketProxyUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 sending request',
|
||||
);
|
||||
|
||||
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 undefined:
|
||||
case '0.1.0-alpha.7':
|
||||
case '0.1.0-alpha.8':
|
||||
case '0.1.0-alpha.9':
|
||||
result = {
|
||||
sent: 'version not supported',
|
||||
recv: 'version not supported',
|
||||
};
|
||||
break;
|
||||
case '0.1.0-alpha.10':
|
||||
result = await verify(proof);
|
||||
break;
|
||||
}
|
||||
|
||||
return result!;
|
||||
}
|
||||
|
||||
async function verify(proof: PresentationJSON) {
|
||||
if (proof.version !== '0.1.0-alpha.10') {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
updateRequestProgress(id, RequestProgress.Error, errorMessage);
|
||||
await setNotaryRequestStatus(id, 'error');
|
||||
await setNotaryRequestError(
|
||||
id,
|
||||
errorMessage || error.message || 'Unknown error',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
15
src/entries/Offscreen/types.ts
Normal file
15
src/entries/Offscreen/types.ts
Normal file
@@ -0,0 +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',
|
||||
}
|
||||
39
src/entries/Offscreen/utils.ts
Normal file
39
src/entries/Offscreen/utils.ts
Normal 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;
|
||||
}
|
||||
9
src/entries/Offscreen/worker.ts
Normal file
9
src/entries/Offscreen/worker.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, Presentation, Verifier } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
Presentation,
|
||||
Verifier,
|
||||
});
|
||||
9
src/entries/Offscreen/workers-v9.ts
Normal file
9
src/entries/Offscreen/workers-v9.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, Presentation, Verifier } from 'tlsn-js-v9';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
Presentation,
|
||||
Verifier,
|
||||
});
|
||||
9
src/entries/Options/index.tsx
Normal file
9
src/entries/Options/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Options from '../../pages/Options';
|
||||
import './index.scss';
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
root.render(<Options />);
|
||||
197
src/entries/Popup/Popup.tsx
Normal file
197
src/entries/Popup/Popup.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
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 { 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';
|
||||
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 { isPopupWindow } from '../../utils/misc';
|
||||
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 { 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';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [isPopup, setIsPopup] = useState(isPopupWindow());
|
||||
useEffect(() => {
|
||||
fetchP2PState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
|
||||
dispatch(setActiveTab(tab || null));
|
||||
|
||||
const logs = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_requests,
|
||||
data: tab?.id,
|
||||
});
|
||||
|
||||
dispatch(setRequests(logs));
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_prove_requests,
|
||||
data: tab?.id,
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.runtime.onMessage.addListener((request) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.push_action: {
|
||||
if (
|
||||
request.data.tabId === store.getState().requests.activeTab?.id ||
|
||||
request.data.tabId === 'background'
|
||||
) {
|
||||
store.dispatch(request.action);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.change_route: {
|
||||
if (request.data.tabId === 'background') {
|
||||
navigate(request.route);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-hidden lg:w-[600px] lg:h-[800px] lg:border lg:m-auto lg:mt-40 lg:bg-white lg:shadow">
|
||||
<div className="flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
|
||||
<img
|
||||
className="absolute left-2 h-5 cursor-pointer"
|
||||
src={logo}
|
||||
alt="logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
<div className="flex flex-row flex-grow items-center justify-end gap-4">
|
||||
{!isPopup && (
|
||||
<>
|
||||
<AppConnectionLogo />
|
||||
<MenuIcon />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/requests/:requestId/*" element={<Request />} />
|
||||
<Route path="/notary/:requestId" element={<Notarize />} />
|
||||
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
|
||||
<Route path="/verify" element={<ProofUploader />} />
|
||||
<Route path="/history" element={<Home tab="history" />} />
|
||||
<Route path="/requests" element={<Home tab="network" />} />
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/plugins" element={<Plugins />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/plugininfo" element={<PluginUploadInfo />} />
|
||||
<Route path="/connection-approval" element={<ConnectionApproval />} />
|
||||
<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="/p2p" element={<P2PHome />} />
|
||||
<Route
|
||||
path="/install-plugin-approval"
|
||||
element={<InstallPluginApproval />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Popup</title>
|
||||
<title>TLSN Extension</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
|
||||
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "@fortawesome/fontawesome-free/scss/brands";
|
||||
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "@fortawesome/fontawesome-free/scss/regular";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
|
||||
|
||||
body {
|
||||
width: 480px;
|
||||
@@ -30,6 +30,10 @@ code {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@apply bg-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -47,6 +51,21 @@ code {
|
||||
@apply text-slate-700;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@apply bg-primary/[0.8];
|
||||
@apply text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-primary/[0.9];
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-50;
|
||||
@apply select-none;
|
||||
23
src/entries/Popup/index.tsx
Normal file
23
src/entries/Popup/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
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');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<Popup />
|
||||
</HashRouter>
|
||||
</Provider>,
|
||||
);
|
||||
400
src/entries/SidePanel/SidePanel.tsx
Normal file
400
src/entries/SidePanel/SidePanel.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import './sidePanel.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import {
|
||||
getPluginConfig,
|
||||
hexToArrayBuffer,
|
||||
makePlugin,
|
||||
PluginConfig,
|
||||
StepConfig,
|
||||
} from '../../utils/misc';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../../components/Icon';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
progressText,
|
||||
RequestProgress,
|
||||
} from '../Background/rpc';
|
||||
import { getPluginByHash, getPluginConfigByHash } 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 [hex, setHex] = useState('');
|
||||
const [p2p, setP2P] = useState(false);
|
||||
const [params, setParams] = useState<Record<string, string> | undefined>();
|
||||
const [started, setStarted] = useState(false);
|
||||
const clientId = useClientId();
|
||||
|
||||
useEffect(() => {
|
||||
fetchP2PState();
|
||||
browser.runtime.sendMessage({
|
||||
type: SidePanelActionTypes.panel_opened,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
browser.runtime.onMessage.addListener(async (request) => {
|
||||
const { type, data } = request;
|
||||
|
||||
switch (type) {
|
||||
case SidePanelActionTypes.execute_plugin_request: {
|
||||
setConfig(await getPluginConfigByHash(data.pluginHash));
|
||||
setHash(data.pluginHash);
|
||||
setParams(data.pluginParams);
|
||||
setStarted(true);
|
||||
break;
|
||||
}
|
||||
case SidePanelActionTypes.run_p2p_plugin_request: {
|
||||
const { pluginHash, plugin } = data;
|
||||
const config =
|
||||
(await getPluginConfigByHash(pluginHash)) ||
|
||||
(await getPluginConfig(hexToArrayBuffer(plugin)));
|
||||
|
||||
setHash(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);
|
||||
setHash('');
|
||||
setHex('');
|
||||
setStarted(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-slate-100 w-screen h-screen">
|
||||
<div className="relative flex flex-nowrap flex-shrink-0 flex-row items-center gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
|
||||
<img className="h-5" src={logo} alt="logo" />
|
||||
<button
|
||||
className="button absolute right-2"
|
||||
onClick={() => window.close()}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{/*{!config && <PluginList />}*/}
|
||||
{started && config && (
|
||||
<PluginBody
|
||||
hash={hash}
|
||||
hex={hex}
|
||||
config={config}
|
||||
p2p={p2p}
|
||||
clientId={clientId}
|
||||
presetParameterValues={params}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginBody(props: {
|
||||
config: PluginConfig;
|
||||
hash: string;
|
||||
hex?: string;
|
||||
clientId?: string;
|
||||
p2p?: boolean;
|
||||
presetParameterValues?: Record<string, string>;
|
||||
}): ReactElement {
|
||||
const { hash, hex, config, p2p, clientId, presetParameterValues } = props;
|
||||
const { title, description, icon, steps } = config;
|
||||
const [responses, setResponses] = useState<any[]>([]);
|
||||
const [notarizationId, setNotarizationId] = useState('');
|
||||
const notaryRequest = useRequestHistory(notarizationId);
|
||||
|
||||
const setResponse = useCallback(
|
||||
(response: any, i: number) => {
|
||||
const result = responses.concat();
|
||||
result[i] = response;
|
||||
setResponses(result);
|
||||
if (i === steps!.length - 1 && !!response) {
|
||||
setNotarizationId(response);
|
||||
}
|
||||
},
|
||||
[hash, responses],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (notaryRequest?.status === 'success') {
|
||||
browser.runtime.sendMessage({
|
||||
type: SidePanelActionTypes.execute_plugin_response,
|
||||
data: {
|
||||
hash,
|
||||
proof: notaryRequest.proof,
|
||||
},
|
||||
});
|
||||
} else if (notaryRequest?.status === 'error') {
|
||||
browser.runtime.sendMessage({
|
||||
type: SidePanelActionTypes.execute_plugin_response,
|
||||
data: {
|
||||
hash,
|
||||
error: notaryRequest.error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [hash, notaryRequest?.status]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<img className="w-12 h-12 self-start" src={icon || DefaultPluginIcon} />
|
||||
<div className="flex flex-col w-full items-start">
|
||||
<div className="font-bold flex flex-row h-6 items-center justify-between w-full text-base">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-slate-500 text-sm">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-8 mt-8">
|
||||
{steps?.map((step, i) => (
|
||||
<StepContent
|
||||
key={i}
|
||||
hash={hash}
|
||||
config={config}
|
||||
hex={hex}
|
||||
index={i}
|
||||
setResponse={setResponse}
|
||||
lastResponse={i > 0 ? responses[i - 1] : undefined}
|
||||
responses={responses}
|
||||
p2p={p2p}
|
||||
clientId={clientId}
|
||||
parameterValues={presetParameterValues}
|
||||
{...step}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContent(
|
||||
props: StepConfig & {
|
||||
hash: string;
|
||||
hex?: string;
|
||||
clientId?: string;
|
||||
index: number;
|
||||
setResponse: (resp: any, i: number) => void;
|
||||
responses: any[];
|
||||
lastResponse?: any;
|
||||
config: PluginConfig;
|
||||
p2p?: boolean;
|
||||
parameterValues?: Record<string, string>;
|
||||
},
|
||||
): ReactElement {
|
||||
const {
|
||||
index,
|
||||
title,
|
||||
description,
|
||||
cta,
|
||||
action,
|
||||
setResponse,
|
||||
lastResponse,
|
||||
prover,
|
||||
hash,
|
||||
hex: _hex,
|
||||
config,
|
||||
p2p = false,
|
||||
clientId = '',
|
||||
parameterValues,
|
||||
} = props;
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [notarizationId, setNotarizationId] = useState('');
|
||||
const notaryRequest = useRequestHistory(notarizationId);
|
||||
|
||||
const getPlugin = useCallback(async () => {
|
||||
const hex = (await getPluginByHash(hash)) || _hex;
|
||||
const arrayBuffer = hexToArrayBuffer(hex!);
|
||||
return makePlugin(arrayBuffer, config, { p2p, clientId });
|
||||
}, [hash, _hex, config, p2p, clientId]);
|
||||
|
||||
const processStep = useCallback(async () => {
|
||||
const plugin = await getPlugin();
|
||||
if (!plugin) return;
|
||||
if (index > 0 && !lastResponse) return;
|
||||
|
||||
setPending(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const out = await plugin.call(
|
||||
action,
|
||||
index > 0
|
||||
? JSON.stringify(lastResponse)
|
||||
: JSON.stringify(parameterValues),
|
||||
);
|
||||
console.log(out);
|
||||
const val = JSON.parse(out.string());
|
||||
if (val && prover) {
|
||||
setNotarizationId(val);
|
||||
} else {
|
||||
setCompleted(!!val);
|
||||
}
|
||||
setResponse(val, index);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e?.message || 'Unkonwn error');
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [action, index, lastResponse, prover, getPlugin]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (
|
||||
pending ||
|
||||
completed ||
|
||||
notaryRequest?.status === 'pending' ||
|
||||
notaryRequest?.status === 'success'
|
||||
)
|
||||
return;
|
||||
processStep();
|
||||
}, [processStep, pending, completed, notaryRequest]);
|
||||
|
||||
const viewProofInPopup = useCallback(async () => {
|
||||
if (!notaryRequest) return;
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.verify_prove_request,
|
||||
data: notaryRequest,
|
||||
});
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.open_popup,
|
||||
data: {
|
||||
position: {
|
||||
left: window.screen.width / 2 - 240,
|
||||
top: window.screen.height / 2 - 300,
|
||||
},
|
||||
route: `/verify/${notaryRequest.id}`,
|
||||
},
|
||||
});
|
||||
}, [notaryRequest, notarizationId]);
|
||||
|
||||
const viewP2P = useCallback(async () => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.open_popup,
|
||||
data: {
|
||||
position: {
|
||||
left: window.screen.width / 2 - 240,
|
||||
top: window.screen.height / 2 - 300,
|
||||
},
|
||||
route: `/p2p`,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
processStep();
|
||||
}, [processStep]);
|
||||
|
||||
let btnContent = null;
|
||||
|
||||
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) {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
'!bg-green-200 !text-black cursor-default border border-green-500 rounded',
|
||||
)}
|
||||
>
|
||||
<Icon className="text-green-600" fa="fa-solid fa-check" />
|
||||
<span className="text-sm">DONE</span>
|
||||
</button>
|
||||
);
|
||||
} else if (notaryRequest?.status === 'success') {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
)}
|
||||
onClick={viewProofInPopup}
|
||||
>
|
||||
<span className="text-sm">View</span>
|
||||
</button>
|
||||
);
|
||||
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
|
||||
btnContent = (
|
||||
<div className="flex flex-col gap-2">
|
||||
{notaryRequest?.progress === RequestProgress.Error && (
|
||||
<div className="flex flex-row items-center gap-2 text-red-600">
|
||||
<Icon fa="fa-solid fa-triangle-exclamation" size={1} />
|
||||
<span className="text-sm">
|
||||
{notaryRequest?.errorMessage ||
|
||||
progressText(notaryRequest.progress)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{notaryRequest?.progress !== RequestProgress.Error && (
|
||||
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
|
||||
<span className="text-sm">
|
||||
{notaryRequest?.progress !== undefined
|
||||
? `(${(((notaryRequest.progress + 1) / 6.06) * 100).toFixed()}%) ${progressText(notaryRequest.progress)}`
|
||||
: 'Pending...'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
)}
|
||||
disabled={index > 0 && typeof lastResponse === 'undefined'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="text-sm">{cta}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 text-base w-full">
|
||||
<div className="text-slate-500 self-start">{index + 1}.</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink w-0">
|
||||
<div
|
||||
className={classNames('font-semibold', {
|
||||
'line-through text-slate-500': completed,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{!!description && (
|
||||
<div className="text-slate-500 text-sm">{description}</div>
|
||||
)}
|
||||
{!!error && <div className="text-red-500 text-sm">{error}</div>}
|
||||
{btnContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/entries/SidePanel/index.html
Normal file
13
src/entries/SidePanel/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" width="480px">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Side Panel</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-container"></div>
|
||||
<div id="modal-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import Popup from './Popup';
|
||||
import './index.scss';
|
||||
import { Provider } from 'react-redux';
|
||||
import SidePanel from './SidePanel';
|
||||
import store from '../../utils/store';
|
||||
import { BackgroundActiontype } from '../Background/actionTypes';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
@@ -27,8 +24,6 @@ chrome.runtime.onMessage.addListener((request) => {
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<Popup />
|
||||
</HashRouter>
|
||||
<SidePanel />
|
||||
</Provider>,
|
||||
);
|
||||
28
src/entries/SidePanel/sidePanel.scss
Normal file
28
src/entries/SidePanel/sidePanel.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
|
||||
@import "../Popup/index.scss";
|
||||
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
10
src/entries/SidePanel/types.ts
Normal file
10
src/entries/SidePanel/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum SidePanelActionTypes {
|
||||
panel_opened = 'sidePanel/panel_opened',
|
||||
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
74
src/entries/utils.ts
Normal 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;
|
||||
};
|
||||
@@ -3,11 +3,16 @@
|
||||
"name": "TLSN Extension",
|
||||
"description": "A chrome extension for TLSN",
|
||||
"options_page": "options.html",
|
||||
"background": { "service_worker": "background.bundle.js" },
|
||||
"background": {
|
||||
"service_worker": "background.bundle.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": "icon-34.png"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidePanel.html"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
@@ -23,18 +28,16 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["content.styles.css", "icon-128.png", "icon-34.png"],
|
||||
"matches": []
|
||||
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "discord_dm.wasm", "twitter_profile.wasm"],
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
|
||||
}
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"permissions": [
|
||||
"offscreen",
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"storage",
|
||||
"webRequest",
|
||||
"activeTab",
|
||||
"<all_urls>"
|
||||
"sidePanel"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
export enum BackgroundActiontype {
|
||||
get_requests = 'get_requests',
|
||||
clear_requests = 'clear_requests',
|
||||
push_action = 'push_action',
|
||||
get_prove_requests = 'get_prove_requests',
|
||||
prove_request_start = 'prove_request_start',
|
||||
process_prove_request = 'process_prove_request',
|
||||
finish_prove_request = 'finish_prove_request',
|
||||
verify_prove_request = 'verify_prove_request',
|
||||
delete_prove_request = 'delete_prove_request',
|
||||
retry_prove_request = 'retry_prove_request',
|
||||
}
|
||||
|
||||
export type BackgroundAction = {
|
||||
type: BackgroundActiontype;
|
||||
data?: any;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export type RequestLog = {
|
||||
requestId: string;
|
||||
tabId: number;
|
||||
method: string;
|
||||
type: string;
|
||||
url: string;
|
||||
initiator: string | null;
|
||||
requestHeaders: chrome.webRequest.HttpHeader[];
|
||||
requestBody?: string;
|
||||
formData?: {
|
||||
[k: string]: string[];
|
||||
};
|
||||
responseHeaders?: chrome.webRequest.HttpHeader[];
|
||||
};
|
||||
|
||||
export type RequestHistory = {
|
||||
id: string;
|
||||
url: string;
|
||||
method: string;
|
||||
headers: { [key: string]: string };
|
||||
body: string;
|
||||
maxTranscriptSize: string;
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
status: '' | 'pending' | 'success' | 'error';
|
||||
error?: any;
|
||||
proof?: { session: any; substrings: any };
|
||||
requestBody?: any;
|
||||
verification?: {
|
||||
sent: string;
|
||||
recv: string;
|
||||
};
|
||||
};
|
||||
@@ -1,441 +0,0 @@
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
RequestLog,
|
||||
type RequestHistory,
|
||||
} from './actionTypes';
|
||||
|
||||
import { Mutex } from 'async-mutex';
|
||||
import NodeCache from 'node-cache';
|
||||
import { addRequest } from '../../reducers/requests';
|
||||
import { addRequestHistory } from '../../reducers/history';
|
||||
import { Level } from 'level';
|
||||
import charwise from 'charwise';
|
||||
|
||||
let RequestsLogs: {
|
||||
[tabId: string]: NodeCache;
|
||||
} = {};
|
||||
|
||||
const mutex = new Mutex();
|
||||
const cache = new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
|
||||
let creatingOffscreen;
|
||||
|
||||
chrome.tabs.onActivated.addListener((tabs) => {
|
||||
RequestsLogs[tabs.tabId] =
|
||||
RequestsLogs[tabs.tabId] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
});
|
||||
|
||||
chrome.tabs.onRemoved.addListener((tab) => {
|
||||
delete RequestsLogs[tab];
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const offscreenUrl = chrome.runtime.getURL('offscreen.html');
|
||||
const existingContexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
||||
documentUrls: [offscreenUrl],
|
||||
});
|
||||
|
||||
if (existingContexts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (creatingOffscreen) {
|
||||
await creatingOffscreen;
|
||||
} else {
|
||||
creatingOffscreen = (chrome as any).offscreen.createDocument({
|
||||
url: 'offscreen.html',
|
||||
reasons: ['WORKERS'],
|
||||
justification: 'workers for multithreading',
|
||||
});
|
||||
await creatingOffscreen;
|
||||
creatingOffscreen = null;
|
||||
}
|
||||
|
||||
chrome.webRequest.onSendHeaders.addListener(
|
||||
(details) => {
|
||||
mutex.runExclusive(async () => {
|
||||
const { method, tabId, requestId } = details;
|
||||
|
||||
if (method !== 'OPTIONS') {
|
||||
RequestsLogs[tabId] =
|
||||
RequestsLogs[tabId] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
|
||||
RequestsLogs[tabId].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,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['requestHeaders', 'extraHeaders'],
|
||||
);
|
||||
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(details) => {
|
||||
mutex.runExclusive(async () => {
|
||||
const { method, requestBody, tabId, requestId } = details;
|
||||
|
||||
if (method === 'OPTIONS') return;
|
||||
|
||||
if (requestBody) {
|
||||
RequestsLogs[tabId] =
|
||||
RequestsLogs[tabId] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
|
||||
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
|
||||
|
||||
if (requestBody.raw && requestBody.raw[0]?.bytes) {
|
||||
try {
|
||||
RequestsLogs[details.tabId].set(requestId, {
|
||||
...existing,
|
||||
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
|
||||
'utf-8',
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (requestBody.formData) {
|
||||
RequestsLogs[details.tabId].set(requestId, {
|
||||
...existing,
|
||||
formData: requestBody.formData,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['requestBody'],
|
||||
);
|
||||
|
||||
chrome.webRequest.onResponseStarted.addListener(
|
||||
(details) => {
|
||||
mutex.runExclusive(async () => {
|
||||
const { method, responseHeaders, tabId, requestId } = details;
|
||||
|
||||
if (method === 'OPTIONS') return;
|
||||
|
||||
RequestsLogs[tabId] =
|
||||
RequestsLogs[tabId] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
|
||||
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
|
||||
const newLog: RequestLog = {
|
||||
requestHeaders: [],
|
||||
...existing,
|
||||
method: details.method,
|
||||
type: details.type,
|
||||
url: details.url,
|
||||
initiator: details.initiator || null,
|
||||
tabId: tabId,
|
||||
requestId: requestId,
|
||||
responseHeaders,
|
||||
};
|
||||
|
||||
RequestsLogs[tabId].set(requestId, newLog);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: details.tabId,
|
||||
request: newLog,
|
||||
},
|
||||
action: addRequest(newLog),
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: ['<all_urls>'],
|
||||
},
|
||||
['responseHeaders', 'extraHeaders'],
|
||||
);
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
async (request, sender, sendResponse) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.get_requests: {
|
||||
const keys = RequestsLogs[request.data]?.keys() || [];
|
||||
const data = keys.map((key) => RequestsLogs[request.data]?.get(key));
|
||||
return sendResponse(data);
|
||||
}
|
||||
case BackgroundActiontype.clear_requests: {
|
||||
RequestsLogs = {};
|
||||
return sendResponse();
|
||||
}
|
||||
case BackgroundActiontype.get_prove_requests: {
|
||||
getNotaryRequests().then((reqs) => {
|
||||
for (const req of reqs) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(req),
|
||||
});
|
||||
}
|
||||
});
|
||||
return sendResponse();
|
||||
}
|
||||
case BackgroundActiontype.finish_prove_request: {
|
||||
const { id, proof, error, verification } = request.data;
|
||||
|
||||
if (proof) {
|
||||
const newReq = await addNotaryRequestProofs(id, proof);
|
||||
if (!newReq) return;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const newReq = await setNotaryRequestError(id, error);
|
||||
if (!newReq) return;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
}
|
||||
|
||||
if (verification) {
|
||||
const newReq = await setNotaryRequestVerification(id, verification);
|
||||
if (!newReq) return;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
}
|
||||
|
||||
return sendResponse();
|
||||
}
|
||||
case BackgroundActiontype.delete_prove_request: {
|
||||
const id = request.data;
|
||||
await removeNotaryRequest(id);
|
||||
return sendResponse();
|
||||
}
|
||||
case BackgroundActiontype.retry_prove_request: {
|
||||
const { id, notaryUrl, websocketProxyUrl } = request.data;
|
||||
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
|
||||
const req = await getNotaryRequest(id);
|
||||
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.process_prove_request,
|
||||
data: {
|
||||
...req,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return sendResponse();
|
||||
}
|
||||
case BackgroundActiontype.prove_request_start: {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
} = request.data;
|
||||
|
||||
const { id } = await addNotaryRequest(Date.now(), {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
});
|
||||
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.process_prove_request,
|
||||
data: {
|
||||
id,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return sendResponse();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
||||
const db = new Level('./ext-db', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const historyDb = db.sublevel('history', { valueEncoding: 'json' });
|
||||
|
||||
async function addNotaryRequest(
|
||||
now = Date.now(),
|
||||
request: RequestHistory,
|
||||
): Promise<RequestHistory> {
|
||||
const id = charwise.encode(now).toString('hex');
|
||||
const newReq = {
|
||||
...request,
|
||||
id,
|
||||
};
|
||||
await historyDb.put(id, newReq);
|
||||
return newReq;
|
||||
}
|
||||
|
||||
async function addNotaryRequestProofs(
|
||||
id: string,
|
||||
proof: { session: any; substrings: any },
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
proof,
|
||||
status: 'success',
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
async function setNotaryRequestStatus(
|
||||
id: string,
|
||||
status: '' | 'pending' | 'success' | 'error',
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
status,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
async function setNotaryRequestError(
|
||||
id: string,
|
||||
error: any,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
error,
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
async function setNotaryRequestVerification(
|
||||
id: string,
|
||||
verification: { sent: string; recv: string },
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
verification,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
async function removeNotaryRequest(id: string): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await historyDb.del(id);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function getNotaryRequests(): Promise<RequestHistory[]> {
|
||||
const retVal = [];
|
||||
for await (const [key, value] of historyDb.iterator()) {
|
||||
retVal.push(value);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
async function getNotaryRequest(id: string): Promise<RequestHistory | null> {
|
||||
return historyDb.get(id);
|
||||
}
|
||||
44
src/pages/BaseApproval/index.tsx
Normal file
44
src/pages/BaseApproval/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
|
||||
export function BaseApproval({
|
||||
onSecondaryClick,
|
||||
onPrimaryClick,
|
||||
header,
|
||||
children,
|
||||
secondaryCTAText = 'Cancel',
|
||||
primaryCTAText = 'Accept',
|
||||
}: {
|
||||
header: ReactNode;
|
||||
children: ReactNode;
|
||||
onSecondaryClick: () => void;
|
||||
onPrimaryClick: () => void;
|
||||
secondaryCTAText?: string;
|
||||
primaryCTAText?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="absolute flex flex-col items-center w-screen h-screen bg-white gap-2 cursor-default">
|
||||
<div className="w-full p-2 border-b border-gray-200 text-gray-500">
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<img className="h-5" src={logo} alt="logo" />
|
||||
<span className="font-semibold">{header}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-2 overflow-y-auto w-full">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex flex-row w-full gap-2 justify-end border-t p-4">
|
||||
{!!onSecondaryClick && !!secondaryCTAText && (
|
||||
<button className="button" onClick={onSecondaryClick}>
|
||||
{secondaryCTAText}
|
||||
</button>
|
||||
)}
|
||||
{!!onPrimaryClick && !!primaryCTAText && (
|
||||
<button className="button button--primary" onClick={onPrimaryClick}>
|
||||
{primaryCTAText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/pages/ConnectionApproval/index.tsx
Normal file
62
src/pages/ConnectionApproval/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
window.onerror = (error) => {
|
||||
console.log('error');
|
||||
console.log(error);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
console.log('Content script works!');
|
||||
console.log('Must reload extension for modifications to take effect.');
|
||||
})();
|
||||
139
src/pages/GetHistoryApproval/index.tsx
Normal file
139
src/pages/GetHistoryApproval/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
138
src/pages/GetPluginsApproval/index.tsx
Normal file
138
src/pages/GetPluginsApproval/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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, JSON.stringify(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>
|
||||
);
|
||||
}
|
||||
68
src/pages/GetProofApproval/index.tsx
Normal file
68
src/pages/GetProofApproval/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
168
src/pages/History/index.tsx
Normal file
168
src/pages/History/index.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { ReactElement, useState, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import NotarizeIcon from '../../assets/img/notarize.png';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
progressText,
|
||||
RequestProgress,
|
||||
} from '../../entries/Background/rpc';
|
||||
import Modal, { ModalContent } from '../../components/Modal/Modal';
|
||||
import classNames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import RequestMenu from './request-menu';
|
||||
|
||||
const charwise = require('charwise');
|
||||
|
||||
export default function History(): ReactElement {
|
||||
const history = useHistoryOrder();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap overflow-y-auto pb-36">
|
||||
{history
|
||||
.map((id) => {
|
||||
return <OneRequestHistory key={id} requestId={id} />;
|
||||
})
|
||||
.reverse()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OneRequestHistory(props: {
|
||||
requestId: string;
|
||||
className?: string;
|
||||
hideActions?: string[];
|
||||
}): ReactElement {
|
||||
const { hideActions = [] } = props;
|
||||
const dispatch = useDispatch();
|
||||
const request = useRequestHistory(props.requestId);
|
||||
const [showingError, showError] = useState(false);
|
||||
const [showingMenu, showMenu] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { status } = request || {};
|
||||
const requestUrl = urlify(request?.url || '');
|
||||
|
||||
const onView = useCallback(() => {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.verify_prove_request,
|
||||
data: request,
|
||||
});
|
||||
navigate('/verify/' + request?.id);
|
||||
}, [request]);
|
||||
|
||||
const onShowError = useCallback(async () => {
|
||||
showError(true);
|
||||
}, [request?.error, showError]);
|
||||
|
||||
const closeAllModal = useCallback(() => {
|
||||
showError(false);
|
||||
}, [showError]);
|
||||
|
||||
const day = dayjs(charwise.decode(props.requestId, 'hex'));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center flex-nowrap border rounded-md px-2.5 py-3 gap-0.5 hover:bg-slate-50 cursor-pointer relative',
|
||||
{
|
||||
'!cursor-default !bg-slate-200': status === 'pending',
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (status === 'success') onView();
|
||||
if (status === 'error') onShowError();
|
||||
}}
|
||||
>
|
||||
<ErrorModal />
|
||||
<div className="w-12 h-12 rounded-full flex flex-row items-center justify-center bg-slate-300">
|
||||
<img
|
||||
className="relative w-7 h-7 top-[-1px] opacity-60"
|
||||
src={NotarizeIcon}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0 gap-1">
|
||||
<div className="flex flex-row text-black text-sm font-semibold px-2 rounded-md overflow-hidden text-ellipsis gap-1">
|
||||
<span>Notarize request</span>
|
||||
<span className="font-normal border-b border-dashed border-slate-400 text-slate-500">
|
||||
{requestUrl?.hostname}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('font-semibold px-2 rounded-sm w-fit', {
|
||||
'text-green-600': status === 'success',
|
||||
'text-red-600': status === 'error',
|
||||
})}
|
||||
>
|
||||
{status === 'success' && 'Success'}
|
||||
{status === 'error' && 'Error'}
|
||||
{status === 'pending' && (
|
||||
<div className="text-center flex flex-row flex-grow-0 gap-2 self-end items-center justify-center text-slate-600">
|
||||
<Icon
|
||||
className="animate-spin"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
<span className="">
|
||||
{request?.progress === RequestProgress.Error
|
||||
? `${progressText(request.progress, request.errorMessage)}`
|
||||
: request?.progress
|
||||
? `(${(
|
||||
((request.progress + 1) / 6.06) *
|
||||
100
|
||||
).toFixed()}%) ${progressText(request.progress)}`
|
||||
: 'Pending...'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="h-4">
|
||||
{!hideActions.length && (
|
||||
<Icon
|
||||
className="text-slate-500 hover:text-slate-600 relative"
|
||||
fa="fa-solid fa-ellipsis"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showMenu(true);
|
||||
}}
|
||||
>
|
||||
{showingMenu && (
|
||||
<RequestMenu requestId={props.requestId} showMenu={showMenu} />
|
||||
)}
|
||||
</Icon>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-slate-500" title={day.format('LLLL')}>
|
||||
{day.fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function ErrorModal(): ReactElement {
|
||||
const msg = typeof request?.error === 'string' && request?.error;
|
||||
return !showingError ? (
|
||||
<></>
|
||||
) : (
|
||||
<Modal
|
||||
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500"
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-slate-500">
|
||||
{msg || request?.errorMessage}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
|
||||
onClick={closeAllModal}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
307
src/pages/History/request-menu.tsx
Normal file
307
src/pages/History/request-menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/pages/Home/index.scss
Normal file
5
src/pages/Home/index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
#home {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -3,126 +3,314 @@ import React, {
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useActiveTabUrl, useRequests } from '../../reducers/requests';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { filterByBookmarks } from '../../../utils/bookmark';
|
||||
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 { fetchPluginHashes } from '../../utils/rpc';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
import { useClientId } from '../../reducers/p2p';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
const requests = useRequests();
|
||||
const url = useActiveTabUrl();
|
||||
const navigate = useNavigate();
|
||||
const suggestions = filterByBookmarks(requests);
|
||||
export default function Home(props: {
|
||||
tab?: 'history' | 'network';
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
|
||||
const scrollableContent = useRef<HTMLDivElement | null>(null);
|
||||
const [shouldFix, setFix] = useState(false);
|
||||
const [actionPanelElement, setActionPanelElement] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPluginHashes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollableContent.current;
|
||||
if (!element) return;
|
||||
if (!actionPanelElement) return;
|
||||
|
||||
let timer = Date.now();
|
||||
const onScroll = () => {
|
||||
const now = Date.now();
|
||||
if (now - timer > 20) {
|
||||
timer = now;
|
||||
setScrollTop(element.scrollTop);
|
||||
if (element.scrollTop >= actionPanelElement.clientHeight) {
|
||||
setFix(true);
|
||||
} else {
|
||||
setFix(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('scroll', onScroll);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, [scrollableContent, actionPanelElement]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
|
||||
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
|
||||
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
|
||||
<span>Requests</span>
|
||||
<span>{`(${requests.length})`}</span>
|
||||
</NavButton>
|
||||
<NavButton
|
||||
fa="fa-solid fa-magnifying-glass"
|
||||
onClick={() => navigate('/custom')}
|
||||
<div
|
||||
id="home"
|
||||
ref={scrollableContent}
|
||||
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 z-10',
|
||||
{
|
||||
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40':
|
||||
shouldFix,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<TabSelector
|
||||
onClick={() => setTab('network')}
|
||||
selected={tab === 'network'}
|
||||
>
|
||||
Custom
|
||||
</NavButton>
|
||||
<NavButton
|
||||
fa="fa-solid fa-magnifying-glass"
|
||||
onClick={() => navigate('/verify')}
|
||||
disabled
|
||||
Network
|
||||
</TabSelector>
|
||||
<TabSelector
|
||||
onClick={() => setTab('history')}
|
||||
selected={tab === 'history'}
|
||||
>
|
||||
Verify
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
|
||||
History
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
|
||||
Options
|
||||
</NavButton>
|
||||
</TabSelector>
|
||||
</div>
|
||||
{!suggestions.length && (
|
||||
<div className="flex flex-col flex-nowrap">
|
||||
<div className="flex flex-col items-center justify-center text-slate-300 cursor-default select-none">
|
||||
<div>No available notarization for {url?.hostname}</div>
|
||||
<div>
|
||||
Browse <Link to="/requests">Requests</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col px-4 gap-4">
|
||||
{suggestions.map((bm, i) => {
|
||||
try {
|
||||
const reqs = requests.filter((req) => {
|
||||
return req?.url?.includes(bm.url);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-row items-center text-xs">
|
||||
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
|
||||
{bm.method}
|
||||
</div>
|
||||
<div className="text-slate-400 px-2 py-1 rounded-md">
|
||||
{bm.type}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-bold">{bm.title}</div>
|
||||
<div className="italic">{bm.description}</div>
|
||||
<div className="text-slate-300">
|
||||
Found {reqs.length} request
|
||||
</div>
|
||||
{reqs.map((r) => (
|
||||
<Link
|
||||
to={`/requests/${r.requestId}`}
|
||||
className="break-all text-slate-500 truncate"
|
||||
>
|
||||
{r.url}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
<div className="flex-grow">
|
||||
{tab === 'history' && <History />}
|
||||
{tab === 'network' && <Requests shouldFix={shouldFix} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionPanel({
|
||||
setActionPanelElement,
|
||||
scrollTop,
|
||||
}: {
|
||||
scrollTop: number;
|
||||
setActionPanelElement: (el: HTMLDivElement) => void;
|
||||
}) {
|
||||
const pluginHashes = usePluginHashes();
|
||||
const navigate = useNavigate();
|
||||
const clientId = useClientId();
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const [isOverflow, setOverflow] = useState(false);
|
||||
const [expanded, setExpand] = useState(false);
|
||||
|
||||
const onCheckSize = useCallback(() => {
|
||||
const element = container.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
setActionPanelElement(element);
|
||||
|
||||
if (element.scrollWidth > element.clientWidth) {
|
||||
setOverflow(true);
|
||||
} else {
|
||||
setOverflow(false);
|
||||
}
|
||||
}, [container]);
|
||||
|
||||
useEffect(() => {
|
||||
onCheckSize();
|
||||
|
||||
window.addEventListener('resize', onCheckSize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onCheckSize);
|
||||
};
|
||||
}, [onCheckSize, 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>
|
||||
<NavButton
|
||||
className={'relative'}
|
||||
fa="fa-solid fa-network-wired"
|
||||
iconSize={0.5}
|
||||
iconClassName={classNames({
|
||||
'!text-green-500': clientId,
|
||||
})}
|
||||
onClick={() => navigate('/p2p')}
|
||||
>
|
||||
P2P
|
||||
</NavButton>
|
||||
{pluginHashes.map((hash) => (
|
||||
<PluginIcon hash={hash} onCheckSize={onCheckSize} />
|
||||
))}
|
||||
<button
|
||||
className={
|
||||
'flex flex-row shrink-0 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,
|
||||
onCheckSize,
|
||||
}: {
|
||||
hash: string;
|
||||
onCheckSize: () => void;
|
||||
}) {
|
||||
const config = usePluginConfig(hash);
|
||||
const onPluginClick = useOnPluginClick(hash);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!config) return;
|
||||
onPluginClick();
|
||||
}, [onPluginClick, config]);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={() => {
|
||||
onCheckSize();
|
||||
}}
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center justify-center',
|
||||
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
className="rounded-full flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
|
||||
url={config?.icon || DefaultPluginIcon}
|
||||
size={2}
|
||||
/>
|
||||
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
|
||||
{config?.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TabSelector(props: {
|
||||
children: string;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
onClick: MouseEventHandler;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
className={classNames(
|
||||
'flex flex-grow items-center justify-center p-2 font-semibold hover:text-slate-700 border-b-2 ',
|
||||
{
|
||||
'font-semibold text-slate-400 border-white': !props.selected,
|
||||
'font-bold text-primary border-primary': props.selected,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function NavButton(props: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
onClick?: MouseEventHandler;
|
||||
className?: string;
|
||||
title?: string;
|
||||
iconClassName?: string;
|
||||
disabled?: boolean;
|
||||
iconSize?: number;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center justify-center',
|
||||
'text-white rounded px-2 py-1 gap-1',
|
||||
{
|
||||
'bg-primary/[.8] hover:bg-primary/[.7] active:bg-primary':
|
||||
!props.disabled,
|
||||
'bg-primary/[.5]': props.disabled,
|
||||
},
|
||||
'flex flex-col flex-nowrap items-center justify-center',
|
||||
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
title={props.title}
|
||||
>
|
||||
<Icon className="flex-grow-0 flex-shrink-0" fa={props.fa} size={1} />
|
||||
<span className="flex-grow flex-shrink w-0 flex-grow font-bold">
|
||||
<Icon
|
||||
className={classNames(
|
||||
'w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0',
|
||||
props.iconClassName,
|
||||
)}
|
||||
fa={props.fa}
|
||||
size={0.875}
|
||||
/>
|
||||
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
|
||||
{props.children}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
108
src/pages/InstallPluginApproval/index.tsx
Normal file
108
src/pages/InstallPluginApproval/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
567
src/pages/Notarize/index.tsx
Normal file
567
src/pages/Notarize/index.tsx
Normal file
@@ -0,0 +1,567 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
useCallback,
|
||||
ReactEventHandler,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { notarizeRequest, useRequest } from '../../reducers/requests';
|
||||
import Icon from '../../components/Icon';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import {
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getMaxSent,
|
||||
getMaxRecv,
|
||||
} from '../../utils/storage';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export default function Notarize(): ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const req = useRequest(params.requestId);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [step, setStep] = useState(0);
|
||||
const [secretHeaders, setSecretHeaders] = useState<string[]>([]);
|
||||
const [secretResps, setSecretResps] = useState<string[]>([]);
|
||||
|
||||
const notarize = useCallback(async () => {
|
||||
if (!req) return;
|
||||
const hostname = urlify(req.url)?.hostname;
|
||||
const notaryUrl = await getNotaryApi();
|
||||
const websocketProxyUrl = await getProxyApi();
|
||||
const maxSentData = await getMaxSent();
|
||||
const maxRecvData = await getMaxRecv();
|
||||
const headers: { [k: string]: string } = req.requestHeaders.reduce(
|
||||
(acc: any, h) => {
|
||||
acc[h.name] = h.value;
|
||||
return acc;
|
||||
},
|
||||
{ Host: hostname },
|
||||
);
|
||||
|
||||
//TODO: for some reason, these needs to be override to work
|
||||
headers['Accept-Encoding'] = 'identity';
|
||||
headers['Connection'] = 'close';
|
||||
|
||||
dispatch(
|
||||
// @ts-ignore
|
||||
notarizeRequest({
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.requestBody,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
}),
|
||||
);
|
||||
navigate(`/history`);
|
||||
}, [req, secretHeaders, secretResps]);
|
||||
|
||||
if (!req) return <></>;
|
||||
|
||||
let body;
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
body = (
|
||||
<RevealHeaderStep
|
||||
onNext={() => setStep(1)}
|
||||
onCancel={() => navigate(-1)}
|
||||
setSecretHeaders={setSecretHeaders}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
body = (
|
||||
<HideResponseStep
|
||||
onNext={notarize}
|
||||
onCancel={() => setStep(0)}
|
||||
setSecretResps={setSecretResps}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
body = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
|
||||
<Icon
|
||||
className="cursor-point text-slate-400 hover:text-slate-700"
|
||||
fa="fa-solid fa-xmark"
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<div className="flex flex-col flex-shrink flex-grow mr-20 w-0 select-none">
|
||||
<span className="font-bold text-slate-700">
|
||||
{`Notarizing a ${req.method} request`}
|
||||
</span>
|
||||
<span
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
title={req.url}
|
||||
>
|
||||
{req.url}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RevealHeaderStep(props: {
|
||||
onNext: () => void;
|
||||
onCancel: () => void;
|
||||
setSecretHeaders: (secrets: string[]) => void;
|
||||
}): ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const req = useRequest(params.requestId);
|
||||
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const headers = req?.requestHeaders;
|
||||
|
||||
useEffect(() => {
|
||||
if (!req) return;
|
||||
|
||||
props.setSecretHeaders(
|
||||
req.requestHeaders
|
||||
.map((h) => {
|
||||
if (!revealed[h.name]) {
|
||||
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter((d) => !!d),
|
||||
);
|
||||
}, [revealed]);
|
||||
|
||||
const changeHeaderKey = useCallback(
|
||||
(key: string, shouldReveal: boolean) => {
|
||||
if (!req) return;
|
||||
|
||||
setRevealed({
|
||||
...revealed,
|
||||
[key]: shouldReveal,
|
||||
});
|
||||
},
|
||||
[revealed, req],
|
||||
);
|
||||
|
||||
if (!headers) return <></>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
|
||||
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
|
||||
`Step 1 of 2: Select which request headers you want to reveal`
|
||||
</div>
|
||||
<div className="flex-grow flex-shrink h-0 overflow-y-auto">
|
||||
<table className="border border-slate-300 border-collapse table-fixed">
|
||||
<tbody className="bg-slate-200">
|
||||
{headers.map((h) => (
|
||||
<tr
|
||||
key={h.name}
|
||||
className={classNames('border-b border-slate-200 text-xs', {
|
||||
'bg-slate-50': revealed[h.name],
|
||||
})}
|
||||
>
|
||||
<td className="border border-slate-300 py-1 px-2 align-top">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer"
|
||||
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
|
||||
checked={revealed[h.name]}
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
|
||||
{h.name}
|
||||
</td>
|
||||
<td className="border border-slate-300 break-all align-top py-1 px-2">
|
||||
{revealed[h.name]
|
||||
? h.value
|
||||
: Array(h.value?.length || 0)
|
||||
.fill('*')
|
||||
.join('')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-2 gap-2 border-t">
|
||||
<button className="button" onClick={props.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
|
||||
onClick={props.onNext}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RevealHeaderTable(props: {
|
||||
headers: { name: string; value: string }[];
|
||||
className?: string;
|
||||
onChange: (revealed: { [key: string]: boolean }) => void;
|
||||
}) {
|
||||
const { headers } = props;
|
||||
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const changeHeaderKey = useCallback(
|
||||
(key: string, shouldReveal: boolean) => {
|
||||
const result = {
|
||||
...revealed,
|
||||
[key]: shouldReveal,
|
||||
};
|
||||
setRevealed(result);
|
||||
props.onChange(result);
|
||||
},
|
||||
[revealed],
|
||||
);
|
||||
|
||||
return (
|
||||
<table
|
||||
className={classNames(
|
||||
'border border-slate-300 border-collapse table-fixed',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<thead className="bg-slate-200">
|
||||
<th className="border border-slate-300 py-1 px-2 align-middle w-8"></th>
|
||||
<th className="border border-slate-300 py-1 px-2 align-middle">Name</th>
|
||||
<th className="border border-slate-300 py-1 px-2 align-middle">
|
||||
Value
|
||||
</th>
|
||||
</thead>
|
||||
<tbody className="bg-slate-100">
|
||||
{headers.map((h) => (
|
||||
<tr
|
||||
key={h.name}
|
||||
className={classNames('border-b border-slate-200 text-xs', {
|
||||
'bg-slate-50': revealed[h.name],
|
||||
})}
|
||||
>
|
||||
<td className="border border-slate-300 py-1 px-2 align-top w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer"
|
||||
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
|
||||
checked={revealed[h.name]}
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
|
||||
{h.name}
|
||||
</td>
|
||||
<td className="border border-slate-300 break-all align-top py-1 px-2">
|
||||
{revealed[h.name]
|
||||
? h.value
|
||||
: Array(h.value?.length || 0)
|
||||
.fill('*')
|
||||
.join('')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
export function HideResponseStep(props: {
|
||||
onNext: () => void;
|
||||
onCancel: () => void;
|
||||
setSecretResps: (secrets: string[]) => void;
|
||||
}): React.ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
const req = useRequest(params.requestId);
|
||||
const [responseText, setResponseText] = useState('');
|
||||
const [redactedRanges, setRedactedRanges] = useState<
|
||||
{ start: number; end: number }[]
|
||||
>([]);
|
||||
const [isRedactMode, setIsRedactMode] = useState(true);
|
||||
const taRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const onSelectionChange: React.MouseEventHandler<HTMLTextAreaElement> =
|
||||
useCallback(
|
||||
(e) => {
|
||||
const ta = e.currentTarget;
|
||||
if (isRedactMode && ta.selectionEnd > ta.selectionStart) {
|
||||
const newRange: { start: number; end: number } = {
|
||||
start: ta.selectionStart,
|
||||
end: ta.selectionEnd,
|
||||
};
|
||||
|
||||
setRedactedRanges((prevRanges) => {
|
||||
let updatedRanges = [...prevRanges, newRange].sort(
|
||||
(a, b) => a.start - b.start,
|
||||
);
|
||||
updatedRanges = mergeRanges(updatedRanges);
|
||||
|
||||
const secretResps = updatedRanges
|
||||
.map(({ start, end }) => responseText.substring(start, end))
|
||||
.filter((d) => !!d);
|
||||
props.setSecretResps(secretResps);
|
||||
|
||||
return updatedRanges;
|
||||
});
|
||||
} else if (!isRedactMode) {
|
||||
const clickPosition = ta.selectionStart;
|
||||
setRedactedRanges((prevRanges) => {
|
||||
const updatedRanges = prevRanges.filter(
|
||||
({ start, end }) => clickPosition < start || clickPosition > end,
|
||||
);
|
||||
|
||||
const secretResps = updatedRanges
|
||||
.map(({ start, end }) => responseText.substring(start, end))
|
||||
.filter((d) => !!d);
|
||||
props.setSecretResps(secretResps);
|
||||
|
||||
return updatedRanges;
|
||||
});
|
||||
}
|
||||
},
|
||||
[responseText, props, isRedactMode],
|
||||
);
|
||||
|
||||
const mergeRanges = (
|
||||
ranges: { start: number; end: number }[],
|
||||
): { start: number; end: number }[] => {
|
||||
if (ranges.length === 0) return [];
|
||||
const mergedRanges: { start: number; end: number }[] = [ranges[0]];
|
||||
|
||||
for (let i = 1; i < ranges.length; i++) {
|
||||
const lastRange = mergedRanges[mergedRanges.length - 1];
|
||||
if (ranges[i].start <= lastRange.end) {
|
||||
lastRange.end = Math.max(lastRange.end, ranges[i].end);
|
||||
} else {
|
||||
mergedRanges.push(ranges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedRanges;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!req) return;
|
||||
|
||||
const options = {
|
||||
method: req.method,
|
||||
headers: req.requestHeaders.reduce(
|
||||
// @ts-ignore
|
||||
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
|
||||
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
|
||||
acc[h.name] = h.value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
body: req.requestBody,
|
||||
};
|
||||
|
||||
if (req?.formData) {
|
||||
const formData = new URLSearchParams();
|
||||
Object.entries(req.formData).forEach(([key, values]) => {
|
||||
values.forEach((v) => formData.append(key, v));
|
||||
});
|
||||
options.body = formData.toString();
|
||||
}
|
||||
|
||||
replay(req.url, options).then((resp) => setResponseText(resp));
|
||||
}, [req]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = taRef.current;
|
||||
|
||||
if (current) {
|
||||
current.focus();
|
||||
}
|
||||
}, [taRef]);
|
||||
|
||||
if (!req) return <></>;
|
||||
|
||||
const shieldedText = responseText.split('');
|
||||
redactedRanges.forEach(({ start, end }) => {
|
||||
for (let i = start; i < end; i++) {
|
||||
shieldedText[i] = '*';
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
|
||||
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
|
||||
Step 2 of 2:{' '}
|
||||
{isRedactMode
|
||||
? 'Highlight text to redact selected portions'
|
||||
: 'Click redacted text to unredact'}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-0.5 gap-2 border-t">
|
||||
<button
|
||||
className={`bg-${isRedactMode ? 'red-500' : 'green-500'} text-white font-bold hover:bg-${isRedactMode ? 'red-400' : 'green-400'} px-2 py-0.5 active:bg-${isRedactMode ? 'red-600' : 'green-600'}`}
|
||||
onClick={() => setIsRedactMode(!isRedactMode)}
|
||||
>
|
||||
{isRedactMode ? 'Unredact Text' : 'Redact Text'}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-500 text-white font-bold hover:bg-gray-400 px-2 py-0.5 active:bg-gray-600"
|
||||
onClick={() => setRedactedRanges([])}
|
||||
>
|
||||
Unredact All
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
|
||||
<textarea
|
||||
ref={taRef}
|
||||
className="flex-grow textarea bg-slate-100 font-mono"
|
||||
value={shieldedText.join('')}
|
||||
onMouseUp={onSelectionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end p-2 gap-2 border-t">
|
||||
<button className="button" onClick={props.onCancel}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
|
||||
onClick={props.onNext}
|
||||
>
|
||||
Notarize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RedactBodyTextarea(props: {
|
||||
className?: string;
|
||||
onChange: (secretResponse: string[]) => void;
|
||||
request: {
|
||||
url: string;
|
||||
method?: string;
|
||||
headers?: { [name: string]: string };
|
||||
formData?: { [k: string]: string[] };
|
||||
body?: string;
|
||||
};
|
||||
}) {
|
||||
const { className, onChange, request } = props;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [responseText, setResponseText] = useState('');
|
||||
const [start, setStart] = useState(0);
|
||||
const [end, setEnd] = useState(0);
|
||||
const taRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(e) => {
|
||||
const ta = e.currentTarget;
|
||||
if (ta.selectionEnd > ta.selectionStart) {
|
||||
setStart(ta.selectionStart);
|
||||
setEnd(ta.selectionEnd);
|
||||
onChange(
|
||||
[
|
||||
responseText.substring(0, ta.selectionStart),
|
||||
responseText.substring(ta.selectionEnd, responseText.length),
|
||||
].filter((d) => !!d),
|
||||
);
|
||||
}
|
||||
},
|
||||
[responseText],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
};
|
||||
|
||||
if (request?.formData) {
|
||||
const formData = new URLSearchParams();
|
||||
Object.entries(request.formData).forEach(([key, values]) => {
|
||||
values.forEach((v) => formData.append(key, v));
|
||||
});
|
||||
options.body = formData.toString();
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
replay(request.url, options).then((resp) => {
|
||||
setResponseText(resp);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = taRef.current;
|
||||
|
||||
if (current) {
|
||||
current.focus();
|
||||
current.setSelectionRange(start, end);
|
||||
}
|
||||
}, [taRef, start, end]);
|
||||
|
||||
let shieldedText = '';
|
||||
|
||||
if (end > start) {
|
||||
shieldedText = Array(start)
|
||||
.fill('*')
|
||||
.join('')
|
||||
.concat(responseText.substring(start, end))
|
||||
.concat(
|
||||
Array(responseText.length - end)
|
||||
.fill('*')
|
||||
.join(''),
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center !pt-4 flex-grow textarea bg-slate-100">
|
||||
<Icon
|
||||
className="animate-spin w-fit text-slate-500"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={taRef}
|
||||
className={classNames(
|
||||
'flex-grow textarea bg-slate-100 font-mono',
|
||||
className,
|
||||
)}
|
||||
value={shieldedText || responseText}
|
||||
onSelect={onSelectionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const replay = async (url: string, options: any) => {
|
||||
const resp = await fetch(url, options);
|
||||
const contentType =
|
||||
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
return resp.text();
|
||||
} else if (contentType?.includes('text')) {
|
||||
return resp.text();
|
||||
} else if (contentType?.includes('image')) {
|
||||
return resp.blob().then((blob) => blob.text());
|
||||
} else {
|
||||
return resp.blob().then((blob) => blob.text());
|
||||
}
|
||||
};
|
||||
178
src/pages/NotarizeApproval/index.tsx
Normal file
178
src/pages/NotarizeApproval/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import { BaseApproval } from '../BaseApproval';
|
||||
import { RedactBodyTextarea, RevealHeaderTable } from '../Notarize';
|
||||
|
||||
export function NotarizeApproval(): ReactElement {
|
||||
const [params] = useSearchParams();
|
||||
const origin = params.get('origin');
|
||||
const favIconUrl = params.get('favIconUrl');
|
||||
const config = JSON.parse(params.get('config')!);
|
||||
const hostname = urlify(origin || '')?.hostname;
|
||||
const [step, setStep] = useState<'overview' | 'headers' | 'response'>(
|
||||
'overview',
|
||||
);
|
||||
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
|
||||
const [secretResps, setSecretResps] = useState<string[]>([]);
|
||||
|
||||
const headerList = Object.entries(config.headers || {}).map(
|
||||
([name, value]) => ({
|
||||
name,
|
||||
value: String(value),
|
||||
}),
|
||||
);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
if (step === 'headers') return setStep('overview');
|
||||
if (step === 'response') return setStep('headers');
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.notarize_response,
|
||||
data: false,
|
||||
});
|
||||
}, [step]);
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
if (step === 'overview') return setStep('headers');
|
||||
if (step === 'headers') return setStep('response');
|
||||
|
||||
const secretHeaders = headerList
|
||||
.map((h) => {
|
||||
if (!revealed[h.name]) {
|
||||
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter((d) => !!d);
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.notarize_response,
|
||||
data: {
|
||||
...config,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
},
|
||||
});
|
||||
}, [revealed, step, secretResps, config]);
|
||||
|
||||
let body, headerText, primaryCta, secondaryCta;
|
||||
|
||||
switch (step) {
|
||||
case 'overview':
|
||||
headerText = 'Notarizing Request';
|
||||
primaryCta = 'Next';
|
||||
secondaryCta = 'Cancel';
|
||||
body = (
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-2 py-8">
|
||||
{!!favIconUrl ? (
|
||||
<img
|
||||
src={favIconUrl}
|
||||
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
|
||||
alt="logo"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
fa="fa-solid fa-globe"
|
||||
size={4}
|
||||
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
|
||||
/>
|
||||
)}
|
||||
<div className="text-2xl text-center px-8">
|
||||
<b className="text-blue-500">{hostname}</b> wants to notarize the
|
||||
following request:
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow break-all">
|
||||
<table className="border border-collapse table-auto rounded text-xs w-full">
|
||||
<tbody>
|
||||
<TableRow label="Method" value={config.method?.toUpperCase()} />
|
||||
<TableRow label="Request URL" value={config.url} />
|
||||
<TableRow label="Notary URL" value={config.notaryUrl} />
|
||||
<TableRow label="Proxy URL" value={config.websocketProxyUrl} />
|
||||
<TableRow label="Max Sent" value={config.maxSentData} />
|
||||
<TableRow label="Max Recv" value={config.maxRecvData} />
|
||||
{config.metadata && (
|
||||
<TableRow
|
||||
label="Metadata"
|
||||
value={JSON.stringify(config.metadata)}
|
||||
/>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-xs px-8 pb-2 text-center text-slate-500">
|
||||
You will be able to review and redact headers and response body.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'headers':
|
||||
headerText = 'Step 1 of 2: Select headers to reveal';
|
||||
primaryCta = 'Next';
|
||||
secondaryCta = 'Back';
|
||||
body = (
|
||||
<div className="px-2 flex flex-col">
|
||||
<RevealHeaderTable
|
||||
className="w-full"
|
||||
onChange={setRevealed}
|
||||
headers={headerList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'response':
|
||||
headerText = 'Step 2 of 2: Highlight response to keep';
|
||||
primaryCta = 'Notarize';
|
||||
secondaryCta = 'Back';
|
||||
body = (
|
||||
<div className="px-2 flex flex-col flex-grow">
|
||||
<RedactBodyTextarea
|
||||
className="w-full "
|
||||
onChange={setSecretResps}
|
||||
request={{
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
headers: config.headers,
|
||||
body: config.body,
|
||||
formData: config.formData,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseApproval
|
||||
header={headerText}
|
||||
onSecondaryClick={onCancel}
|
||||
onPrimaryClick={onAccept}
|
||||
primaryCTAText={primaryCta}
|
||||
secondaryCTAText={secondaryCta}
|
||||
>
|
||||
{body}
|
||||
</BaseApproval>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<tr>
|
||||
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top text-left w-24">
|
||||
{label}
|
||||
</td>
|
||||
<td className="px-2 py-1 border border-slate-300 font-semibold text-slate-800 text-left">
|
||||
<input
|
||||
className="outline-0 flex-grow cursor-default w-full"
|
||||
type="text"
|
||||
value={value}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import * as Comlink from 'comlink';
|
||||
import { BackgroundActiontype } from '../Background/actionTypes';
|
||||
|
||||
const TLSN: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
let tlsn: any | null = null;
|
||||
|
||||
async function getTLSN(): Promise<any | null> {
|
||||
if (tlsn) return tlsn;
|
||||
tlsn = await new TLSN();
|
||||
return tlsn;
|
||||
}
|
||||
|
||||
const Offscreen = () => {
|
||||
useEffect(() => {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
async (request, sender, sendResponse) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.process_prove_request: {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body = '',
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
id,
|
||||
} = request.data;
|
||||
|
||||
const tlsn = await getTLSN();
|
||||
|
||||
try {
|
||||
const proof = await tlsn.prover(url, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
});
|
||||
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
proof,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.verify_prove_request: {
|
||||
const tlsn = await getTLSN();
|
||||
|
||||
const result = await tlsn.verify(
|
||||
request.data.proof,
|
||||
`-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBv36FI4ZFszJa0DQFJ3wWCXvVLFr\ncRzMG5kaTeHGoSzDu6cFqx3uEWYpFGo6C0EOUgf+mEgbktLrXocv5yHzKg==\n-----END PUBLIC KEY-----`,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id: request.data.id,
|
||||
verification: {
|
||||
sent: result.sent,
|
||||
recv: result.recv,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return <div className="App" />;
|
||||
};
|
||||
|
||||
export default Offscreen;
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import { urlify, devlog } from '../../utils/misc';
|
||||
import init, {
|
||||
initThreadPool,
|
||||
prover,
|
||||
verify,
|
||||
} from '../../../wasm/prover/pkg/tlsn_extension_rs';
|
||||
|
||||
class TLSN {
|
||||
private startPromise: any;
|
||||
private resolveStart: any;
|
||||
|
||||
constructor() {
|
||||
console.log('worker module initiated.');
|
||||
this.startPromise = new Promise((resolve) => {
|
||||
this.resolveStart = resolve;
|
||||
});
|
||||
this.start();
|
||||
}
|
||||
|
||||
async start() {
|
||||
devlog('start');
|
||||
const numConcurrency = navigator.hardwareConcurrency;
|
||||
devlog('!@# navigator.hardwareConcurrency=', numConcurrency);
|
||||
const res = await init();
|
||||
devlog('!@# res.memory=', res.memory);
|
||||
// 6422528 ~= 6.12 mb
|
||||
devlog('!@# res.memory.buffer.length=', res.memory.buffer.byteLength);
|
||||
await initThreadPool(numConcurrency);
|
||||
this.resolveStart();
|
||||
}
|
||||
|
||||
async waitForStart() {
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
async prover(
|
||||
url: string,
|
||||
options?: {
|
||||
method?: string;
|
||||
headers?: { [key: string]: string };
|
||||
body?: string;
|
||||
maxTranscriptSize?: number;
|
||||
notaryUrl?: string;
|
||||
websocketProxyUrl?: string;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
await this.waitForStart();
|
||||
console.log('worker', url, {
|
||||
...options,
|
||||
notaryUrl: options.notaryUrl,
|
||||
websocketProxyUrl: options.websocketProxyUrl,
|
||||
});
|
||||
const resProver = await prover(url, {
|
||||
...options,
|
||||
notaryUrl: options.notaryUrl,
|
||||
websocketProxyUrl: options.websocketProxyUrl,
|
||||
});
|
||||
const resJSON = JSON.parse(resProver);
|
||||
devlog('!@# resProver,resJSON=', { resProver, resJSON });
|
||||
devlog('!@# resAfter.memory=', resJSON.memory);
|
||||
// 1105920000 ~= 1.03 gb
|
||||
devlog(
|
||||
'!@# resAfter.memory.buffer.length=',
|
||||
resJSON.memory?.buffer?.byteLength,
|
||||
);
|
||||
|
||||
return resJSON;
|
||||
} catch (e: any) {
|
||||
devlog(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
async verify(proof: any, pubkey: string) {
|
||||
await this.waitForStart();
|
||||
const raw = await verify(JSON.stringify(proof), pubkey);
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
}
|
||||
|
||||
Comlink.expose(TLSN);
|
||||
@@ -1,9 +1,345 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
} from 'react';
|
||||
import {
|
||||
set,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
MAX_SENT_LS_KEY,
|
||||
MAX_RECEIVED_LS_KEY,
|
||||
getMaxSent,
|
||||
getMaxRecv,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getLoggingFilter,
|
||||
LOGGING_FILTER_KEY,
|
||||
getRendezvousApi,
|
||||
RENDEZVOUS_API_LS_KEY,
|
||||
} from '../../utils/storage';
|
||||
import {
|
||||
EXPLORER_API,
|
||||
NOTARY_API,
|
||||
NOTARY_PROXY,
|
||||
MAX_RECV,
|
||||
MAX_SENT,
|
||||
RENDEZVOUS_API,
|
||||
} from '../../utils/constants';
|
||||
import Modal, { ModalContent } from '../../components/Modal/Modal';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { LoggingLevel } from 'tlsn-js';
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
import Options from '../../components/Options';
|
||||
import './index.css';
|
||||
export default function Options(): ReactElement {
|
||||
const [notary, setNotary] = useState(NOTARY_API);
|
||||
const [proxy, setProxy] = useState(NOTARY_PROXY);
|
||||
const [maxSent, setMaxSent] = useState(MAX_SENT);
|
||||
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
|
||||
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
|
||||
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
root.render(<Options />);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [shouldReload, setShouldReload] = useState(false);
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const [showReloadModal, setShowReloadModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setNotary(await getNotaryApi());
|
||||
setProxy(await getProxyApi());
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setMaxReceived((await getMaxRecv()) || MAX_RECV);
|
||||
setMaxSent((await getMaxSent()) || MAX_SENT);
|
||||
setLoggingLevel((await getLoggingFilter()) || 'Info');
|
||||
setRendezvous((await getRendezvousApi()) || RENDEZVOUS_API);
|
||||
})();
|
||||
}, [advanced]);
|
||||
|
||||
const onSave = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>, skipCheck = false) => {
|
||||
if (!skipCheck && shouldReload) {
|
||||
setShowReloadModal(true);
|
||||
return;
|
||||
}
|
||||
await set(NOTARY_API_LS_KEY, notary);
|
||||
await set(PROXY_API_LS_KEY, proxy);
|
||||
await set(MAX_SENT_LS_KEY, maxSent.toString());
|
||||
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
|
||||
await set(LOGGING_FILTER_KEY, loggingLevel);
|
||||
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
|
||||
setDirty(false);
|
||||
},
|
||||
[
|
||||
notary,
|
||||
proxy,
|
||||
maxSent,
|
||||
maxReceived,
|
||||
loggingLevel,
|
||||
rendezvous,
|
||||
shouldReload,
|
||||
],
|
||||
);
|
||||
|
||||
const onSaveAndReload = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
await onSave(e, true);
|
||||
browser.runtime.reload();
|
||||
},
|
||||
[onSave],
|
||||
);
|
||||
|
||||
const onAdvanced = useCallback(() => {
|
||||
setAdvanced(!advanced);
|
||||
}, [advanced]);
|
||||
|
||||
const openInTab = useCallback((url: string) => {
|
||||
browser.tabs.create({ url });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
|
||||
{showReloadModal && (
|
||||
<Modal
|
||||
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
|
||||
onClose={() => setShowReloadModal(false)}
|
||||
>
|
||||
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
|
||||
Modifying your logging your will require your extension to reload.
|
||||
Do you want to proceed?
|
||||
</ModalContent>
|
||||
<div className="flex flex-row justify-end items-center gap-2 w-full">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setShowReloadModal(false)}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
className="button button--primary"
|
||||
onClick={onSaveAndReload}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
<div className="flex flex-row flex-nowrap justify-between items-between py-1 px-2 gap-2">
|
||||
<p className="font-bold text-base">Settings</p>
|
||||
</div>
|
||||
<NormalOptions
|
||||
notary={notary}
|
||||
setNotary={setNotary}
|
||||
proxy={proxy}
|
||||
setProxy={setProxy}
|
||||
setDirty={setDirty}
|
||||
/>
|
||||
<div className="justify-left px-2 pt-3 gap-2">
|
||||
<button className="font-bold" onClick={onAdvanced}>
|
||||
<i
|
||||
className={
|
||||
advanced
|
||||
? 'fa-solid fa-caret-down pr-1'
|
||||
: 'fa-solid fa-caret-right pr-1'
|
||||
}
|
||||
></i>
|
||||
Advanced
|
||||
</button>
|
||||
</div>
|
||||
{!advanced ? (
|
||||
<></>
|
||||
) : (
|
||||
<AdvancedOptions
|
||||
maxSent={maxSent}
|
||||
setMaxSent={setMaxSent}
|
||||
maxReceived={maxReceived}
|
||||
setMaxReceived={setMaxReceived}
|
||||
setDirty={setDirty}
|
||||
loggingLevel={loggingLevel}
|
||||
setLoggingLevel={setLoggingLevel}
|
||||
setShouldReload={setShouldReload}
|
||||
rendezvous={rendezvous}
|
||||
setRendezvous={setRendezvous}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
disabled={!dirty}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col w-full items-end gap-2 p-2">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() =>
|
||||
openInTab('https://github.com/tlsnotary/tlsn-extension/issues/new')
|
||||
}
|
||||
>
|
||||
File an issue
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => openInTab('https://discord.gg/9XwESXtcN7')}
|
||||
>
|
||||
Join our Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputField(props: {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
min?: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const { label, placeholder, value, type, min, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold cursor-default">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
className="input border"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NormalOptions(props: {
|
||||
notary: string;
|
||||
setNotary: (value: string) => void;
|
||||
proxy: string;
|
||||
setProxy: (value: string) => void;
|
||||
setDirty: (value: boolean) => void;
|
||||
}) {
|
||||
const { notary, setNotary, proxy, setProxy, setDirty } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
|
||||
<div className="font-semibold">Version</div>
|
||||
<div className="input border bg-slate-100">{version}</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Notary API"
|
||||
placeholder="https://api.tlsnotary.org"
|
||||
value={notary}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setNotary(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Proxy API"
|
||||
placeholder="https://proxy.tlsnotary.org"
|
||||
value={proxy}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setProxy(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
|
||||
<div className="font-semibold">Explorer URL</div>
|
||||
<div className="input border bg-slate-100">{EXPLORER_API}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedOptions(props: {
|
||||
maxSent: number;
|
||||
maxReceived: number;
|
||||
loggingLevel: LoggingLevel;
|
||||
rendezvous: string;
|
||||
setShouldReload: (reload: boolean) => void;
|
||||
setMaxSent: (value: number) => void;
|
||||
setMaxReceived: (value: number) => void;
|
||||
setDirty: (value: boolean) => void;
|
||||
setLoggingLevel: (level: LoggingLevel) => void;
|
||||
setRendezvous: (api: string) => void;
|
||||
}) {
|
||||
const {
|
||||
maxSent,
|
||||
setMaxSent,
|
||||
maxReceived,
|
||||
setMaxReceived,
|
||||
setDirty,
|
||||
setLoggingLevel,
|
||||
loggingLevel,
|
||||
setShouldReload,
|
||||
rendezvous,
|
||||
setRendezvous,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputField
|
||||
label="Set Max Received Data"
|
||||
value={maxReceived.toString()}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxReceived(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Set Max Sent Data"
|
||||
value={maxSent.toString()}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxSent(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Rendezvous API (for P2P)"
|
||||
value={rendezvous}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setRendezvous(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Logging Level</div>
|
||||
<select
|
||||
className="select !bg-white border !px-2 !py-1"
|
||||
onChange={(e) => {
|
||||
setLoggingLevel(e.target.value as LoggingLevel);
|
||||
setDirty(true);
|
||||
setShouldReload(true);
|
||||
}}
|
||||
value={loggingLevel}
|
||||
>
|
||||
<option value="Error">Error</option>
|
||||
<option value="Warn">Warn</option>
|
||||
<option value="Info">Info</option>
|
||||
<option value="Debug">Debug</option>
|
||||
<option value="Trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
498
src/pages/PeerToPeer/index.tsx
Normal file
498
src/pages/PeerToPeer/index.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
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 { verify } from 'tlsn-js-v5';
|
||||
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>
|
||||
);
|
||||
}
|
||||
10
src/pages/Plugins/index.tsx
Normal file
10
src/pages/Plugins/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import { PluginList } from "../../components/PluginList";
|
||||
|
||||
export default function Plugins(): ReactElement {
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<PluginList className="p-2 overflow-y-auto" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Navigate, Route, Routes, useNavigate } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
setActiveTab,
|
||||
setRequests,
|
||||
useActiveTab,
|
||||
useActiveTabUrl,
|
||||
} from '../../reducers/requests';
|
||||
import { BackgroundActiontype } from '../Background/actionTypes';
|
||||
import Requests from '../Requests';
|
||||
import Options from '../../components/Options';
|
||||
import Request from '../Requests/Request';
|
||||
import Home from '../Home';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
import RequestBuilder from '../RequestBuilder';
|
||||
import Notarize from '../../components/Notarize';
|
||||
import ProofViewer from '../../components/ProofViewer';
|
||||
import History from '../../components/History';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
const activeTab = useActiveTab();
|
||||
const url = useActiveTabUrl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [tab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
|
||||
dispatch(setActiveTab(tab || null));
|
||||
|
||||
const logs = await chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_requests,
|
||||
data: tab?.id,
|
||||
});
|
||||
|
||||
dispatch(setRequests(logs));
|
||||
|
||||
const history = await chrome.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_prove_requests,
|
||||
data: tab?.id,
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
||||
<div className="flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
|
||||
<img
|
||||
className="absolute left-2 h-5 cursor-pointer"
|
||||
src={logo}
|
||||
alt="logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
<div className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit">
|
||||
<img
|
||||
src={activeTab?.favIconUrl}
|
||||
className="h-5 rounded-full"
|
||||
alt="logo"
|
||||
/>
|
||||
<div className="text-xs">{url?.hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/requests/:requestId/*" element={<Request />} />
|
||||
<Route path="/notary/:requestId/*" element={<Notarize />} />
|
||||
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/requests" element={<Requests />} />
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
104
src/pages/ProofUploader/index.tsx
Normal file
104
src/pages/ProofUploader/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
useCallback,
|
||||
ChangeEventHandler,
|
||||
} from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import ProofViewer from '../ProofViewer';
|
||||
import { convertNotaryWsToHttp } from '../../utils/misc';
|
||||
|
||||
export default function ProofUploader(): ReactElement {
|
||||
const [proof, setProof] = useState<{
|
||||
recv: string;
|
||||
sent: string;
|
||||
verifierKey?: string;
|
||||
notaryKey?: string;
|
||||
} | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [metadata, setMetaData] = useState<any>({ meta: '', version: '' });
|
||||
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
async (e) => {
|
||||
// @ts-ignore
|
||||
const [file] = e.target.files || [];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', async (event) => {
|
||||
const result = event.target?.result;
|
||||
if (result) {
|
||||
const proof = JSON.parse(result as string);
|
||||
const notaryUrl = convertNotaryWsToHttp(proof.meta.notaryUrl);
|
||||
proof.meta.notaryUrl = notaryUrl;
|
||||
setMetaData({ meta: proof.meta, version: proof.version });
|
||||
const res = await chrome.runtime
|
||||
.sendMessage<
|
||||
any,
|
||||
{
|
||||
recv: string;
|
||||
sent: string;
|
||||
verifierKey?: string;
|
||||
notaryKey?: string;
|
||||
}
|
||||
>({
|
||||
type: BackgroundActiontype.verify_proof,
|
||||
data: proof,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (proof) {
|
||||
setUploading(false);
|
||||
setProof(res);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setUploading(true);
|
||||
reader.readAsText(file);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (proof) {
|
||||
return (
|
||||
<ProofViewer
|
||||
recv={proof.recv}
|
||||
sent={proof.sent}
|
||||
verifierKey={proof.verifierKey}
|
||||
notaryKey={proof.notaryKey}
|
||||
info={metadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow flex-shrink h-0 overflow-y-auto">
|
||||
<div className="flex flex-col items-center justify-center relative border-slate-400 border-2 text-slate-500 border-dashed flex-grow flex-shrink h-0 m-2 bg-slate-200">
|
||||
<input
|
||||
type="file"
|
||||
className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
|
||||
onChange={onFileUpload}
|
||||
accept=".json"
|
||||
disabled={uploading}
|
||||
/>
|
||||
{uploading ? (
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
|
||||
) : (
|
||||
<>
|
||||
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
|
||||
<div className="text-lg">Drop your proof here to continue</div>
|
||||
<div className="text-sm">or</div>
|
||||
<button
|
||||
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white cursor-pointer"
|
||||
onClick={() => null}
|
||||
>
|
||||
Browse Files
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
223
src/pages/ProofViewer/index.tsx
Normal file
223
src/pages/ProofViewer/index.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, {
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
useState,
|
||||
useEffect,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import c from 'classnames';
|
||||
import {
|
||||
deleteRequestHistory,
|
||||
useRequestHistory,
|
||||
} from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import {
|
||||
convertNotaryWsToHttp,
|
||||
download,
|
||||
isPopupWindow,
|
||||
} from '../../utils/misc';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { RemoveHistory } from '../History/request-menu';
|
||||
import { PresentationJSON } from 'tlsn-js/build/types';
|
||||
import { RequestHistory } from '../../entries/Background/rpc';
|
||||
|
||||
export default function ProofViewer(props?: {
|
||||
className?: string;
|
||||
recv?: string;
|
||||
sent?: string;
|
||||
verifierKey?: string;
|
||||
notaryKey?: string;
|
||||
info?: {
|
||||
meta: { notaryUrl: string; websocketProxyUrl: string };
|
||||
version: string;
|
||||
};
|
||||
}): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const { requestId } = useParams<{ requestId: string }>();
|
||||
const request = useRequestHistory(requestId);
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState('sent');
|
||||
const [isPopup, setIsPopup] = useState(isPopupWindow());
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
if (requestId) {
|
||||
dispatch(deleteRequestHistory(requestId));
|
||||
if (isPopup) window.close();
|
||||
navigate(-1);
|
||||
}
|
||||
}, [requestId]);
|
||||
|
||||
const notaryUrl = extractFromProps('notaryUrl', props, request);
|
||||
const websocketProxyUrl = extractFromProps(
|
||||
'websocketProxyUrl',
|
||||
props,
|
||||
request,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col w-full py-2 gap-2 flex-grow',
|
||||
props?.className,
|
||||
)}
|
||||
>
|
||||
<RemoveHistory
|
||||
onRemove={onDelete}
|
||||
showRemovalModal={showRemoveModal}
|
||||
setShowRemoveModal={setShowRemoveModal}
|
||||
onCancel={() => setShowRemoveModal(false)}
|
||||
/>
|
||||
<div className="flex flex-col px-2">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{!isPopup && (
|
||||
<Icon
|
||||
className={c(
|
||||
'px-1 select-none cursor-pointer',
|
||||
'text-slate-400 border-b-2 border-transparent hover:text-slate-500 active:text-slate-800',
|
||||
)}
|
||||
onClick={() => navigate(-1)}
|
||||
fa="fa-solid fa-xmark"
|
||||
/>
|
||||
)}
|
||||
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
|
||||
Sent
|
||||
</TabLabel>
|
||||
<TabLabel onClick={() => setTab('recv')} active={tab === 'recv'}>
|
||||
Recv
|
||||
</TabLabel>
|
||||
<TabLabel
|
||||
onClick={() => setTab('metadata')}
|
||||
active={tab === 'metadata'}
|
||||
>
|
||||
Metadata
|
||||
</TabLabel>
|
||||
<div className="flex flex-row flex-grow items-center justify-end">
|
||||
{!props?.recv && (
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => {
|
||||
if (!request) return;
|
||||
download(request.id, JSON.stringify(request.proof));
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="button !text-red-500"
|
||||
onClick={() => setShowRemoveModal(true)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow px-2">
|
||||
{tab === 'sent' && (
|
||||
<textarea
|
||||
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
|
||||
value={props?.sent || request?.verification?.sent}
|
||||
readOnly
|
||||
></textarea>
|
||||
)}
|
||||
{tab === 'recv' && (
|
||||
<textarea
|
||||
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
|
||||
value={props?.recv || request?.verification?.recv}
|
||||
readOnly
|
||||
></textarea>
|
||||
)}
|
||||
{tab === 'metadata' && (
|
||||
<div className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono">
|
||||
<MetadataRow
|
||||
label="Version"
|
||||
//@ts-ignore
|
||||
value={props?.info?.version || request?.proof?.version}
|
||||
/>
|
||||
<MetadataRow label="Notary URL" value={notaryUrl} />
|
||||
<MetadataRow
|
||||
label="Websocket Proxy URL"
|
||||
value={websocketProxyUrl}
|
||||
/>
|
||||
<MetadataRow
|
||||
label="Verifying Key"
|
||||
value={props?.verifierKey || request?.verification?.verifierKey}
|
||||
/>
|
||||
<MetadataRow
|
||||
label="Notary Key"
|
||||
value={props?.notaryKey || request?.verification?.notaryKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function extractFromProps(
|
||||
key: 'notaryUrl' | 'websocketProxyUrl',
|
||||
props?: {
|
||||
className?: string;
|
||||
recv?: string;
|
||||
sent?: string;
|
||||
verifierKey?: string;
|
||||
notaryKey?: string;
|
||||
info?: {
|
||||
meta: { notaryUrl: string; websocketProxyUrl: string };
|
||||
version: string;
|
||||
};
|
||||
},
|
||||
request?: RequestHistory,
|
||||
) {
|
||||
let value;
|
||||
|
||||
if (props?.info?.meta) {
|
||||
value = props.info.meta[key];
|
||||
} else if (request && (request?.proof as PresentationJSON)?.meta) {
|
||||
value = (request.proof as PresentationJSON).meta[key];
|
||||
} else {
|
||||
value = '';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function TabLabel(props: {
|
||||
children: ReactNode;
|
||||
onClick: MouseEventHandler;
|
||||
active?: boolean;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className={c('px-1 select-none cursor-pointer font-bold', {
|
||||
'text-slate-800 border-b-2 border-green-500': props.active,
|
||||
'text-slate-400 border-b-2 border-transparent hover:text-slate-500':
|
||||
!props.active,
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function MetadataRow({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div>{label}:</div>
|
||||
<div className="text-sm font-semibold whitespace-pre-wrap">
|
||||
{value || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,20 @@ import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import NavigateWithParams from '../../components/NavigateWithParams';
|
||||
import ResponseDetail from '../../components/ResponseDetail';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import { notarizeRequest } from '../../reducers/requests';
|
||||
import {
|
||||
getMaxRecv,
|
||||
getMaxSent,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
} from '../../utils/storage';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
formatForRequest,
|
||||
InputBody,
|
||||
FormBodyTable,
|
||||
parseResponse,
|
||||
} from '../../components/RequestBuilder';
|
||||
|
||||
enum TabType {
|
||||
Params = 'Params',
|
||||
@@ -25,25 +39,33 @@ export default function RequestBuilder(props?: {
|
||||
headers?: [string, string, boolean?][];
|
||||
body?: string;
|
||||
method?: string;
|
||||
response?: Response;
|
||||
}): ReactElement {
|
||||
const loc = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const subpath = props.subpath || '/custom';
|
||||
const [_url, setUrl] = useState(props.url || '');
|
||||
const subpath = props?.subpath || '/custom';
|
||||
const [_url, setUrl] = useState(props?.url || '');
|
||||
const [params, setParams] = useState<[string, string, boolean?][]>(
|
||||
props.params || [],
|
||||
props?.params || [],
|
||||
);
|
||||
const [body, setBody] = useState<string | undefined>(props?.body);
|
||||
const [formBody, setFormBody] = useState<[string, string, boolean?][]>([
|
||||
['', '', true],
|
||||
]);
|
||||
const [method, setMethod] = useState<string>(props?.method || 'GET');
|
||||
const [type, setType] = useState<string>('text/plain');
|
||||
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
|
||||
props.headers || [],
|
||||
);
|
||||
const [body, setBody] = useState<string | undefined>(props.body);
|
||||
const [method, setMethod] = useState<string>(props.method || 'GET');
|
||||
const [response, setResponse] = useState<Response | null>(
|
||||
props.response || null,
|
||||
props?.headers || [['Content-Type', type, true]],
|
||||
);
|
||||
|
||||
const [responseData, setResponseData] = useState<{
|
||||
json: any | null;
|
||||
text: string | null;
|
||||
img: string | null;
|
||||
headers: [string, string][] | null;
|
||||
} | null>(null);
|
||||
|
||||
const url = urlify(_url);
|
||||
|
||||
const href = !url
|
||||
@@ -57,6 +79,26 @@ export default function RequestBuilder(props?: {
|
||||
setParams(Array.from(url?.searchParams || []));
|
||||
}, [_url]);
|
||||
|
||||
useEffect(() => {
|
||||
updateContentType(type);
|
||||
}, [type, method]);
|
||||
|
||||
const updateContentType = useCallback(
|
||||
(type: string) => {
|
||||
const updateHeaders = headers.filter(
|
||||
([key]) => key.toLowerCase() !== 'content-type',
|
||||
);
|
||||
if (method === 'GET' || method === 'HEAD') {
|
||||
updateHeaders.push(['Content-Type', type, true]);
|
||||
} else {
|
||||
updateHeaders.push(['Content-Type', type, false]);
|
||||
}
|
||||
|
||||
setHeaders(updateHeaders);
|
||||
},
|
||||
[method, type, headers],
|
||||
);
|
||||
|
||||
const toggleParam = useCallback(
|
||||
(i: number) => {
|
||||
params[i][2] = !params[i][2];
|
||||
@@ -91,7 +133,7 @@ export default function RequestBuilder(props?: {
|
||||
|
||||
const sendRequest = useCallback(async () => {
|
||||
if (!href) return;
|
||||
|
||||
setResponseData(null);
|
||||
// eslint-disable-next-line no-undef
|
||||
const opts: RequestInit = {
|
||||
method,
|
||||
@@ -102,9 +144,13 @@ export default function RequestBuilder(props?: {
|
||||
return map;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
if (body) opts.body = body;
|
||||
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
if (type === 'application/x-www-form-urlencoded') {
|
||||
opts.body = formatForRequest(formBody, type);
|
||||
} else {
|
||||
opts.body = formatForRequest(body!, type);
|
||||
}
|
||||
}
|
||||
const cookie = headers.find(([key]) => key === 'Cookie');
|
||||
|
||||
if (cookie) {
|
||||
@@ -114,15 +160,64 @@ export default function RequestBuilder(props?: {
|
||||
|
||||
const res = await fetch(href, opts);
|
||||
|
||||
setResponse(res);
|
||||
const contentType =
|
||||
res.headers.get('content-type') || res.headers.get('Content-Type');
|
||||
|
||||
setResponseData(await parseResponse(contentType!, res));
|
||||
|
||||
navigate(subpath + '/response');
|
||||
}, [href, method, headers, body]);
|
||||
}, [href, method, headers, body, type]);
|
||||
|
||||
const onNotarize = useCallback(async () => {
|
||||
const maxSentData = await getMaxSent();
|
||||
const maxRecvData = await getMaxRecv();
|
||||
|
||||
const notaryUrl = await getNotaryApi();
|
||||
const websocketProxyUrl = await getProxyApi();
|
||||
|
||||
dispatch(
|
||||
notarizeRequest(
|
||||
//@ts-ignore
|
||||
{
|
||||
url: href || '',
|
||||
method,
|
||||
headers: headers.reduce((map: { [key: string]: string }, [k, v]) => {
|
||||
if (k !== 'Cookie') {
|
||||
map[k] = v;
|
||||
}
|
||||
return map;
|
||||
}, {}),
|
||||
body: body ? formatForRequest(body, type) : undefined,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
secretHeaders: [],
|
||||
secretResps: [],
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
navigate('/history');
|
||||
}, [href, method, headers, body, type]);
|
||||
|
||||
const onMethod = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === 'GET' || value === 'HEAD') {
|
||||
setType('');
|
||||
setMethod(value);
|
||||
} else {
|
||||
setMethod(value);
|
||||
}
|
||||
},
|
||||
[method, type],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
|
||||
<div className="flex flex-row px-2">
|
||||
<select className="select" onChange={(e) => setMethod(e.target.value)}>
|
||||
<select className="select" onChange={(e) => onMethod(e)}>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
@@ -134,8 +229,14 @@ export default function RequestBuilder(props?: {
|
||||
<input
|
||||
className="input border flex-grow"
|
||||
type="text"
|
||||
value={url ? href : _url}
|
||||
value={_url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onBlur={() => {
|
||||
const formattedUrl = urlify(_url);
|
||||
if (formattedUrl) {
|
||||
setUrl(formattedUrl.href);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button className="button" disabled={!url} onClick={sendRequest}>
|
||||
Send
|
||||
@@ -161,13 +262,19 @@ export default function RequestBuilder(props?: {
|
||||
>
|
||||
Body
|
||||
</TabLabel>
|
||||
{response && (
|
||||
<TabLabel
|
||||
onClick={() => navigate(subpath + '/response')}
|
||||
active={loc.pathname.includes('response')}
|
||||
>
|
||||
Response
|
||||
</TabLabel>
|
||||
{responseData && (
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<TabLabel
|
||||
onClick={() => navigate(subpath + '/response')}
|
||||
active={loc.pathname.includes('response')}
|
||||
>
|
||||
Response
|
||||
</TabLabel>
|
||||
|
||||
<button className="button" onClick={onNotarize}>
|
||||
Notarize
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,16 +304,38 @@ export default function RequestBuilder(props?: {
|
||||
<Route
|
||||
path="body"
|
||||
element={
|
||||
<textarea
|
||||
className="textarea h-full w-full resize-none"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
<div className="h-full">
|
||||
<select
|
||||
className={c('select', {
|
||||
'w-[80px]':
|
||||
type === 'application/json' ||
|
||||
type === 'text/plain' ||
|
||||
type === '',
|
||||
'w-[200px]': type === 'application/x-www-form-urlencoded',
|
||||
})}
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
>
|
||||
<option value="text/plain">Text</option>
|
||||
<option value="application/json">JSON</option>
|
||||
<option value="application/x-www-form-urlencoded">
|
||||
x-www-form-urlencoded
|
||||
</option>
|
||||
</select>
|
||||
{type === 'application/x-www-form-urlencoded' ? (
|
||||
<FormBodyTable
|
||||
formBody={formBody}
|
||||
setFormBody={setFormBody}
|
||||
/>
|
||||
) : (
|
||||
<InputBody body={body!} setBody={setBody} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="response"
|
||||
element={<ResponseDetail response={response} />}
|
||||
element={<ResponseDetail responseData={responseData} />}
|
||||
/>
|
||||
<Route path="/" element={<NavigateWithParams to="/params" />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import RequestDetail from '../../components/RequestDetail';
|
||||
import { useParams } from 'react-router';
|
||||
import { useRequest } from '../../reducers/requests';
|
||||
|
||||
export default function Request(): ReactElement {
|
||||
const params = useParams<{ requestId: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RequestDetail requestId={params.requestId} />
|
||||
</>
|
||||
<>{!!params.requestId && <RequestDetail requestId={params.requestId} />}</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { ReactElement } from 'react';
|
||||
import RequestTable from '../../components/RequestTable';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
|
||||
export default function Requests(): ReactElement {
|
||||
export default function Requests(props: { shouldFix?: boolean }): ReactElement {
|
||||
const requests = useRequests();
|
||||
return (
|
||||
<>
|
||||
<RequestTable requests={requests} />
|
||||
<RequestTable shouldFix={props.shouldFix} requests={requests} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
143
src/pages/RunPluginApproval/index.tsx
Normal file
143
src/pages/RunPluginApproval/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { type PluginConfig, PluginMetadata, urlify } from '../../utils/misc';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import { BaseApproval } from '../BaseApproval';
|
||||
import { PluginPermissions } from '../../components/PluginInfo';
|
||||
import {
|
||||
getPluginConfigByHash,
|
||||
getPluginMetadataByHash,
|
||||
} from '../../entries/Background/db';
|
||||
import { runPlugin } from '../../utils/rpc';
|
||||
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
|
||||
import { deferredPromise } from '../../utils/promise';
|
||||
|
||||
export function RunPluginApproval(): ReactElement {
|
||||
const [params] = useSearchParams();
|
||||
const origin = params.get('origin');
|
||||
const favIconUrl = params.get('favIconUrl');
|
||||
const hash = params.get('hash');
|
||||
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 });
|
||||
|
||||
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: {
|
||||
pluginHash: hash,
|
||||
pluginParams: pluginParams ? JSON.parse(pluginParams) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.run_plugin_response,
|
||||
data: true,
|
||||
});
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!hash) return;
|
||||
try {
|
||||
const config = await getPluginConfigByHash(hash);
|
||||
const metadata = await getPluginMetadataByHash(hash);
|
||||
setPluginContent(config);
|
||||
setPluginMetadata(metadata);
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
}
|
||||
})();
|
||||
}, [hash]);
|
||||
|
||||
return (
|
||||
<BaseApproval
|
||||
header={`Execute Plugin`}
|
||||
onSecondaryClick={onCancel}
|
||||
onPrimaryClick={onAccept}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 py-8">
|
||||
{!!favIconUrl ? (
|
||||
<img
|
||||
src={favIconUrl}
|
||||
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
|
||||
alt="logo"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
fa="fa-solid fa-globe"
|
||||
size={4}
|
||||
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
|
||||
/>
|
||||
)}
|
||||
<div className="text-2xl text-center px-8">
|
||||
<b className="text-blue-500">{hostname}</b> wants to execute a plugin:
|
||||
</div>
|
||||
</div>
|
||||
{!pluginContent && (
|
||||
<div className="flex flex-col items-center flex-grow gap-4 border border-slate-300 p-8 mx-8 rounded bg-slate-100">
|
||||
<Icon
|
||||
className="animate-spin w-fit text-slate-500"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{pluginContent && (
|
||||
<div className="flex flex-col gap-4 border border-slate-300 p-4 mx-8 rounded bg-slate-100">
|
||||
<div className="flex flex-col items-center">
|
||||
<img
|
||||
className="w-12 h-12 mb-2"
|
||||
src={pluginContent.icon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-2xl text-blue-600 font-semibold">
|
||||
{pluginContent.title}
|
||||
</span>
|
||||
<div className="text-slate-500 text-base">
|
||||
{pluginContent.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</BaseApproval>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
RequestHistory,
|
||||
} from '../pages/Background/actionTypes';
|
||||
RequestProgress,
|
||||
} from '../entries/Background/rpc';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from './index';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
enum ActionType {
|
||||
'/history/addRequest' = '/history/addRequest',
|
||||
'/history/setRequests' = '/history/setRequests',
|
||||
'/history/deleteRequest' = '/history/deleteRequest',
|
||||
'/history/addRequestCid' = '/history/addRequestCid',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
@@ -30,13 +33,27 @@ const initialState: State = {
|
||||
order: [],
|
||||
};
|
||||
|
||||
export const addRequestHistory = (request: RequestHistory) => {
|
||||
export const addRequestHistory = (request?: RequestHistory | null) => {
|
||||
return {
|
||||
type: ActionType['/history/addRequest'],
|
||||
payload: request,
|
||||
};
|
||||
};
|
||||
|
||||
export const setRequests = (requests: RequestHistory[]) => {
|
||||
return {
|
||||
type: ActionType['/history/setRequests'],
|
||||
payload: requests,
|
||||
};
|
||||
};
|
||||
|
||||
export const addRequestCid = (requestId: string, cid: string) => {
|
||||
return {
|
||||
type: ActionType['/history/addRequestCid'],
|
||||
payload: { requestId, cid },
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteRequestHistory = (id: string) => {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.delete_prove_request,
|
||||
@@ -56,7 +73,13 @@ export default function history(
|
||||
switch (action.type) {
|
||||
case ActionType['/history/addRequest']: {
|
||||
const payload: RequestHistory = action.payload;
|
||||
|
||||
if (!payload) return state;
|
||||
|
||||
const existing = state.map[payload.id];
|
||||
if (existing?.progress === RequestProgress.Error) {
|
||||
return state;
|
||||
}
|
||||
const newMap = {
|
||||
...state.map,
|
||||
[payload.id]: payload,
|
||||
@@ -69,6 +92,25 @@ export default function history(
|
||||
order: newOrder,
|
||||
};
|
||||
}
|
||||
case ActionType['/history/setRequests']: {
|
||||
const payload: RequestHistory[] = action.payload;
|
||||
const newMap = payload.reduce(
|
||||
(map: { [id: string]: RequestHistory }, req) => {
|
||||
if (state.map[req.id]?.progress === RequestProgress.Error) {
|
||||
map[req.id] = state.map[req.id];
|
||||
} else {
|
||||
map[req.id] = req;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
map: newMap,
|
||||
order: payload.map(({ id }) => id),
|
||||
};
|
||||
}
|
||||
case ActionType['/history/deleteRequest']: {
|
||||
const reqId: string = action.payload;
|
||||
const newMap = { ...state.map };
|
||||
@@ -80,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;
|
||||
}
|
||||
@@ -91,8 +147,15 @@ export const useHistoryOrder = (): string[] => {
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useRequestHistory = (id: string): RequestHistory | undefined => {
|
||||
export const useAllProofHistory = (): RequestHistory[] => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.history.order.map((id) => state.history.map[id]);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useRequestHistory = (id?: string): RequestHistory | undefined => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!id) return undefined;
|
||||
return state.history.map[id];
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user