mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-09 21:18:02 -05:00
Compare commits
24 Commits
requests-f
...
tlsn-banke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
336f30b56c | ||
|
|
31a531fd07 | ||
|
|
4ad394419b | ||
|
|
25f35d0051 | ||
|
|
5ccdd9b06a | ||
|
|
186f77d3cb | ||
|
|
08c4f74479 | ||
|
|
737cc10af7 | ||
|
|
d1cbc34126 | ||
|
|
30a7b7b36b | ||
|
|
4c78625f12 | ||
|
|
d15d021b4a | ||
|
|
be27560631 | ||
|
|
a018acb7bf | ||
|
|
d9dacdfb14 | ||
|
|
998c9c091e | ||
|
|
c04556620c | ||
|
|
4b473273dc | ||
|
|
585a8f2d3d | ||
|
|
1c29fee920 | ||
|
|
884e55dccf | ||
|
|
87e96c0f50 | ||
|
|
3b8cd0fba3 | ||
|
|
c41b4ff401 |
@@ -42,7 +42,7 @@ at your option.
|
||||
```
|
||||
$ git clone https://github.com/novnc/websockify && cd websockify
|
||||
$ ./docker/build.sh
|
||||
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.twitter.com:443
|
||||
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.x.com:443
|
||||
```
|
||||
|
||||
## Running Websockify Docker Image
|
||||
|
||||
540
package-lock.json
generated
540
package-lock.json
generated
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.1.0.5",
|
||||
"version": "0.1.0.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.1.0.5",
|
||||
"version": "0.1.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@extism/extism": "^1.0.2",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"async-mutex": "^0.4.0",
|
||||
"axios": "^1.7.7",
|
||||
"buffer": "^6.0.3",
|
||||
"charwise": "^3.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
@@ -20,6 +22,7 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"level": "^8.0.0",
|
||||
"minimatch": "^9.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -30,7 +33,8 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.5.3"
|
||||
"tlsn-js": "0.1.0-alpha.6.2",
|
||||
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
@@ -3174,6 +3178,16 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -3189,6 +3203,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
@@ -3238,6 +3264,28 @@
|
||||
"node": ">=10.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -3503,6 +3551,16 @@
|
||||
"integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/axios": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
||||
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
||||
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
@@ -4789,6 +4847,12 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
@@ -4850,6 +4914,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||
@@ -5161,13 +5236,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -5516,6 +5589,18 @@
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz",
|
||||
@@ -6188,6 +6273,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -6824,6 +6918,16 @@
|
||||
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
@@ -6845,6 +6949,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jest": {
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz",
|
||||
@@ -6899,6 +7015,28 @@
|
||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
|
||||
@@ -6973,6 +7111,16 @@
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -6985,6 +7133,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
||||
"version": "2.0.0-next.5",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||
@@ -7058,6 +7218,16 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -7140,6 +7310,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -7574,7 +7756,6 @@
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -7625,6 +7806,20 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -7833,6 +8028,28 @@
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -9434,7 +9651,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -9443,7 +9659,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -9467,15 +9682,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -11287,6 +11504,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
@@ -12637,14 +12860,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/sucrase/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sucrase/node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@@ -12674,20 +12889,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sucrase/node_modules/minimatch": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@@ -12862,9 +13063,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tlsn-js": {
|
||||
"version": "0.1.0-alpha.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.3.tgz",
|
||||
"integrity": "sha512-eTSCQ6MaH8mN2oCfLsQDe/mdfTlIq74MbKVesV6M01C2SRE0FJcVlTWMsnT3L+wOpvOlvsiBeCWV7T9m/YMzew==",
|
||||
"version": "0.1.0-alpha.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.6.2.tgz",
|
||||
"integrity": "sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw==",
|
||||
"engines": {
|
||||
"node": ">= 16.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tlsn-js-v5": {
|
||||
"name": "tlsn-js",
|
||||
"version": "0.1.0-alpha.5.4",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.4.tgz",
|
||||
"integrity": "sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.1"
|
||||
},
|
||||
@@ -15920,6 +16131,16 @@
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -15929,6 +16150,15 @@
|
||||
"type-fest": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
@@ -15962,6 +16192,27 @@
|
||||
"@humanwhocodes/object-schema": "^2.0.2",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/module-importer": {
|
||||
@@ -16142,6 +16393,14 @@
|
||||
"integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/axios": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
||||
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
||||
"requires": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
@@ -17191,6 +17450,11 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
@@ -17220,6 +17484,16 @@
|
||||
"integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||
@@ -17461,13 +17735,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
@@ -17706,6 +17978,14 @@
|
||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"comlink": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz",
|
||||
@@ -18166,6 +18446,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -18575,6 +18860,16 @@
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -18627,6 +18922,15 @@
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -18750,6 +19054,16 @@
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
@@ -18767,6 +19081,15 @@
|
||||
"requires": {
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18801,6 +19124,27 @@
|
||||
"minimatch": "^3.1.2",
|
||||
"object.entries": "^1.1.7",
|
||||
"object.fromentries": "^2.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
@@ -18839,6 +19183,16 @@
|
||||
"string.prototype.matchall": "^4.0.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -18848,6 +19202,15 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "2.0.0-next.5",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||
@@ -19217,8 +19580,7 @@
|
||||
"follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
|
||||
},
|
||||
"for-each": {
|
||||
"version": "0.3.3",
|
||||
@@ -19245,6 +19607,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -19368,6 +19740,27 @@
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
@@ -20492,14 +20885,12 @@
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
@@ -20517,12 +20908,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
@@ -21591,6 +21981,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
@@ -22556,14 +22951,6 @@
|
||||
"ts-interface-checker": "^0.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@@ -22580,14 +22967,6 @@
|
||||
"minipass": "^7.1.2",
|
||||
"path-scurry": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -22712,9 +23091,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"tlsn-js": {
|
||||
"version": "0.1.0-alpha.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.3.tgz",
|
||||
"integrity": "sha512-eTSCQ6MaH8mN2oCfLsQDe/mdfTlIq74MbKVesV6M01C2SRE0FJcVlTWMsnT3L+wOpvOlvsiBeCWV7T9m/YMzew==",
|
||||
"version": "0.1.0-alpha.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.6.2.tgz",
|
||||
"integrity": "sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw=="
|
||||
},
|
||||
"tlsn-js-v5": {
|
||||
"version": "npm:tlsn-js@0.1.0-alpha.5.4",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.5.4.tgz",
|
||||
"integrity": "sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==",
|
||||
"requires": {
|
||||
"comlink": "^4.4.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.1.0.5",
|
||||
"version": "0.1.0.6",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -18,7 +18,9 @@
|
||||
"dependencies": {
|
||||
"@extism/extism": "^1.0.2",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"async-mutex": "^0.4.0",
|
||||
"axios": "^1.7.7",
|
||||
"buffer": "^6.0.3",
|
||||
"charwise": "^3.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
@@ -27,6 +29,7 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"level": "^8.0.0",
|
||||
"minimatch": "^9.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -37,7 +40,8 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.5.3"
|
||||
"tlsn-js": "0.1.0-alpha.6.2",
|
||||
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Plugin Development for the TLSNotary Browser Extension
|
||||
|
||||
This folder is dedicated to the development of plugins for the TLSNotary browser extension, utilizing the Extism framework. Currently, the folder includes a TypeScript-based plugin example, `twitter_profile`, with plans to add more plugins showcasing different programming languages and functionalities.
|
||||
|
||||
## Installation of Extism-js
|
||||
|
||||
1. **Download and Install Extism-js**: Begin by setting up `extism-js`, which enables you to compile and manage your plugins. Run these commands to download and install it:
|
||||
|
||||
```sh
|
||||
curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh
|
||||
sh install.sh
|
||||
```
|
||||
|
||||
This script installs the Extism JavaScript Plugin Development Kit from its GitHub repository, preparing your environment for plugin compilation.
|
||||
|
||||
## Building the Twitter Profile Plugin
|
||||
|
||||
Navigate to the `twitter_profile` directory within this folder and run the following command to build the plugin:
|
||||
|
||||
```sh
|
||||
extism-js index.js -i index.d.ts -o index.wasm
|
||||
```
|
||||
This command compiles the TypeScript code in index.js into a WebAssembly module, ready for integration with the TLSNotary extension.
|
||||
|
||||
### Running the Twitter Plugin Example:
|
||||
|
||||
1. Build the `twitter_profile` plugin as explained above.
|
||||
2. Build and install the `tlsn-extension` as documented in the [main README.md](../README.md).
|
||||
3. [Run a local notary server](https://github.com/tlsnotary/tlsn/blob/main/notary-server/README.md), ensuring `TLS` is disabled in the [config file](https://github.com/tlsnotary/tlsn/blob/main/notary-server/config/config.yaml#L18).
|
||||
4. Install the plugin: Click the **Add a Plugin (+)** button and select the `index.wasm` file you built in step 1. A **Twitter Profile** button should then appear below the default buttons.
|
||||
5. Click the **Twitter Profile** button. This action opens the Twitter webpage along with a TLSNotary sidebar.
|
||||
6. Follow the steps in the TLSNotary sidebar.
|
||||
7. Access the TLSNotary results by clicking the **History** button in the TLSNotary extension.
|
||||
|
||||
## Future Plugins
|
||||
|
||||
This directory will be expanded with more plugins designed to demonstrate the functionality of the TLSNotary extension. Plugins enable flexible use of the TLSNotary across a broad range of applications. The use of Extism facilitates plugin development in various languages, further enhancing flexibility.
|
||||
|
||||
## Create an icon
|
||||
|
||||
1. resize to 320x320 pixels:
|
||||
```sh
|
||||
convert icon.png -resize 320x320! icon_320.png
|
||||
```
|
||||
2. convert to base64
|
||||
```sh
|
||||
base64 -i icon_320.png -o icon_320.txt
|
||||
```
|
||||
4
plugins/hello/hello.d.ts
vendored
4
plugins/hello/hello.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
declare module 'main' {
|
||||
// Extism exports take no params and return an I32
|
||||
export function hello(): I32;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
function hello() {
|
||||
const name = Host.inputString();
|
||||
Host.outputString(`Hello, ${name}`);
|
||||
}
|
||||
|
||||
module.exports = { hello };
|
||||
Binary file not shown.
14
plugins/reddit/index.d.ts
vendored
14
plugins/reddit/index.d.ts
vendored
@@ -1,14 +0,0 @@
|
||||
declare module 'main' {
|
||||
// Extism exports take no params and return an I32
|
||||
export function start(): I32;
|
||||
export function two(): I32;
|
||||
export function three(): I32;
|
||||
export function config(): I32;
|
||||
}
|
||||
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
redirect(ptr: I64): void;
|
||||
notarize(ptr: I64): I64;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
14
plugins/twitter_profile/index.d.ts
vendored
14
plugins/twitter_profile/index.d.ts
vendored
@@ -1,14 +0,0 @@
|
||||
declare module 'main' {
|
||||
// Extism exports take no params and return an I32
|
||||
export function start(): I32;
|
||||
export function two(): I32;
|
||||
export function three(): I32;
|
||||
export function config(): I32;
|
||||
}
|
||||
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
redirect(ptr: I64): void;
|
||||
notarize(ptr: I64): I64;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ dependencies:
|
||||
level:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.1
|
||||
minimatch:
|
||||
specifier: ^9.0.4
|
||||
version: 9.0.4
|
||||
node-cache:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
@@ -69,8 +72,11 @@ dependencies:
|
||||
specifier: ^3.3.3
|
||||
version: 3.4.3
|
||||
tlsn-js:
|
||||
specifier: 0.1.0-alpha.5.3
|
||||
version: 0.1.0-alpha.5.3
|
||||
specifier: 0.1.0-alpha.6.2
|
||||
version: 0.1.0-alpha.6.2
|
||||
tlsn-js-v5:
|
||||
specifier: npm:tlsn-js@0.1.0-alpha.5.4
|
||||
version: /tlsn-js@0.1.0-alpha.5.4
|
||||
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
@@ -7968,13 +7974,18 @@ packages:
|
||||
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
|
||||
dev: true
|
||||
|
||||
/tlsn-js@0.1.0-alpha.5.3:
|
||||
resolution: {integrity: sha512-eTSCQ6MaH8mN2oCfLsQDe/mdfTlIq74MbKVesV6M01C2SRE0FJcVlTWMsnT3L+wOpvOlvsiBeCWV7T9m/YMzew==}
|
||||
/tlsn-js@0.1.0-alpha.5.4:
|
||||
resolution: {integrity: sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==}
|
||||
engines: {node: '>= 16.20.2'}
|
||||
dependencies:
|
||||
comlink: 4.4.1
|
||||
dev: false
|
||||
|
||||
/tlsn-js@0.1.0-alpha.6.2:
|
||||
resolution: {integrity: sha512-6PANWQEFw48VFNfDgA017zcNRae2OutzNmE5Xcc/h6lwkZZ8MBZIFAFfz/WHZJ3fcFJumaKrG80gpomP6blZqw==}
|
||||
engines: {node: '>= 16.20.2'}
|
||||
dev: false
|
||||
|
||||
/tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
BIN
src/assets/plugins/discord_dm.wasm
Normal file
BIN
src/assets/plugins/discord_dm.wasm
Normal file
Binary file not shown.
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;
|
||||
@@ -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 && (
|
||||
|
||||
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;
|
||||
}
|
||||
208
src/components/PluginInfo/index.tsx
Normal file
208
src/components/PluginInfo/index.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
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(): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
|
||||
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
|
||||
|
||||
const onAddPlugin = useCallback(
|
||||
async (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
try {
|
||||
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
|
||||
setPluginContent(null);
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
}
|
||||
},
|
||||
[pluginContent, pluginBuffer],
|
||||
);
|
||||
|
||||
const onPluginInfo = useCallback(
|
||||
async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!evt.target.files) return;
|
||||
try {
|
||||
const [file] = evt.target.files;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const plugin = await makePlugin(arrayBuffer);
|
||||
setPluginContent(await getPluginConfig(plugin));
|
||||
setPluginBuffer(arrayBuffer);
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
} finally {
|
||||
evt.target.value = '';
|
||||
}
|
||||
},
|
||||
[setPluginContent, setPluginBuffer],
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setPluginContent(null);
|
||||
setPluginBuffer(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
|
||||
type="file"
|
||||
onChange={onPluginInfo}
|
||||
/>
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
{pluginContent && (
|
||||
<PluginInfoModal
|
||||
pluginContent={pluginContent}
|
||||
onClose={onClose}
|
||||
onAddPlugin={onAddPlugin}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginInfoModalHeader(props: {
|
||||
className?: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
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.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>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transition: 200ms opacity;
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -20,4 +21,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
@@ -20,6 +19,12 @@ 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';
|
||||
|
||||
export function PluginList(props: { className?: string }): ReactElement {
|
||||
const hashes = usePluginHashes();
|
||||
@@ -29,7 +34,9 @@ export function PluginList(props: { className?: string }): ReactElement {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col flex-nowrap', props.className)}>
|
||||
<div
|
||||
className={classNames('flex flex-col flex-nowrap gap-1', props.className)}
|
||||
>
|
||||
{!hashes.length && (
|
||||
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
|
||||
<div>No available plugins</div>
|
||||
@@ -48,9 +55,11 @@ export function Plugin(props: {
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
const [pluginInfo, showPluginInfo] = useState(false);
|
||||
const [remove, showRemove] = useState(false);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (!config) return;
|
||||
if (!config || remove) return;
|
||||
|
||||
try {
|
||||
await runPlugin(props.hash, 'start');
|
||||
@@ -69,11 +78,11 @@ export function Plugin(props: {
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [props.hash, config]);
|
||||
}, [props.hash, config, remove]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setConfig(await fetchPluginConfigByHash(props.hash));
|
||||
setConfig(await getPluginConfigByHash(props.hash));
|
||||
})();
|
||||
}, [props.hash]);
|
||||
|
||||
@@ -81,33 +90,128 @@ export function Plugin(props: {
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
removePlugin(props.hash);
|
||||
showRemove(false);
|
||||
},
|
||||
[props.hash],
|
||||
[props.hash, remove],
|
||||
);
|
||||
|
||||
const onConfirmRemove: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showRemove(true);
|
||||
},
|
||||
[props.hash, remove],
|
||||
);
|
||||
|
||||
const onPluginInfo: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showPluginInfo(true);
|
||||
},
|
||||
[props.hash, pluginInfo],
|
||||
);
|
||||
|
||||
if (!config) return <></>;
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row border rounded border-slate-300 p-2 gap-2 plugin-box',
|
||||
'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',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
<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}
|
||||
<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={onRemove}
|
||||
/>
|
||||
{!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}
|
||||
/>
|
||||
<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>
|
||||
<div>{config.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -4,14 +4,6 @@ let RequestsLogs: {
|
||||
[tabId: string]: NodeCache;
|
||||
} = {};
|
||||
|
||||
let HeadersStore: {
|
||||
[hostname: string]: NodeCache;
|
||||
} = {};
|
||||
|
||||
let CookieStore: {
|
||||
[hostname: string]: NodeCache;
|
||||
} = {};
|
||||
|
||||
export const deleteCacheByTabId = (tabId: number) => {
|
||||
delete RequestsLogs[tabId];
|
||||
};
|
||||
@@ -27,50 +19,10 @@ export const getCacheByTabId = (tabId: number): NodeCache => {
|
||||
return RequestsLogs[tabId];
|
||||
};
|
||||
|
||||
export const deleteHeadersByHost = (hostname: string) => {
|
||||
delete HeadersStore[hostname];
|
||||
};
|
||||
|
||||
export const getHeaderStoreByHost = (hostname: string): NodeCache => {
|
||||
HeadersStore[hostname] =
|
||||
HeadersStore[hostname] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
|
||||
return HeadersStore[hostname];
|
||||
};
|
||||
|
||||
export const deleteCookiesByHost = (hostname: string) => {
|
||||
delete CookieStore[hostname];
|
||||
};
|
||||
|
||||
export const getCookieStoreByHost = (hostname: string): NodeCache => {
|
||||
CookieStore[hostname] =
|
||||
CookieStore[hostname] ||
|
||||
new NodeCache({
|
||||
stdTTL: 60 * 5, // default 5m TTL
|
||||
maxKeys: 1000000,
|
||||
});
|
||||
|
||||
return CookieStore[hostname];
|
||||
};
|
||||
|
||||
export const clearRequestCache = () => {
|
||||
RequestsLogs = {};
|
||||
};
|
||||
|
||||
export const clearHeaderCache = () => {
|
||||
HeadersStore = {};
|
||||
};
|
||||
|
||||
export const clearCookieCache = () => {
|
||||
CookieStore = {};
|
||||
};
|
||||
|
||||
export const clearCache = () => {
|
||||
clearRequestCache();
|
||||
clearHeaderCache();
|
||||
clearCookieCache();
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Level } from 'level';
|
||||
import type { RequestHistory } from './rpc';
|
||||
import { PluginConfig, sha256 } from '../../utils/misc';
|
||||
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
|
||||
import mutex from './mutex';
|
||||
const charwise = require('charwise');
|
||||
|
||||
const db = new Level('./ext-db', {
|
||||
export const db = new Level('./ext-db', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const historyDb = db.sublevel<string, RequestHistory>('history', {
|
||||
@@ -15,6 +16,24 @@ const pluginDb = db.sublevel<string, string>('plugin', {
|
||||
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 appDb = db.sublevel<string, any>('app', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
enum AppDatabaseKey {
|
||||
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
|
||||
}
|
||||
|
||||
export async function addNotaryRequest(
|
||||
now = Date.now(),
|
||||
@@ -127,14 +146,12 @@ export async function getNotaryRequests(): Promise<RequestHistory[]> {
|
||||
export async function getNotaryRequest(
|
||||
id: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
return historyDb.get(id);
|
||||
return historyDb.get(id).catch(() => null);
|
||||
}
|
||||
|
||||
export async function getPluginHashes(): Promise<string[]> {
|
||||
const retVal: string[] = [];
|
||||
for await (const [key] of pluginDb.iterator()) {
|
||||
// pluginDb.del(key);
|
||||
// pluginConfigDb.del(key);
|
||||
retVal.push(key);
|
||||
}
|
||||
return retVal;
|
||||
@@ -205,6 +222,59 @@ export async function removePluginConfig(
|
||||
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 || {
|
||||
filePath: '',
|
||||
origin: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -222,3 +292,105 @@ export async function setNotaryRequestCid(
|
||||
|
||||
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(host: string, name: string) {
|
||||
try {
|
||||
const existing = await cookiesDb.sublevel(host).get(name);
|
||||
return existing;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCookiesByHost(host: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
for await (const [key, value] of cookiesDb.sublevel(host).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(host: string, name: string, value?: string) {
|
||||
if (!value) return null;
|
||||
return mutex.runExclusive(async () => {
|
||||
await headersDb.sublevel(host).put(name, value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearHeaders(host: string) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await headersDb.sublevel(host).clear();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHeaders(host: string, name: string) {
|
||||
try {
|
||||
const existing = await headersDb.sublevel(host).get(name);
|
||||
return existing;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHeadersByHost(host: string) {
|
||||
const ret: { [key: string]: string } = {};
|
||||
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
|
||||
ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function getDefaultPluginsInstalled(): Promise<boolean> {
|
||||
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
|
||||
}
|
||||
|
||||
export async function setDefaultPluginsInstalled(installed = false) {
|
||||
return mutex.runExclusive(async () => {
|
||||
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAppState() {
|
||||
return {
|
||||
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
getCacheByTabId,
|
||||
getCookieStoreByHost,
|
||||
getHeaderStoreByHost,
|
||||
} from './cache';
|
||||
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 { setCookies, setHeaders } from './db';
|
||||
|
||||
export const onSendHeaders = (
|
||||
details: browser.WebRequest.OnSendHeadersDetailsType,
|
||||
@@ -21,20 +18,17 @@ export const onSendHeaders = (
|
||||
const { hostname } = urlify(details.url) || {};
|
||||
|
||||
if (hostname && details.requestHeaders) {
|
||||
const headerStore = getHeaderStoreByHost(hostname);
|
||||
|
||||
details.requestHeaders.forEach((header) => {
|
||||
const { name, value } = header;
|
||||
if (/^cookie$/i.test(name) && value) {
|
||||
const cookieStore = getCookieStoreByHost(hostname);
|
||||
value
|
||||
.split(';')
|
||||
.map((v) => v.split('='))
|
||||
.forEach((cookie) => {
|
||||
cookieStore.set(cookie[0].trim(), cookie[1]);
|
||||
setCookies(hostname, cookie[0].trim(), cookie[1]);
|
||||
});
|
||||
} else {
|
||||
headerStore.set(name, value);
|
||||
setHeaders(hostname, name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
|
||||
import { deleteCacheByTabId } from './cache';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { getAppState, setDefaultPluginsInstalled } from './db';
|
||||
import { installPlugin } from './plugins/utils';
|
||||
|
||||
(async () => {
|
||||
browser.webRequest.onSendHeaders.addListener(
|
||||
@@ -31,6 +33,19 @@ import browser from 'webextension-polyfill';
|
||||
deleteCacheByTabId(tabId);
|
||||
});
|
||||
|
||||
const { defaultPluginsInstalled } = await getAppState();
|
||||
|
||||
if (!defaultPluginsInstalled) {
|
||||
try {
|
||||
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
|
||||
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
|
||||
await installPlugin(twitterProfileUrl);
|
||||
await installPlugin(discordDmUrl);
|
||||
} finally {
|
||||
await setDefaultPluginsInstalled(true);
|
||||
}
|
||||
}
|
||||
|
||||
const { initRPC } = await import('./rpc');
|
||||
await createOffscreenDocument();
|
||||
initRPC();
|
||||
|
||||
29
src/entries/Background/plugins/utils.ts
Normal file
29
src/entries/Background/plugins/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
|
||||
import { getPluginConfig } from '../../../utils/misc';
|
||||
|
||||
export async function installPlugin(
|
||||
urlOrBuffer: ArrayBuffer | string,
|
||||
origin = '',
|
||||
filePath = '',
|
||||
metadata: {[key: string]: string} = {},
|
||||
) {
|
||||
let arrayBuffer;
|
||||
|
||||
if (typeof urlOrBuffer === 'string') {
|
||||
const resp = await fetch(urlOrBuffer);
|
||||
arrayBuffer = await resp.arrayBuffer();
|
||||
} else {
|
||||
arrayBuffer = urlOrBuffer;
|
||||
}
|
||||
|
||||
const config = await getPluginConfig(arrayBuffer);
|
||||
const hex = Buffer.from(arrayBuffer).toString('hex');
|
||||
const hash = await addPlugin(hex);
|
||||
await addPluginConfig(hash!, config);
|
||||
await addPluginMetadata(hash!, {
|
||||
...metadata,
|
||||
origin,
|
||||
filePath,
|
||||
});
|
||||
return hash;
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import {
|
||||
clearCache,
|
||||
getCacheByTabId,
|
||||
getCookieStoreByHost,
|
||||
getHeaderStoreByHost,
|
||||
} from './cache';
|
||||
import { clearCache, getCacheByTabId } from './cache';
|
||||
import { addRequestHistory } from '../../reducers/history';
|
||||
import {
|
||||
addNotaryRequest,
|
||||
@@ -22,14 +17,23 @@ import {
|
||||
addPluginConfig,
|
||||
getPluginConfigByHash,
|
||||
removePluginConfig,
|
||||
getConnection,
|
||||
setConnection,
|
||||
deleteConnection,
|
||||
addPluginMetadata,
|
||||
getPlugins,
|
||||
getCookiesByHost,
|
||||
getHeadersByHost,
|
||||
getAppState,
|
||||
setDefaultPluginsInstalled,
|
||||
} from './db';
|
||||
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
|
||||
import {
|
||||
devlog,
|
||||
extractBodyFromResponse,
|
||||
getPluginConfig,
|
||||
hexToArrayBuffer,
|
||||
makePlugin,
|
||||
PluginConfig,
|
||||
} from '../../utils/misc';
|
||||
import {
|
||||
getLoggingFilter,
|
||||
@@ -38,6 +42,10 @@ import {
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
} from '../../utils/storage';
|
||||
import { deferredPromise } from '../../utils/promise';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { OffscreenActionTypes } from '../Offscreen/types';
|
||||
import { SidePanelActionTypes } from '../SidePanel/types';
|
||||
|
||||
const charwise = require('charwise');
|
||||
|
||||
@@ -64,6 +72,23 @@ export enum BackgroundActiontype {
|
||||
get_plugin_hashes = 'get_plugin_hashes',
|
||||
open_popup = 'open_popup',
|
||||
change_route = 'change_route',
|
||||
connect_request = 'connect_request',
|
||||
connect_response = 'connect_response',
|
||||
get_history_request = 'get_history_request',
|
||||
get_history_response = 'get_history_response',
|
||||
get_proof_request = 'get_proof_request',
|
||||
get_proof_response = 'get_proof_response',
|
||||
notarize_request = 'notarize_request',
|
||||
notarize_response = 'notarize_response',
|
||||
install_plugin_request = 'install_plugin_request',
|
||||
install_plugin_response = 'install_plugin_response',
|
||||
get_plugins_request = 'get_plugins_request',
|
||||
get_plugins_response = 'get_plugins_response',
|
||||
run_plugin_request = 'run_plugin_request',
|
||||
run_plugin_response = 'run_plugin_response',
|
||||
get_logging_level = 'get_logging_level',
|
||||
get_app_state = 'get_app_state',
|
||||
set_default_plugins_installed = 'set_default_plugins_installed',
|
||||
}
|
||||
|
||||
export type BackgroundAction = {
|
||||
@@ -110,11 +135,14 @@ export type RequestHistory = {
|
||||
secretHeaders?: string[];
|
||||
secretResps?: string[];
|
||||
cid?: string;
|
||||
metadata?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const initRPC = () => {
|
||||
browser.runtime.onMessage.addListener(
|
||||
async (request, sender, sendResponse) => {
|
||||
(request, sender, sendResponse): any => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.get_requests:
|
||||
return handleGetRequests(request, sendResponse);
|
||||
@@ -126,8 +154,7 @@ export const initRPC = () => {
|
||||
case BackgroundActiontype.finish_prove_request:
|
||||
return handleFinishProveRequest(request, sendResponse);
|
||||
case BackgroundActiontype.delete_prove_request:
|
||||
await removeNotaryRequest(request.data);
|
||||
return sendResponse();
|
||||
return removeNotaryRequest(request.data);
|
||||
case BackgroundActiontype.retry_prove_request:
|
||||
return handleRetryProveReqest(request, sendResponse);
|
||||
case BackgroundActiontype.prove_request_start:
|
||||
@@ -152,6 +179,29 @@ export const initRPC = () => {
|
||||
return handleExecPluginProver(request);
|
||||
case BackgroundActiontype.open_popup:
|
||||
return handleOpenPopup(request);
|
||||
case BackgroundActiontype.connect_request:
|
||||
return handleConnect(request);
|
||||
case BackgroundActiontype.get_history_request:
|
||||
return handleGetHistory(request);
|
||||
case BackgroundActiontype.get_proof_request:
|
||||
return handleGetProof(request);
|
||||
case BackgroundActiontype.notarize_request:
|
||||
return handleNotarizeRequest(request);
|
||||
case BackgroundActiontype.install_plugin_request:
|
||||
return handleInstallPluginRequest(request);
|
||||
case BackgroundActiontype.get_plugins_request:
|
||||
return handleGetPluginsRequest(request);
|
||||
case BackgroundActiontype.run_plugin_request:
|
||||
return handleRunPluginCSRequest(request);
|
||||
case BackgroundActiontype.get_logging_level:
|
||||
getLoggingFilter().then(sendResponse);
|
||||
return true;
|
||||
case BackgroundActiontype.get_app_state:
|
||||
getAppState().then(sendResponse);
|
||||
return true;
|
||||
case BackgroundActiontype.set_default_plugins_installed:
|
||||
setDefaultPluginsInstalled(request.data).then(sendResponse);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -162,28 +212,32 @@ export const initRPC = () => {
|
||||
function handleGetRequests(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
): boolean {
|
||||
const cache = getCacheByTabId(request.data);
|
||||
const keys = cache.keys() || [];
|
||||
const data = keys.map((key) => cache.get(key));
|
||||
return data;
|
||||
sendResponse(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleGetProveRequests(
|
||||
function handleGetProveRequests(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const reqs = await getNotaryRequests();
|
||||
for (const req of reqs) {
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(req),
|
||||
});
|
||||
}
|
||||
return sendResponse();
|
||||
): boolean {
|
||||
getNotaryRequests().then(async (reqs) => {
|
||||
for (const req of reqs) {
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(req),
|
||||
});
|
||||
}
|
||||
sendResponse(reqs);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleFinishProveRequest(
|
||||
@@ -259,7 +313,6 @@ async function handleRetryProveReqest(
|
||||
...req,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -308,7 +361,7 @@ async function handleProveRequestStart(
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.process_prove_request,
|
||||
data: {
|
||||
id,
|
||||
@@ -323,7 +376,6 @@ async function handleProveRequestStart(
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -335,17 +387,14 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
notaryUrl: _notaryUrl,
|
||||
websocketProxyUrl: _websocketProxyUrl,
|
||||
maxSentData: _maxSentData,
|
||||
maxRecvData: _maxRecvData,
|
||||
} = request.data;
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
});
|
||||
const body = await extractBodyFromResponse(resp);
|
||||
const notaryUrl = _notaryUrl || (await getNotaryApi());
|
||||
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
|
||||
const maxSentData = _maxSentData || (await getMaxSent());
|
||||
@@ -362,6 +411,8 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
maxSentData,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
});
|
||||
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
@@ -387,7 +438,8 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
maxSentData,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -402,27 +454,23 @@ export async function handleExecPluginProver(request: BackgroundAction) {
|
||||
function handleGetCookiesByHostname(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const cache = getCookieStoreByHost(request.data);
|
||||
const keys = cache.keys() || [];
|
||||
const data = keys.reduce((acc: { [k: string]: string }, key) => {
|
||||
acc[key] = cache.get(key) || '';
|
||||
return acc;
|
||||
}, {});
|
||||
return data;
|
||||
): boolean {
|
||||
(async () => {
|
||||
const store = await getCookiesByHost(request.data);
|
||||
sendResponse(store);
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleGetHeadersByHostname(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const cache = getHeaderStoreByHost(request.data);
|
||||
const keys = cache.keys() || [];
|
||||
const data = keys.reduce((acc: { [k: string]: string }, key) => {
|
||||
acc[key] = cache.get(key) || '';
|
||||
return acc;
|
||||
}, {});
|
||||
return data;
|
||||
): boolean {
|
||||
(async () => {
|
||||
const cache = await getHeadersByHost(request.data);
|
||||
sendResponse(cache);
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleAddPlugin(
|
||||
@@ -504,43 +552,60 @@ async function handleGetPluginConfigByHash(
|
||||
return config;
|
||||
}
|
||||
|
||||
async function handleRunPlugin(
|
||||
function handleRunPlugin(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const { hash, method, params } = request.data;
|
||||
const hex = await getPluginByHash(hash);
|
||||
const arrayBuffer = hexToArrayBuffer(hex!);
|
||||
const config = await getPluginConfig(arrayBuffer);
|
||||
const plugin = await makePlugin(arrayBuffer, config);
|
||||
devlog(`plugin::${method}`, params);
|
||||
const out = await plugin.call(method, params);
|
||||
devlog(`plugin response: `, out.string());
|
||||
return JSON.parse(out.string());
|
||||
(async () => {
|
||||
const { hash, method, params } = request.data;
|
||||
const hex = await getPluginByHash(hash);
|
||||
const arrayBuffer = hexToArrayBuffer(hex!);
|
||||
const config = await getPluginConfig(arrayBuffer);
|
||||
const plugin = await makePlugin(arrayBuffer, config);
|
||||
devlog(`plugin::${method}`, params);
|
||||
const out = await plugin.call(method, params);
|
||||
devlog(`plugin response: `, out.string());
|
||||
sendResponse(JSON.parse(out.string()));
|
||||
})();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let cachePopup: browser.Windows.Window | null = null;
|
||||
|
||||
async function openPopup(route: string, left?: number, top?: number) {
|
||||
const tab = await browser.tabs.create({
|
||||
url: browser.runtime.getURL('popup.html') + '#' + route,
|
||||
active: false,
|
||||
});
|
||||
|
||||
const popup = await browser.windows.create({
|
||||
tabId: tab.id,
|
||||
type: 'popup',
|
||||
focused: true,
|
||||
width: 480,
|
||||
height: 640,
|
||||
left: Math.round(left || 0),
|
||||
top: Math.round(top || 0),
|
||||
});
|
||||
|
||||
return { popup, tab };
|
||||
}
|
||||
|
||||
async function handleOpenPopup(request: BackgroundAction) {
|
||||
if (cachePopup) {
|
||||
browser.windows.update(cachePopup.id!, {
|
||||
focused: true,
|
||||
});
|
||||
} else {
|
||||
const tab = await browser.tabs.create({
|
||||
browser.tabs.update(cachePopup.id!, {
|
||||
url: browser.runtime.getURL('popup.html') + '#' + request.data.route,
|
||||
active: false,
|
||||
});
|
||||
|
||||
const popup = await browser.windows.create({
|
||||
tabId: tab.id,
|
||||
type: 'popup',
|
||||
focused: true,
|
||||
width: 480,
|
||||
height: 640,
|
||||
left: request.data.position.left,
|
||||
top: request.data.position.top,
|
||||
});
|
||||
} else {
|
||||
const { popup } = await openPopup(
|
||||
request.data.route,
|
||||
request.data.position.left,
|
||||
request.data.position.top,
|
||||
);
|
||||
|
||||
cachePopup = popup;
|
||||
|
||||
@@ -554,3 +619,479 @@ async function handleOpenPopup(request: BackgroundAction) {
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConnect(request: BackgroundAction) {
|
||||
const connection = await getConnection(request.data.origin);
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
if (!connection) {
|
||||
const defer = deferredPromise();
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`connection-approval?origin=${encodeURIComponent(request.data.origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
request.data.position.left,
|
||||
request.data.position.top,
|
||||
);
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.connect_response) {
|
||||
defer.resolve(req.data);
|
||||
if (req.data) {
|
||||
await setConnection(request.data.origin);
|
||||
} else {
|
||||
await deleteConnection(request.data.origin);
|
||||
}
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
defer.resolve(false);
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleGetHistory(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const {
|
||||
origin,
|
||||
position,
|
||||
method: filterMethod,
|
||||
url: filterUrl,
|
||||
metadata: filterMetadata,
|
||||
} = request.data;
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`get-history-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}method=${filterMethod}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.get_history_response) {
|
||||
if (req.data) {
|
||||
const response = await getNotaryRequests();
|
||||
|
||||
const result = response
|
||||
.map(
|
||||
({ id, method, url, notaryUrl, websocketProxyUrl, metadata }) => ({
|
||||
id,
|
||||
time: new Date(charwise.decode(id)),
|
||||
method,
|
||||
url,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
}),
|
||||
)
|
||||
.filter(({ method, url, 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(method, filterMethod, { nocase: true }) &&
|
||||
minimatch(url, filterUrl) &&
|
||||
matchedMetadata
|
||||
);
|
||||
});
|
||||
|
||||
defer.resolve(result);
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
async function handleGetProof(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const { origin, position, id } = request.data;
|
||||
|
||||
const response = await getNotaryRequest(id);
|
||||
|
||||
if (!response) {
|
||||
defer.reject(new Error('proof id not found.'));
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`get-proof-approval?id=${id}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.get_proof_response) {
|
||||
if (req.data) {
|
||||
defer.resolve(response?.proof || null);
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
async function handleNotarizeRequest(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
headers,
|
||||
body,
|
||||
maxSentData = await getMaxSent(),
|
||||
maxRecvData = await getMaxRecv(),
|
||||
maxTranscriptSize,
|
||||
notaryUrl = await getNotaryApi(),
|
||||
websocketProxyUrl = await getProxyApi(),
|
||||
origin,
|
||||
position,
|
||||
metadata,
|
||||
} = request.data;
|
||||
|
||||
const config = JSON.stringify({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
metadata,
|
||||
});
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`notarize-approval?config=${encodeURIComponent(config)}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const id = charwise.encode(now).toString('hex');
|
||||
let isUserClose = true;
|
||||
|
||||
const onNotarizationResponse = async (req: any) => {
|
||||
if (req.type !== OffscreenActionTypes.notarization_response) return;
|
||||
if (req.data.id !== id) return;
|
||||
|
||||
if (req.data.error) defer.reject(req.data.error);
|
||||
if (req.data.proof) defer.resolve(req.data.proof);
|
||||
|
||||
browser.runtime.onMessage.removeListener(onNotarizationResponse);
|
||||
};
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.notarize_response) {
|
||||
if (req.data) {
|
||||
try {
|
||||
const { secretHeaders, secretResps } = req.data;
|
||||
await addNotaryRequest(now, req.data);
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
|
||||
browser.runtime.onMessage.addListener(onNotarizationResponse);
|
||||
browser.runtime.sendMessage({
|
||||
type: OffscreenActionTypes.notarization_request,
|
||||
data: {
|
||||
id,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
defer.reject(e);
|
||||
}
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
isUserClose = false;
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (isUserClose && windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
async function handleInstallPluginRequest(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const { origin, position, url, metadata } = request.data;
|
||||
|
||||
let arrayBuffer: ArrayBuffer, config: PluginConfig;
|
||||
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
arrayBuffer = await resp.arrayBuffer();
|
||||
config = await getPluginConfig(arrayBuffer);
|
||||
} catch (e) {
|
||||
defer.reject(e);
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`install-plugin-approval?${metadata ? `metadata=${JSON.stringify(metadata)}&` : ''}url=${url}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.install_plugin_response) {
|
||||
if (req.data) {
|
||||
try {
|
||||
const hex = Buffer.from(arrayBuffer).toString('hex');
|
||||
const hash = await addPlugin(hex);
|
||||
await addPluginConfig(hash!, config);
|
||||
await addPluginMetadata(hash!, {
|
||||
...metadata,
|
||||
origin,
|
||||
filePath: url,
|
||||
});
|
||||
defer.resolve(hash);
|
||||
} catch (e) {
|
||||
defer.reject(e);
|
||||
}
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
async function handleGetPluginsRequest(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const {
|
||||
origin,
|
||||
position,
|
||||
origin: filterOrigin,
|
||||
url: filterUrl,
|
||||
metadata: filterMetadata,
|
||||
} = request.data;
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`get-plugins-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}&filterOrigin=${filterOrigin}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.get_plugins_response) {
|
||||
if (req.data) {
|
||||
const response = await getPlugins();
|
||||
|
||||
const result = 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, filterUrl) &&
|
||||
minimatch(metadata.origin, filterOrigin || '**') &&
|
||||
matchedMetadata
|
||||
);
|
||||
});
|
||||
|
||||
defer.resolve(result);
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
async function handleRunPluginCSRequest(request: BackgroundAction) {
|
||||
const [currentTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
const defer = deferredPromise();
|
||||
const { origin, position, hash } = request.data;
|
||||
|
||||
const plugin = await getPluginByHash(hash);
|
||||
const config = await getPluginConfigByHash(hash);
|
||||
let isUserClose = true;
|
||||
|
||||
if (!plugin || !config) {
|
||||
defer.reject(new Error('plugin not found.'));
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
const { popup, tab } = await openPopup(
|
||||
`run-plugin-approval?hash=${hash}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
|
||||
position.left,
|
||||
position.top,
|
||||
);
|
||||
|
||||
const onPluginRequest = async (req: any) => {
|
||||
console.log(req);
|
||||
if (req.type !== SidePanelActionTypes.execute_plugin_response) return;
|
||||
if (req.data.hash !== hash) return;
|
||||
|
||||
if (req.data.error) defer.reject(req.data.error);
|
||||
if (req.data.proof) defer.resolve(req.data.proof);
|
||||
|
||||
browser.runtime.onMessage.removeListener(onPluginRequest);
|
||||
};
|
||||
|
||||
const onMessage = async (req: BackgroundAction) => {
|
||||
if (req.type === BackgroundActiontype.run_plugin_response) {
|
||||
if (req.data) {
|
||||
browser.runtime.onMessage.addListener(onPluginRequest);
|
||||
} else {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.removeListener(onMessage);
|
||||
isUserClose = false;
|
||||
browser.tabs.remove(tab.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (isUserClose && windowId === popup.id) {
|
||||
defer.reject(new Error('user rejected.'));
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(onMessage);
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
120
src/entries/Content/content.ts
Normal file
120
src/entries/Content/content.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { ContentScriptTypes, RPCClient } from './rpc';
|
||||
import { RequestHistory } from '../Background/rpc';
|
||||
import { PluginConfig, PluginMetadata } from '../../utils/misc';
|
||||
import { Proof } 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<Proof | 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;
|
||||
maxTranscriptSize?: number;
|
||||
metadata?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
},
|
||||
): Promise<Proof> {
|
||||
const resp = await client.call(ContentScriptTypes.notarize, {
|
||||
url,
|
||||
method: requestOptions?.method,
|
||||
headers: requestOptions?.headers,
|
||||
body: requestOptions?.body,
|
||||
maxSentData: proofOptions?.maxSentData,
|
||||
maxRecvData: proofOptions?.maxRecvData,
|
||||
maxTranscriptSize: proofOptions?.maxTranscriptSize,
|
||||
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) {
|
||||
const resp = await client.call(ContentScriptTypes.run_plugin, {
|
||||
hash,
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
const resp = await client.call(ContentScriptTypes.connect);
|
||||
|
||||
if (resp) {
|
||||
return new TLSN();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
window.tlsn = {
|
||||
connect,
|
||||
};
|
||||
@@ -1,9 +1,218 @@
|
||||
window.onerror = (error) => {
|
||||
// console.log('error');
|
||||
// console.log(error);
|
||||
};
|
||||
import browser from 'webextension-polyfill';
|
||||
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
|
||||
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
|
||||
import { urlify } from '../../utils/misc';
|
||||
|
||||
(async () => {
|
||||
console.log('Content script works!');
|
||||
console.log('Must reload extension for modifications to take effect.');
|
||||
loadScript('content.bundle.js');
|
||||
const server = new RPCServer();
|
||||
|
||||
server.on(ContentScriptTypes.connect, async () => {
|
||||
const connected = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.connect_request,
|
||||
data: {
|
||||
...getPopupData(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!connected) throw new Error('user rejected.');
|
||||
|
||||
return connected;
|
||||
});
|
||||
|
||||
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;
|
||||
maxTranscriptSize?: number;
|
||||
}>,
|
||||
) => {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
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,
|
||||
maxTranscriptSize,
|
||||
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 }>) => {
|
||||
const { hash } = request.params || {};
|
||||
|
||||
if (!hash) throw new Error('params must include hash');
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.run_plugin_request,
|
||||
data: {
|
||||
...getPopupData(),
|
||||
hash,
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,120 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import { prove, set_logging_filter, verify } from 'tlsn-js';
|
||||
import * as Comlink from 'comlink';
|
||||
import { OffscreenActionTypes } from './types';
|
||||
import {
|
||||
NotaryServer,
|
||||
Prover as _Prover,
|
||||
NotarizedSession as _NotarizedSession,
|
||||
TlsProof as _TlsProof,
|
||||
} from 'tlsn-js';
|
||||
import { verify } from 'tlsn-js-v5';
|
||||
|
||||
import { urlify } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { getLoggingFilter } from '../../utils/storage';
|
||||
import { LOGGING_LEVEL_INFO } from '../../utils/constants';
|
||||
import { Proof, ProofV1 } from '../../utils/types';
|
||||
import { Method } from 'tlsn-js/wasm/pkg';
|
||||
|
||||
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const Offscreen = () => {
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.process_prove_request: {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body = '',
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
id,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
loggingFilter = LOGGING_LEVEL_INFO,
|
||||
} = request.data;
|
||||
(async () => {
|
||||
const loggingLevel = await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_logging_level,
|
||||
});
|
||||
await init({ loggingLevel });
|
||||
// @ts-ignore
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
switch (request.type) {
|
||||
case OffscreenActionTypes.notarization_request: {
|
||||
const { id } = request.data;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const token = urlify(url)?.hostname || '';
|
||||
await set_logging_filter(loggingFilter);
|
||||
const proof = await prove(url, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl: websocketProxyUrl + `?token=${token}`,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
});
|
||||
(async () => {
|
||||
try {
|
||||
const proof = await createProof(request.data);
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
proof,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('i caught an error');
|
||||
console.error(error);
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
proof,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.verify_proof: {
|
||||
(async () => {
|
||||
const result = await verify(request.data);
|
||||
sendResponse(result);
|
||||
})();
|
||||
browser.runtime.sendMessage({
|
||||
type: OffscreenActionTypes.notarization_response,
|
||||
data: {
|
||||
id,
|
||||
proof,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
case BackgroundActiontype.verify_prove_request: {
|
||||
(async () => {
|
||||
const result = await verify(request.data.proof);
|
||||
browser.runtime.sendMessage({
|
||||
type: OffscreenActionTypes.notarization_response,
|
||||
data: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.process_prove_request: {
|
||||
const { id } = request.data;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const proof = await createProof(request.data);
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
proof: proof,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.verify_proof: {
|
||||
(async () => {
|
||||
const result = await verifyProof(request.data);
|
||||
sendResponse(result);
|
||||
})();
|
||||
|
||||
return true;
|
||||
}
|
||||
case BackgroundActiontype.verify_prove_request: {
|
||||
(async () => {
|
||||
const proof: Proof = request.data.proof;
|
||||
const result: { sent: string; recv: string } =
|
||||
await verifyProof(proof);
|
||||
|
||||
if (result) {
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
@@ -90,17 +125,176 @@ const Offscreen = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
break;
|
||||
})();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div className="App" />;
|
||||
};
|
||||
|
||||
export default Offscreen;
|
||||
|
||||
function subtractRanges(
|
||||
ranges: { start: number; end: number },
|
||||
negatives: { start: number; end: number }[],
|
||||
): { start: number; end: number }[] {
|
||||
const returnVal: { start: number; end: number }[] = [ranges];
|
||||
|
||||
negatives
|
||||
.sort((a, b) => (a.start < b.start ? -1 : 1))
|
||||
.forEach(({ start, end }) => {
|
||||
const last = returnVal.pop()!;
|
||||
|
||||
if (start < last.start || end > last.end) {
|
||||
console.error('invalid ranges');
|
||||
return;
|
||||
}
|
||||
|
||||
if (start === last.start && end === last.end) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start === last.start && end < last.end) {
|
||||
returnVal.push({ start: end, end: last.end });
|
||||
return;
|
||||
}
|
||||
|
||||
if (start > last.start && end < last.end) {
|
||||
returnVal.push({ start: last.start, end: start });
|
||||
returnVal.push({ start: end, end: last.end });
|
||||
return;
|
||||
}
|
||||
|
||||
if (start > last.start && end === last.end) {
|
||||
returnVal.push({ start: last.start, end: start });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
async function createProof(options: {
|
||||
url: string;
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
method?: Method;
|
||||
headers?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
body?: any;
|
||||
maxSentData?: number;
|
||||
maxRecvData?: number;
|
||||
id: string;
|
||||
secretHeaders: string[];
|
||||
secretResps: string[];
|
||||
}): Promise<ProofV1> {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
id,
|
||||
secretHeaders = [],
|
||||
secretResps = [],
|
||||
} = options;
|
||||
|
||||
const hostname = urlify(url)?.hostname || '';
|
||||
const notary = NotaryServer.from(notaryUrl);
|
||||
const prover: _Prover = await new Prover({
|
||||
id,
|
||||
serverDns: hostname,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
});
|
||||
|
||||
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
|
||||
|
||||
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const transcript = await prover.transcript();
|
||||
|
||||
const commit = {
|
||||
sent: subtractRanges(
|
||||
transcript.ranges.sent.all,
|
||||
secretHeaders
|
||||
.map((secret: string) => {
|
||||
const index = transcript.sent.indexOf(secret);
|
||||
return index > -1
|
||||
? {
|
||||
start: index,
|
||||
end: index + secret.length,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter((data: any) => !!data) as { start: number; end: number }[],
|
||||
),
|
||||
recv: subtractRanges(
|
||||
transcript.ranges.recv.all,
|
||||
secretResps
|
||||
.map((secret: string) => {
|
||||
const index = transcript.recv.indexOf(secret);
|
||||
return index > -1
|
||||
? {
|
||||
start: index,
|
||||
end: index + secret.length,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter((data: any) => !!data) as { start: number; end: number }[],
|
||||
),
|
||||
};
|
||||
|
||||
const session: _NotarizedSession = await new NotarizedSession(
|
||||
await prover.notarize(commit),
|
||||
);
|
||||
|
||||
const proofHex = await session.proof(commit);
|
||||
const proof: ProofV1 = {
|
||||
version: '1.0',
|
||||
meta: {
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
},
|
||||
data: proofHex,
|
||||
};
|
||||
return proof;
|
||||
}
|
||||
|
||||
async function verifyProof(
|
||||
proof: Proof,
|
||||
): Promise<{ sent: string; recv: string }> {
|
||||
let result: { sent: string; recv: string };
|
||||
|
||||
switch (proof.version) {
|
||||
case undefined: {
|
||||
result = await verify(proof);
|
||||
break;
|
||||
}
|
||||
case '1.0': {
|
||||
const tlsProof: _TlsProof = await new TlsProof(proof.data);
|
||||
result = await tlsProof.verify({
|
||||
typ: 'P256',
|
||||
key: await NotaryServer.from(proof.meta.notaryUrl).publicKey(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
4
src/entries/Offscreen/types.ts
Normal file
4
src/entries/Offscreen/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum OffscreenActionTypes {
|
||||
notarization_request = 'offscreen/notarization_request',
|
||||
notarization_response = 'offscreen/notarization_response',
|
||||
}
|
||||
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, NotarizedSession, TlsProof } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
NotarizedSession,
|
||||
TlsProof,
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Options from '../../pages/Options';
|
||||
import './index.css';
|
||||
import './index.scss';
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Navigate, Route, Routes, useNavigate } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import Requests from '../../pages/Requests';
|
||||
import Options from '../../pages/Options';
|
||||
import Request from '../../pages/Requests/Request';
|
||||
import Home from '../../pages/Home';
|
||||
import Chat from '../../pages/Chat';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
import RequestBuilder from '../../pages/RequestBuilder';
|
||||
import Notarize from '../../pages/Notarize';
|
||||
@@ -20,11 +21,22 @@ import History from '../../pages/History';
|
||||
import ProofUploader from '../../pages/ProofUploader';
|
||||
import browser from 'webextension-polyfill';
|
||||
import store from '../../utils/store';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
import ConnectionDetailsModal from '../../components/ConnectionDetailsModal';
|
||||
import { ConnectionApproval } from '../../pages/ConnectionApproval';
|
||||
import { GetHistoryApproval } from '../../pages/GetHistoryApproval';
|
||||
import { GetProofApproval } from '../../pages/GetProofApproval';
|
||||
import { 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';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
const activeTab = useActiveTab();
|
||||
const url = useActiveTabUrl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,16 +93,7 @@ const Popup = () => {
|
||||
alt="logo"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
<div className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit">
|
||||
{!!activeTab?.favIconUrl && (
|
||||
<img
|
||||
src={activeTab?.favIconUrl}
|
||||
className="h-5 rounded-full"
|
||||
alt="logo"
|
||||
/>
|
||||
)}
|
||||
<div className="text-xs">{url?.hostname}</div>
|
||||
</div>
|
||||
<AppConnectionLogo />
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/requests/:requestId/*" element={<Request />} />
|
||||
@@ -102,6 +105,18 @@ const Popup = () => {
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/chat" element={<Chat />} />
|
||||
<Route path="/plugininfo" element={<PluginUploadInfo />} />
|
||||
<Route path="/connection-approval" element={<ConnectionApproval />} />
|
||||
<Route path="/get-history-approval" element={<GetHistoryApproval />} />
|
||||
<Route path="/get-proof-approval" element={<GetProofApproval />} />
|
||||
<Route path="/notarize-approval" element={<NotarizeApproval />} />
|
||||
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
|
||||
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
|
||||
<Route
|
||||
path="/install-plugin-approval"
|
||||
element={<InstallPluginApproval />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
@@ -109,3 +124,58 @@ const Popup = () => {
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
|
||||
function AppConnectionLogo() {
|
||||
const dispatch = useDispatch();
|
||||
const activeTab = useActiveTab();
|
||||
const url = useActiveTabUrl();
|
||||
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
|
||||
const connected = useIsConnected();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (url) {
|
||||
const isConnected: boolean | null = await getConnection(url?.origin);
|
||||
dispatch(setConnection(!!isConnected));
|
||||
}
|
||||
})();
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute right-2 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>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Popup from './Popup';
|
||||
import './index.scss';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from '../../utils/store';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
|
||||
@@ -2,7 +2,13 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import './sidePanel.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
|
||||
import { PluginConfig, StepConfig } from '../../utils/misc';
|
||||
import {
|
||||
getPluginConfig,
|
||||
hexToArrayBuffer,
|
||||
makePlugin,
|
||||
PluginConfig,
|
||||
StepConfig,
|
||||
} from '../../utils/misc';
|
||||
import { PluginList } from '../../components/PluginList';
|
||||
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
@@ -10,6 +16,10 @@ import classNames from 'classnames';
|
||||
import Icon from '../../components/Icon';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
|
||||
import type { Plugin } from '@extism/extism';
|
||||
import { OffscreenActionTypes } from '../Offscreen/types';
|
||||
import { SidePanelActionTypes } from './types';
|
||||
|
||||
export default function SidePanel(): ReactElement {
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
@@ -19,7 +29,7 @@ export default function SidePanel(): ReactElement {
|
||||
(async function () {
|
||||
const result = await browser.storage.local.get('plugin_hash');
|
||||
const { plugin_hash } = result;
|
||||
const config = await fetchPluginConfigByHash(plugin_hash);
|
||||
const config = await getPluginConfigByHash(plugin_hash);
|
||||
setHash(plugin_hash);
|
||||
setConfig(config);
|
||||
// await browser.storage.local.set({ plugin_hash: '' });
|
||||
@@ -50,16 +60,41 @@ function PluginBody(props: {
|
||||
const { hash } = props;
|
||||
const { title, description, icon, steps } = props.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);
|
||||
}
|
||||
},
|
||||
[responses],
|
||||
[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">
|
||||
@@ -102,10 +137,10 @@ function StepContent(
|
||||
description,
|
||||
cta,
|
||||
action,
|
||||
hash,
|
||||
setResponse,
|
||||
lastResponse,
|
||||
prover,
|
||||
hash,
|
||||
} = props;
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
@@ -113,26 +148,37 @@ function StepContent(
|
||||
const [notarizationId, setNotarizationId] = useState('');
|
||||
const notaryRequest = useRequestHistory(notarizationId);
|
||||
|
||||
const getPlugin = useCallback(async () => {
|
||||
const hex = await getPluginByHash(hash);
|
||||
const config = await getPluginConfigByHash(hash);
|
||||
const arrayBuffer = hexToArrayBuffer(hex!);
|
||||
return makePlugin(arrayBuffer, config!);
|
||||
}, [hash]);
|
||||
|
||||
const processStep = useCallback(async () => {
|
||||
const plugin = await getPlugin();
|
||||
if (!plugin) return;
|
||||
if (index > 0 && !lastResponse) return;
|
||||
|
||||
setPending(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
setError('');
|
||||
const val = await runPlugin(hash, action, JSON.stringify(lastResponse));
|
||||
const out = await plugin.call(action, JSON.stringify(lastResponse));
|
||||
const val = JSON.parse(out.string());
|
||||
if (val && prover) {
|
||||
setNotarizationId(val);
|
||||
} else {
|
||||
setCompleted(!!val);
|
||||
setResponse(val, index);
|
||||
}
|
||||
setResponse(val, index);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e?.message || 'Unkonwn error');
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [hash, action, index, lastResponse, prover]);
|
||||
}, [action, index, lastResponse, prover, getPlugin]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (
|
||||
|
||||
4
src/entries/SidePanel/types.ts
Normal file
4
src/entries/SidePanel/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum SidePanelActionTypes {
|
||||
execute_plugin_request = 'sidePanel/execute_plugin_request',
|
||||
execute_plugin_response = 'sidePanel/execute_plugin_response',
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
"name": "TLSN Extension",
|
||||
"description": "A chrome extension for TLSN",
|
||||
"options_page": "options.html",
|
||||
"background": { "service_worker": "background.bundle.js",
|
||||
"persistent": true
|
||||
"background": {
|
||||
"service_worker": "background.bundle.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
@@ -28,8 +28,8 @@
|
||||
],
|
||||
"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>"],
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
63
src/pages/Chat/chat.css
Normal file
63
src/pages/Chat/chat.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.chat-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.user {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bot {
|
||||
background-color: #f1f0f0;
|
||||
color: black;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chat-input input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-input button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
303
src/pages/Chat/index.tsx
Normal file
303
src/pages/Chat/index.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import './Chat.css';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
import { extractBodyFromResponse } from '../../utils/misc';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
text: string;
|
||||
sender: 'user' | 'bot';
|
||||
}
|
||||
|
||||
interface CapturedData {
|
||||
request: string;
|
||||
headers: Record<string, string>;
|
||||
response: string;
|
||||
}
|
||||
|
||||
interface RequestData {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
interface TabInfo {
|
||||
url: string;
|
||||
title: string;
|
||||
favicon: string;
|
||||
}
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const [messages, setMessages] = useState<Message[]>(() => {
|
||||
const savedMessages = localStorage.getItem('chatMessages');
|
||||
return savedMessages ? JSON.parse(savedMessages) : [];
|
||||
});
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [allRequests, setAllRequests] = useState<RequestData[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const socketRef = useRef<WebSocket | null>(null);
|
||||
const [chatId, setChatId] = useState<string | null>(null);
|
||||
const requests = useRequests();
|
||||
const [capturedData, setCapturedData] = useState<CapturedData[]>([]);
|
||||
const [hasSetInitialTabInfo, setHasSetInitialTabInfo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('chatMessages', JSON.stringify(messages));
|
||||
}, [messages]);
|
||||
|
||||
const getCurrentTabInfo = async (): Promise<TabInfo> => {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
const currentTab = tabs[0];
|
||||
resolve({
|
||||
url: currentTab.url || '',
|
||||
title: currentTab.title || '',
|
||||
favicon: currentTab.favIconUrl || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initializeChat = async () => {
|
||||
const storedChatId = localStorage.getItem('chatId');
|
||||
if (storedChatId) {
|
||||
setChatId(storedChatId);
|
||||
await connectWebSocket(storedChatId);
|
||||
} else {
|
||||
await fetchNewChatId();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeChat();
|
||||
return () => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Effect to set initial tab info when connection is established
|
||||
useEffect(() => {
|
||||
const setInitialTabInfo = async () => {
|
||||
if (isConnected && messages.length === 0 && !hasSetInitialTabInfo) {
|
||||
const tabInfo = await getCurrentTabInfo();
|
||||
setInputMessage(`Current Page: ${tabInfo.title}\n website URL: ${tabInfo.url}`);
|
||||
setHasSetInitialTabInfo(true);
|
||||
|
||||
// Send initial info to background script
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'TAB_INFO',
|
||||
data: tabInfo
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setInitialTabInfo();
|
||||
}, [isConnected, messages.length, hasSetInitialTabInfo]);
|
||||
|
||||
const fetchNewChatId = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/get_chat_id');
|
||||
const data = await response.json();
|
||||
const newChatId = data.chat_id;
|
||||
localStorage.setItem('chatId', newChatId);
|
||||
setChatId(newChatId);
|
||||
await connectWebSocket(newChatId);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch chat ID:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const captureRequestAndResponse = useCallback(async (req: RequestData) => {
|
||||
try {
|
||||
const response = await fetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
});
|
||||
const responseText = await extractBodyFromResponse(response);
|
||||
const headers: Record<string, string> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
setCapturedData(prevData => [...prevData, {
|
||||
request: `${req.method} ${req.url}`,
|
||||
headers,
|
||||
response: responseText,
|
||||
}]);
|
||||
} catch (error) {
|
||||
console.error('Error capturing request and response:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMultipleRequests = async (requests: RequestData[]) => {
|
||||
try {
|
||||
const fetchPromises = requests.map(async (req) => {
|
||||
if (req.headers === null || req.headers === undefined) {
|
||||
req.headers = {};
|
||||
}
|
||||
if (req.body === null || req.body === undefined) {
|
||||
req.body = '';
|
||||
}
|
||||
const response = await fetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
});
|
||||
const responseText = await response.text();
|
||||
return {
|
||||
request: `${req.method} ${req.url}`,
|
||||
headers: req.headers,
|
||||
response: responseText,
|
||||
};
|
||||
});
|
||||
|
||||
const responses = await Promise.all(fetchPromises);
|
||||
setCapturedData(prevData => [...prevData, ...responses]);
|
||||
const response_message = responses.map(data => data.response).join('\n');
|
||||
setInputMessage(response_message);
|
||||
} catch (error) {
|
||||
console.error('Error fetching multiple requests:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const connectWebSocket = async (id: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
socketRef.current = new WebSocket(`ws://localhost:8000/ws/${id}`);
|
||||
|
||||
socketRef.current.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
setIsConnected(true);
|
||||
resolve();
|
||||
};
|
||||
|
||||
socketRef.current.onmessage = (event) => {
|
||||
const botResponse: Message = {
|
||||
id: Date.now(),
|
||||
text: event.data,
|
||||
sender: 'bot',
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, botResponse]);
|
||||
|
||||
if (botResponse.text.includes("send_request_function")) {
|
||||
const updatedRequests = requests.map(req => ({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
headers: req.requestHeaders.reduce((acc: { [key: string]: string }, h: any) => {
|
||||
if (h.name && h.value) acc[h.name] = h.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
}));
|
||||
|
||||
setAllRequests(updatedRequests);
|
||||
const requestDetails = updatedRequests.map(req =>
|
||||
`${req.method} ${req.url}\nHeaders: ${JSON.stringify(req.headers, null, 2)}`
|
||||
).join('\n\n');
|
||||
setInputMessage(requestDetails);
|
||||
}
|
||||
|
||||
if (botResponse.text.includes("send_response_function")) {
|
||||
const regex = /"send_response_function"\s*:\s*(\[.*?\])/s;
|
||||
const match = botResponse.text.match(regex);
|
||||
if (match) {
|
||||
const requestArrayString = match[1];
|
||||
try {
|
||||
const requestArray: RequestData[] = JSON.parse(requestArrayString);
|
||||
fetchMultipleRequests(requestArray);
|
||||
} catch (error) {
|
||||
console.error("Error parsing JSON:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socketRef.current.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
socketRef.current.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (capturedData.length > 0 && isConnected) {
|
||||
const capturedDataMessage = JSON.stringify(capturedData);
|
||||
socketRef.current?.send(capturedDataMessage);
|
||||
setCapturedData([]);
|
||||
}
|
||||
}, [capturedData, isConnected]);
|
||||
|
||||
const sendMessage = () => {
|
||||
if (inputMessage.trim() === '' || !isConnected) return;
|
||||
|
||||
const newMessage: Message = {
|
||||
id: Date.now(),
|
||||
text: inputMessage,
|
||||
sender: 'user',
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
||||
setInputMessage('');
|
||||
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.send(inputMessage);
|
||||
} else {
|
||||
console.error('WebSocket is not connected');
|
||||
}
|
||||
};
|
||||
|
||||
const clearChat = () => {
|
||||
setMessages([]);
|
||||
setAllRequests([]);
|
||||
setCapturedData([]);
|
||||
setHasSetInitialTabInfo(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="chat-window">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`message ${message.sender === 'user' ? 'user' : 'bot'}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="chat-input">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
||||
placeholder="Type your message..."
|
||||
className="chat-input-field"
|
||||
/>
|
||||
<div className="chat-buttons">
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
className="send-button"
|
||||
disabled={!isConnected}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
onClick={clearChat}
|
||||
className="clear-button"
|
||||
style={{ backgroundColor: '#f44336', color: 'white', border: 'none' }}
|
||||
>
|
||||
Clear Chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!isConnected && <div className="connection-status">Disconnected</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
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>
|
||||
);
|
||||
}
|
||||
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, 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>
|
||||
);
|
||||
}
|
||||
@@ -7,13 +7,7 @@ import {
|
||||
deleteRequestHistory,
|
||||
} from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import {
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
} from '../../utils/storage';
|
||||
import { getNotaryApi, getProxyApi } from '../../utils/storage';
|
||||
import { urlify, download, upload } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import Modal, { ModalContent } from '../../components/Modal/Modal';
|
||||
@@ -24,6 +18,7 @@ import {
|
||||
getNotaryRequest,
|
||||
setNotaryRequestCid,
|
||||
} from '../../entries/Background/db';
|
||||
const charwise = require('charwise');
|
||||
|
||||
export default function History(): ReactElement {
|
||||
const history = useHistoryOrder();
|
||||
@@ -37,7 +32,12 @@ export default function History(): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
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);
|
||||
@@ -115,7 +115,12 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
}, [props.requestId, request, cid]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<ShareConfirmationModal />
|
||||
<ErrorModal />
|
||||
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
|
||||
@@ -127,6 +132,12 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
{requestUrl?.pathname}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="font-bold text-slate-400">Time:</div>
|
||||
<div className="ml-2 text-slate-800">
|
||||
{new Date(charwise.decode(props.requestId, 'hex')).toISOString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="font-bold text-slate-400">Host:</div>
|
||||
<div className="ml-2 text-slate-800">{requestUrl?.host}</div>
|
||||
@@ -136,7 +147,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
<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="font-bold text-slate-400">TLS Proxy API:</div>
|
||||
<div className="ml-2 text-slate-800">
|
||||
{request?.websocketProxyUrl}
|
||||
</div>
|
||||
@@ -150,6 +161,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
onClick={onView}
|
||||
fa="fa-solid fa-receipt"
|
||||
ctaText="View Proof"
|
||||
hidden={hideActions.includes('view')}
|
||||
/>
|
||||
<ActionButton
|
||||
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
|
||||
@@ -158,35 +170,42 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
}
|
||||
fa="fa-solid fa-download"
|
||||
ctaText="Download"
|
||||
hidden={hideActions.includes('download')}
|
||||
/>
|
||||
<ActionButton
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
onClick={() => setShowingShareConfirmation(true)}
|
||||
fa="fa-solid fa-upload"
|
||||
ctaText="Share"
|
||||
hidden={hideActions.includes('share')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{status === 'error' && !!request?.error && <ErrorButton />}
|
||||
{(!status || status === 'error') && <RetryButton />}
|
||||
{status === 'error' && !!request?.error && (
|
||||
<ErrorButton hidden={hideActions.includes('error')} />
|
||||
)}
|
||||
{(!status || status === 'error') && (
|
||||
<RetryButton hidden={hideActions.includes('retry')} />
|
||||
)}
|
||||
{status === 'pending' && (
|
||||
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
|
||||
<span className="text-xs font-bold">Pending</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
<ActionButton
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-trash" size={1} />
|
||||
<span className="text-xs font-bold">Delete</span>
|
||||
</button>
|
||||
fa="fa-solid fa-trash"
|
||||
ctaText="Delete"
|
||||
hidden={hideActions.includes('delete')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function RetryButton(): ReactElement {
|
||||
function RetryButton(p: { hidden?: boolean }): ReactElement {
|
||||
if (p.hidden) return <></>;
|
||||
return (
|
||||
<button
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
@@ -198,7 +217,8 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorButton(): ReactElement {
|
||||
function ErrorButton(p: { hidden?: boolean }): ReactElement {
|
||||
if (p.hidden) return <></>;
|
||||
return (
|
||||
<button
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
|
||||
@@ -211,6 +231,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
}
|
||||
|
||||
function ErrorModal(): ReactElement {
|
||||
const msg = typeof request?.error === 'string' && request?.error;
|
||||
return !showingError ? (
|
||||
<></>
|
||||
) : (
|
||||
@@ -219,7 +240,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-slate-500">
|
||||
{request?.error || 'Something went wrong :('}
|
||||
{msg || 'Something went wrong :('}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
|
||||
@@ -309,7 +330,10 @@ function ActionButton(props: {
|
||||
fa: string;
|
||||
ctaText: string;
|
||||
className?: string;
|
||||
hidden?: boolean;
|
||||
}): ReactElement {
|
||||
if (props.hidden) return <></>;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
import { makePlugin, getPluginConfig } from '../../utils/misc';
|
||||
import { addPlugin } from '../../utils/rpc';
|
||||
import { PluginList } from '../../components/PluginList';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
import { ErrorModal } from '../../components/ErrorModal';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
@@ -20,22 +17,6 @@ export default function Home(): ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const [error, showError] = useState('');
|
||||
|
||||
const onAddPlugin = 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);
|
||||
await getPluginConfig(plugin);
|
||||
await addPlugin(Buffer.from(arrayBuffer).toString('hex'));
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
@@ -57,16 +38,15 @@ export default function Home(): ReactElement {
|
||||
History
|
||||
</NavButton>
|
||||
<NavButton className="relative" fa="fa-solid fa-plus">
|
||||
<input
|
||||
className="opacity-0 absolute top-0 right-0 h-full w-full"
|
||||
type="file"
|
||||
onChange={onAddPlugin}
|
||||
/>
|
||||
<PluginUploadInfo />
|
||||
Add a plugin
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
|
||||
Options
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-comment-dots" onClick={() => navigate('/chat')}>
|
||||
Chat
|
||||
</NavButton>
|
||||
</div>
|
||||
<PluginList className="mx-4" />
|
||||
</div>
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
useState,
|
||||
useCallback,
|
||||
@@ -8,14 +7,11 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { notarizeRequest, useRequest } from '../../reducers/requests';
|
||||
import Icon from '../../components/Icon';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import {
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getMaxSent,
|
||||
@@ -125,7 +121,7 @@ export default function Notarize(): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
function RevealHeaderStep(props: {
|
||||
export function RevealHeaderStep(props: {
|
||||
onNext: () => void;
|
||||
onCancel: () => void;
|
||||
setSecretHeaders: (secrets: string[]) => void;
|
||||
@@ -134,6 +130,8 @@ function RevealHeaderStep(props: {
|
||||
const req = useRequest(params.requestId);
|
||||
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
const headers = req?.requestHeaders;
|
||||
|
||||
useEffect(() => {
|
||||
if (!req) return;
|
||||
|
||||
@@ -161,21 +159,21 @@ function RevealHeaderStep(props: {
|
||||
[revealed, req],
|
||||
);
|
||||
|
||||
if (!req) return <></>;
|
||||
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
|
||||
`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">
|
||||
{req.requestHeaders?.map((h) => (
|
||||
{headers.map((h) => (
|
||||
<tr
|
||||
key={h.name}
|
||||
className={classNames('border-b border-slate-200 text-xs', {
|
||||
'bg-slate-50': !!revealed[h.name],
|
||||
'bg-slate-50': revealed[h.name],
|
||||
})}
|
||||
>
|
||||
<td className="border border-slate-300 py-1 px-2 align-top">
|
||||
@@ -183,14 +181,14 @@ function RevealHeaderStep(props: {
|
||||
type="checkbox"
|
||||
className="cursor-pointer"
|
||||
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
|
||||
checked={!!revealed[h.name]}
|
||||
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]
|
||||
{revealed[h.name]
|
||||
? h.value
|
||||
: Array(h.value?.length || 0)
|
||||
.fill('*')
|
||||
@@ -216,6 +214,73 @@ function RevealHeaderStep(props: {
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function HideResponseStep(props: {
|
||||
onNext: () => void;
|
||||
onCancel: () => void;
|
||||
@@ -298,6 +363,7 @@ function HideResponseStep(props: {
|
||||
.join(''),
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
@@ -326,6 +392,112 @@ function HideResponseStep(props: {
|
||||
);
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -24,20 +24,18 @@ import {
|
||||
NOTARY_PROXY,
|
||||
MAX_RECV,
|
||||
MAX_SENT,
|
||||
LOGGING_LEVEL_INFO,
|
||||
LOGGING_LEVEL_NONE,
|
||||
LOGGING_LEVEL_DEBUG,
|
||||
LOGGING_LEVEL_TRACE,
|
||||
} 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';
|
||||
|
||||
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(LOGGING_LEVEL_INFO);
|
||||
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
|
||||
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [shouldReload, setShouldReload] = useState(false);
|
||||
@@ -50,7 +48,7 @@ export default function Options(): ReactElement {
|
||||
setProxy((await getProxyApi()) || NOTARY_PROXY);
|
||||
setMaxReceived((await getMaxRecv()) || MAX_RECV);
|
||||
setMaxSent((await getMaxSent()) || MAX_SENT);
|
||||
setLoggingLevel((await getLoggingFilter()) || LOGGING_LEVEL_INFO);
|
||||
setLoggingLevel((await getLoggingFilter()) || 'Info');
|
||||
})();
|
||||
}, [advanced]);
|
||||
|
||||
@@ -170,7 +168,7 @@ function InputField(props: {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">{label}</div>
|
||||
<div className="font-semibold cursor-default">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
className="input border"
|
||||
@@ -194,6 +192,10 @@ function NormalOptions(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"
|
||||
@@ -214,9 +216,9 @@ function NormalOptions(props: {
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<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">{EXPLORER_API}</div>
|
||||
<div className="input border bg-slate-100">{EXPLORER_API}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -225,12 +227,12 @@ function NormalOptions(props: {
|
||||
function AdvancedOptions(props: {
|
||||
maxSent: number;
|
||||
maxReceived: number;
|
||||
loggingLevel: string;
|
||||
loggingLevel: LoggingLevel;
|
||||
setShouldReload: (reload: boolean) => void;
|
||||
setMaxSent: (value: number) => void;
|
||||
setMaxReceived: (value: number) => void;
|
||||
setDirty: (value: boolean) => void;
|
||||
setLoggingLevel: (level: string) => void;
|
||||
setLoggingLevel: (level: LoggingLevel) => void;
|
||||
}) {
|
||||
const {
|
||||
maxSent,
|
||||
@@ -270,16 +272,17 @@ function AdvancedOptions(props: {
|
||||
<select
|
||||
className="select !bg-white border !px-2 !py-1"
|
||||
onChange={(e) => {
|
||||
setLoggingLevel(e.target.value);
|
||||
setLoggingLevel(e.target.value as LoggingLevel);
|
||||
setDirty(true);
|
||||
setShouldReload(true);
|
||||
}}
|
||||
value={loggingLevel}
|
||||
>
|
||||
<option value={LOGGING_LEVEL_NONE}>None</option>
|
||||
<option value={LOGGING_LEVEL_INFO}>Info</option>
|
||||
<option value={LOGGING_LEVEL_DEBUG}>Debug</option>
|
||||
<option value={LOGGING_LEVEL_TRACE}>Trace</option>
|
||||
<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>
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function ProofUploader(): ReactElement {
|
||||
recv: string;
|
||||
sent: string;
|
||||
} | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
async (e) => {
|
||||
@@ -25,16 +26,21 @@ export default function ProofUploader(): ReactElement {
|
||||
const result = event.target?.result;
|
||||
if (result) {
|
||||
const proof = JSON.parse(result as string);
|
||||
const res = await chrome.runtime.sendMessage<
|
||||
any,
|
||||
{ recv: string; sent: string }
|
||||
>({
|
||||
type: BackgroundActiontype.verify_proof,
|
||||
data: proof,
|
||||
});
|
||||
setProof(res);
|
||||
const res = await chrome.runtime
|
||||
.sendMessage<any, { recv: string; sent: string }>({
|
||||
type: BackgroundActiontype.verify_proof,
|
||||
data: proof,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (proof) {
|
||||
setUploading(false);
|
||||
setProof(res);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setUploading(true);
|
||||
reader.readAsText(file);
|
||||
}
|
||||
},
|
||||
@@ -53,16 +59,23 @@ export default function ProofUploader(): ReactElement {
|
||||
className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
|
||||
onChange={onFileUpload}
|
||||
accept=".json"
|
||||
disabled={uploading}
|
||||
/>
|
||||
<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>
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -57,12 +57,14 @@ export default function ProofViewer(props?: {
|
||||
<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>
|
||||
)}
|
||||
</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 [params, setParams] = useState<[string, string, boolean?][]>(
|
||||
props?.params || [],
|
||||
);
|
||||
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
|
||||
props?.headers || [],
|
||||
);
|
||||
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 [response, setResponse] = useState<Response | null>(
|
||||
props?.response || null,
|
||||
const [type, setType] = useState<string>('text/plain');
|
||||
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
|
||||
props?.headers || [['Content-Type', type, true]],
|
||||
);
|
||||
|
||||
const [responseData, setResponseData] = useState<{
|
||||
json: any | null;
|
||||
text: string | null;
|
||||
img: string | null;
|
||||
headers: [string, string][] | null;
|
||||
} | null>(null);
|
||||
|
||||
const url = urlify(_url);
|
||||
|
||||
const href = !url
|
||||
@@ -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,65 @@ 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: [],
|
||||
maxTranscriptSize: 0,
|
||||
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 +230,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 +263,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 +305,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>
|
||||
|
||||
119
src/pages/RunPluginApproval/index.tsx
Normal file
119
src/pages/RunPluginApproval/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
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';
|
||||
|
||||
export function RunPluginApproval(): ReactElement {
|
||||
const [params] = useSearchParams();
|
||||
const origin = params.get('origin');
|
||||
const favIconUrl = params.get('favIconUrl');
|
||||
const hash = params.get('hash');
|
||||
const hostname = urlify(origin || '')?.hostname;
|
||||
const [error, showError] = useState('');
|
||||
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
|
||||
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.run_plugin_response,
|
||||
data: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onAccept = useCallback(async () => {
|
||||
if (!hash) return;
|
||||
try {
|
||||
const tab = await browser.tabs.create({
|
||||
active: true,
|
||||
});
|
||||
|
||||
await browser.storage.local.set({ plugin_hash: hash });
|
||||
|
||||
// @ts-ignore
|
||||
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.run_plugin_response,
|
||||
data: true,
|
||||
});
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
(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>
|
||||
);
|
||||
}
|
||||
@@ -94,6 +94,12 @@ export const useHistoryOrder = (): string[] => {
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useAllProofHistory = (): RequestHistory[] => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.history.order.map((id) => state.history.map[id]);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useRequestHistory = (id?: string): RequestHistory | undefined => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!id) return undefined;
|
||||
|
||||
@@ -18,6 +18,7 @@ enum ActionType {
|
||||
'/requests/setRequests' = '/requests/setRequests',
|
||||
'/requests/addRequest' = '/requests/addRequest',
|
||||
'/requests/setActiveTab' = '/requests/setActiveTab',
|
||||
'/requests/isConnected' = '/requests/isConnected',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
@@ -32,11 +33,22 @@ type State = {
|
||||
[requestId: string]: RequestLog;
|
||||
};
|
||||
activeTab: chrome.tabs.Tab | null;
|
||||
isConnected: boolean;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
map: {},
|
||||
activeTab: null,
|
||||
isConnected: false,
|
||||
};
|
||||
|
||||
export const setConnection = (isConnected: boolean): Action<boolean> => ({
|
||||
type: ActionType['/requests/isConnected'],
|
||||
payload: isConnected,
|
||||
});
|
||||
|
||||
export const isConnected = (isConnected: boolean) => async () => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
|
||||
@@ -113,6 +125,11 @@ export default function requests(
|
||||
[action.payload.requestId]: action.payload,
|
||||
},
|
||||
};
|
||||
case ActionType['/requests/isConnected']:
|
||||
return {
|
||||
...state,
|
||||
isConnected: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -142,3 +159,7 @@ export const useActiveTabUrl = (): URL | null => {
|
||||
return activeTab?.url ? new URL(activeTab.url) : null;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useIsConnected = (): boolean => {
|
||||
return useSelector((state: AppRootState) => state.requests.isConnected);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,3 @@ export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.5';
|
||||
export const NOTARY_PROXY = 'wss://notary.pse.dev/proxy';
|
||||
export const MAX_RECV = 16384;
|
||||
export const MAX_SENT = 4096;
|
||||
export const LOGGING_LEVEL_NONE = ' ';
|
||||
export const LOGGING_LEVEL_INFO = 'info,tlsn_extension_rs=info';
|
||||
export const LOGGING_LEVEL_DEBUG = 'debug,tlsn_extension_rs=debug';
|
||||
export const LOGGING_LEVEL_TRACE = 'trace,tlsn_extension_rs=trace';
|
||||
|
||||
@@ -4,14 +4,16 @@ import {
|
||||
RequestLog,
|
||||
} from '../entries/Background/rpc';
|
||||
import { EXPLORER_API } from './constants';
|
||||
import createPlugin, { CallContext, Plugin } from '@extism/extism';
|
||||
import createPlugin, {
|
||||
CallContext,
|
||||
ExtismPluginOptions,
|
||||
Plugin,
|
||||
} from '@extism/extism';
|
||||
import browser from 'webextension-polyfill';
|
||||
import NodeCache from 'node-cache';
|
||||
import {
|
||||
getCookieStoreByHost,
|
||||
getHeaderStoreByHost,
|
||||
} from '../entries/Background/cache';
|
||||
import { getNotaryApi, getProxyApi } from './storage';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
|
||||
|
||||
const charwise = require('charwise');
|
||||
|
||||
@@ -151,7 +153,7 @@ export const makePlugin = async (
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
const injectedConfig = {
|
||||
tabUrl: tab?.url,
|
||||
tabUrl: tab?.url || 'x://x',
|
||||
tabId: tab?.id,
|
||||
};
|
||||
|
||||
@@ -175,7 +177,8 @@ export const makePlugin = async (
|
||||
|
||||
if (
|
||||
!approvedRequests.find(
|
||||
({ method, url }) => method === params.method && url === params.url,
|
||||
({ method, url }) =>
|
||||
method === params.method && minimatch(params.url, url),
|
||||
)
|
||||
) {
|
||||
throw new Error(`Unapproved request - ${params.method}: ${params.url}`);
|
||||
@@ -195,13 +198,37 @@ export const makePlugin = async (
|
||||
throw new Error(`Unapproved proxy: ${params.websocketProxyUrl}`);
|
||||
}
|
||||
|
||||
handleExecPluginProver({
|
||||
type: BackgroundActiontype.execute_plugin_prover,
|
||||
data: {
|
||||
...params,
|
||||
now,
|
||||
},
|
||||
});
|
||||
(async () => {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
getSecretResponse,
|
||||
body: reqBody,
|
||||
} = params;
|
||||
let secretResps;
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: reqBody,
|
||||
});
|
||||
const body = await extractBodyFromResponse(resp);
|
||||
|
||||
if (getSecretResponse) {
|
||||
const out = await plugin.call(getSecretResponse, body);
|
||||
secretResps = JSON.parse(out.string());
|
||||
}
|
||||
|
||||
handleExecPluginProver({
|
||||
type: BackgroundActiontype.execute_plugin_prover,
|
||||
data: {
|
||||
...params,
|
||||
body: reqBody,
|
||||
secretResps,
|
||||
now,
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
return context.store(`${id}`);
|
||||
},
|
||||
@@ -226,8 +253,8 @@ export const makePlugin = async (
|
||||
if (config?.cookies) {
|
||||
const cookies: { [hostname: string]: { [key: string]: string } } = {};
|
||||
for (const host of config.cookies) {
|
||||
const cache = getCookieStoreByHost(host);
|
||||
cookies[host] = cacheToMap(cache);
|
||||
const cache = await getCookiesByHost(host);
|
||||
cookies[host] = cache;
|
||||
}
|
||||
// @ts-ignore
|
||||
injectedConfig.cookies = JSON.stringify(cookies);
|
||||
@@ -236,20 +263,22 @@ export const makePlugin = async (
|
||||
if (config?.headers) {
|
||||
const headers: { [hostname: string]: { [key: string]: string } } = {};
|
||||
for (const host of config.headers) {
|
||||
const cache = getHeaderStoreByHost(host);
|
||||
headers[host] = cacheToMap(cache);
|
||||
const cache = await getHeadersByHost(host);
|
||||
headers[host] = cache;
|
||||
}
|
||||
// @ts-ignore
|
||||
injectedConfig.headers = JSON.stringify(headers);
|
||||
}
|
||||
|
||||
const pluginConfig = {
|
||||
const pluginConfig: ExtismPluginOptions = {
|
||||
useWasi: true,
|
||||
config: injectedConfig,
|
||||
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
|
||||
functions: {
|
||||
'extism:host/user': funcs,
|
||||
},
|
||||
};
|
||||
|
||||
const plugin = await createPlugin(module, pluginConfig);
|
||||
return plugin;
|
||||
};
|
||||
@@ -275,6 +304,11 @@ export type PluginConfig = {
|
||||
proxyUrls?: string[];
|
||||
};
|
||||
|
||||
export type PluginMetadata = {
|
||||
origin: string;
|
||||
filePath: string;
|
||||
} & { [k: string]: string };
|
||||
|
||||
export const getPluginConfig = async (
|
||||
data: Plugin | ArrayBuffer,
|
||||
): Promise<PluginConfig> => {
|
||||
@@ -348,3 +382,11 @@ export const cacheToMap = (cache: NodeCache) => {
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export function safeParseJSON(data?: string | null) {
|
||||
try {
|
||||
return JSON.parse(data!);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
87
src/utils/plugins.tsx
Normal file
87
src/utils/plugins.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { PluginConfig } from './misc';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import Icon from '../components/Icon';
|
||||
|
||||
export const HostFunctionsDescriptions: {
|
||||
[key: string]: (pluginContent: PluginConfig) => ReactElement;
|
||||
} = {
|
||||
redirect: () => {
|
||||
return (
|
||||
<PermissionDescription fa="fa-solid fa-diamond-turn-right">
|
||||
<span>Redirect your current tab to any URL</span>
|
||||
</PermissionDescription>
|
||||
);
|
||||
},
|
||||
notarize: ({ notaryUrls, proxyUrls }) => {
|
||||
const notaries = ['default notary'].concat(notaryUrls || []);
|
||||
const proxies = ['default proxy'].concat(proxyUrls || []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PermissionDescription fa="fa-solid fa-route">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Proxy notarization requests thru</span>
|
||||
<MultipleParts parts={proxies} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
<PermissionDescription fa="fa-solid fa-stamp">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Submit notarization requests to</span>
|
||||
<MultipleParts parts={notaries} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export function PermissionDescription({
|
||||
fa,
|
||||
children,
|
||||
}: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="flex flex-row gap-4 items-start cursor-default">
|
||||
<Icon className="" size={1.6125} fa={fa} />
|
||||
<div className="text-sm mt-[0.125rem]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MultipleParts({ parts }: { parts: string[] }): ReactElement {
|
||||
const content = [];
|
||||
|
||||
if (parts.length > 1) {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
content.push(
|
||||
<span key={i} className="text-blue-600">
|
||||
{parts[i]}
|
||||
</span>,
|
||||
);
|
||||
|
||||
if (parts.length - i === 2) {
|
||||
content.push(
|
||||
<span key={i + 'separator'} className="inline-block mx-1">
|
||||
and
|
||||
</span>,
|
||||
);
|
||||
} else if (parts.length - i > 1) {
|
||||
content.push(
|
||||
<span key={i + 'separator'} className="inline-block mr-1">
|
||||
,
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content.push(
|
||||
<span key={0} className="text-blue-600">
|
||||
{parts[0]}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return <>{content}</>;
|
||||
}
|
||||
17
src/utils/promise.ts
Normal file
17
src/utils/promise.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const deferredPromise = (): {
|
||||
promise: Promise<never>;
|
||||
resolve: (data?: any) => void;
|
||||
reject: (reason?: any) => void;
|
||||
} => {
|
||||
let resolve: (data?: any) => void, reject: (reason?: any) => void;
|
||||
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
export type PromiseResolvers = ReturnType<typeof deferredPromise>;
|
||||
@@ -2,20 +2,6 @@ import browser from 'webextension-polyfill';
|
||||
import { BackgroundActiontype } from '../entries/Background/rpc';
|
||||
import { PluginConfig } from './misc';
|
||||
|
||||
export async function getCookiesByHost(hostname: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_cookies_by_hostname,
|
||||
data: hostname,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHeadersByHost(hostname: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_headers_by_hostname,
|
||||
data: hostname,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addPlugin(hex: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.add_plugin,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LOGGING_LEVEL_INFO } from './constants';
|
||||
import { LoggingLevel } from 'tlsn-js';
|
||||
|
||||
export const NOTARY_API_LS_KEY = 'notary-api';
|
||||
export const PROXY_API_LS_KEY = 'proxy-api';
|
||||
export const MAX_SENT_LS_KEY = 'max-sent';
|
||||
export const MAX_RECEIVED_LS_KEY = 'max-received';
|
||||
export const LOGGING_FILTER_KEY = 'logging-filter';
|
||||
export const LOGGING_FILTER_KEY = 'logging-filter-2';
|
||||
|
||||
export async function set(key: string, value: string) {
|
||||
return chrome.storage.sync.set({ [key]: value });
|
||||
@@ -14,7 +14,7 @@ export async function get(key: string, defaultValue?: string) {
|
||||
return chrome.storage.sync
|
||||
.get(key)
|
||||
.then((json: any) => json[key] || defaultValue)
|
||||
.catch(() => '');
|
||||
.catch(() => defaultValue);
|
||||
}
|
||||
|
||||
export async function getMaxSent() {
|
||||
@@ -26,13 +26,13 @@ export async function getMaxRecv() {
|
||||
}
|
||||
|
||||
export async function getNotaryApi() {
|
||||
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.5');
|
||||
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.6');
|
||||
}
|
||||
|
||||
export async function getProxyApi() {
|
||||
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
|
||||
}
|
||||
|
||||
export async function getLoggingFilter() {
|
||||
return await get(LOGGING_FILTER_KEY, LOGGING_LEVEL_INFO);
|
||||
export async function getLoggingFilter(): Promise<LoggingLevel> {
|
||||
return await get(LOGGING_FILTER_KEY, 'Info');
|
||||
}
|
||||
|
||||
18
src/utils/types.ts
Normal file
18
src/utils/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type Proof = ProofV0 | ProofV1;
|
||||
|
||||
export type ProofV0 = {
|
||||
version?: undefined;
|
||||
session: any;
|
||||
substrings: any;
|
||||
notaryUrl: string;
|
||||
};
|
||||
|
||||
export type ProofV1 = {
|
||||
version: '1.0';
|
||||
data: string;
|
||||
meta: {
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
pluginUrl?: string;
|
||||
};
|
||||
};
|
||||
@@ -40,7 +40,8 @@ var options = {
|
||||
mode: process.env.NODE_ENV || "development",
|
||||
ignoreWarnings: [
|
||||
/Circular dependency between chunks with runtime/,
|
||||
/ResizeObserver loop completed with undelivered notifications/
|
||||
/ResizeObserver loop completed with undelivered notifications/,
|
||||
/Should not import the named export/,
|
||||
],
|
||||
|
||||
entry: {
|
||||
@@ -48,6 +49,7 @@ var options = {
|
||||
popup: path.join(__dirname, "src", "entries", "Popup", "index.tsx"),
|
||||
background: path.join(__dirname, "src", "entries", "Background", "index.ts"),
|
||||
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
|
||||
content: path.join(__dirname, "src", "entries", "Content", "content.ts"),
|
||||
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
|
||||
sidePanel: path.join(__dirname, "src", "entries", "SidePanel", "index.tsx"),
|
||||
},
|
||||
@@ -198,31 +200,21 @@ var options = {
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/7.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/250.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/278.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
// {
|
||||
// from: "node_modules/tlsn-js/build/349.js",
|
||||
// to: path.join(__dirname, "build"),
|
||||
// force: true,
|
||||
// },
|
||||
{
|
||||
from: "node_modules/tlsn-js/build",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: "src/assets/plugins/discord_dm.wasm",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: "src/assets/plugins/twitter_profile.wasm",
|
||||
to: path.join(__dirname, "build"),
|
||||
force: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
|
||||
Reference in New Issue
Block a user