Compare commits

..

3 Commits

Author SHA1 Message Date
Codetrauma
104457d2c6 feat: pass verifier and notary api urls to prover 2025-05-20 14:43:24 +02:00
Codetrauma
d20441b553 fix: wip 2025-05-20 11:10:17 +02:00
Codetrauma
f955ab7fd5 feat: making changes to runPlugins with the interactive verifier 2025-05-20 11:03:14 +02:00
21 changed files with 1147 additions and 1725 deletions

View File

@@ -49,7 +49,7 @@ jobs:
uses: actions/download-artifact@v4
with:
name: tlsn-extension-${{ github.ref_name }}.zip
path: .
path: ./tlsn-extension-${{ github.ref_name }}.zip
- name: 📦 Add extension zip file to release
env:

View File

@@ -12,7 +12,7 @@
# Chrome Extension (MV3) for TLSNotary
> [!IMPORTANT]
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/main/crates/notary/server), please ensure that the server's version is the same as the version of this extension
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
## License
This repository is licensed under either of

1902
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-extension",
"version": "0.1.0.1200",
"version": "0.1.0.1000",
"license": "MIT",
"repository": {
"type": "git",
@@ -40,7 +40,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.12.0"
"tlsn-js": "0.1.0-alpha.10.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -94,4 +94,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

View File

@@ -5,12 +5,7 @@ import React, {
useEffect,
useState,
} from 'react';
import {
fetchPluginHashes,
removePlugin,
runPlugin,
addPlugin,
} from '../../utils/rpc';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import {
getPluginConfig,
@@ -36,78 +31,20 @@ export function PluginList({
className,
unremovable,
onClick,
showAddButton = false,
}: {
className?: string;
unremovable?: boolean;
onClick?: (hash: string) => void;
showAddButton?: boolean;
}): ReactElement {
const hashes = usePluginHashes();
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchPluginHashes();
}, []);
const handleFileUpload = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.wasm')) {
alert('Please select a .wasm file');
return;
}
setUploading(true);
try {
const arrayBuffer = await file.arrayBuffer();
const hex = Buffer.from(arrayBuffer).toString('hex');
const url = `file://${file.name}`;
await addPlugin(hex, url);
await fetchPluginHashes();
} catch (error: any) {
alert(`Failed to add plugin: ${error.message}`);
} finally {
setUploading(false);
e.target.value = '';
}
},
[],
);
return (
<div className={classNames('flex flex-col flex-nowrap gap-1', className)}>
{showAddButton && (
<div className="relative">
<input
type="file"
accept=".wasm"
onChange={handleFileUpload}
disabled={uploading}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<button
className="flex flex-row items-center justify-center gap-2 p-3 border-2 border-dashed border-slate-300 rounded-lg text-slate-500 hover:text-slate-700 hover:border-slate-400 transition-colors cursor-pointer w-full"
disabled={uploading}
>
{uploading ? (
<>
<Icon fa="fa-solid fa-spinner" className="animate-spin" />
<span>Adding Plugin...</span>
</>
) : (
<>
<Icon fa="fa-solid fa-plus" />
<span>Add Plugin (.wasm file)</span>
</>
)}
</button>
</div>
)}
{!hashes.length && !showAddButton && (
{!hashes.length && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>

View File

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

View File

@@ -1,12 +1,6 @@
import { Level } from 'level';
import { AbstractSublevel } from 'abstract-level';
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
import {
RequestHistory,
RequestLog,
RequestProgress,
UpsertRequestLog,
} from './rpc';
import { RequestHistory, RequestProgress } from './rpc';
import mutex from './mutex';
import { minimatch } from 'minimatch';
const charwise = require('charwise');
@@ -29,6 +23,12 @@ const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
});
const cookiesDb = db.sublevel<string, boolean>('cookies', {
valueEncoding: 'json',
});
const headersDb = db.sublevel<string, boolean>('headers', {
valueEncoding: 'json',
});
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
valueEncoding: 'json',
});
@@ -38,81 +38,10 @@ const sessionStorageDb = db.sublevel<string, any>('localStorage', {
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
const requestDb = db.sublevel<string, any>('requests', {
valueEncoding: 'json',
});
enum AppDatabaseKey {
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
}
export async function upsertRequestLog(request: UpsertRequestLog) {
const existing = await getRequestLog(request.requestId);
if (existing) {
await requestDb.put(request.requestId, {
...existing,
...request,
});
} else if (request.url) {
const host = urlify(request.url)?.host;
if (host) {
await requestDb.put(request.requestId, request);
await requestDb
.sublevel(request.tabId.toString())
.put(request.requestId, '');
await requestDb.sublevel(host).put(request.requestId, '');
}
}
}
export async function getRequestLog(
requestId: string,
): Promise<RequestLog | null> {
return requestDb.get(requestId).catch(() => null);
}
export async function removeRequestLog(requestId: string) {
const existing = await getRequestLog(requestId);
if (existing) {
await requestDb.del(requestId);
await requestDb.sublevel(existing.tabId.toString()).del(requestId);
const host = urlify(existing.url)?.host;
if (host) {
await requestDb.sublevel(host).del(requestId);
}
}
}
export async function removeRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
for await (const [requestId] of requests.iterator()) {
await removeRequestLog(requestId);
}
}
export async function getRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function getRequestLogsByHost(host: string) {
const requests = requestDb.sublevel(host);
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function clearAllRequestLogs() {
await requestDb.clear();
}
export async function addNotaryRequest(
now = Date.now(),
request: Omit<RequestHistory, 'status' | 'id'>,
@@ -410,43 +339,48 @@ export async function setConnection(origin: string) {
return true;
}
export async function getCookiesByHost(linkOrHost: string) {
export async function setCookies(host: string, name: string, value: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearCookies(host: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).clear();
return true;
});
}
export async function getCookies(link: string, name: string) {
try {
const existing = await cookiesDb.sublevel(link).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(link: string) {
const ret: { [key: string]: string } = {};
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
const links: { [k: string]: boolean } = {};
const url = urlify(link);
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
for await (const sublevel of cookiesDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
}
if (!filteredRequest) return ret;
const cookieLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() === 'cookie') {
header.value?.split(';').forEach((cookie) => {
const [name, value] = cookie.split('=');
ret[name.trim()] = value.trim();
});
}
if (!cookieLink) return ret;
for await (const [key, value] of cookiesDb.sublevel(cookieLink).iterator()) {
ret[key] = value;
}
return ret;
}
@@ -466,40 +400,49 @@ export async function getConnection(origin: string) {
return null;
}
}
export async function getHeadersByHost(linkOrHost: string) {
export async function setHeaders(link: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(link).put(name, value);
return true;
});
}
export async function clearHeaders(host: string) {
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).clear();
return true;
});
}
export async function getHeaders(host: string, name: string) {
try {
const existing = await headersDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getHeadersByHost(link: string) {
const ret: { [key: string]: string } = {};
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
const url = urlify(link);
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
const links: { [k: string]: boolean } = {};
for await (const sublevel of headersDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
}
if (!filteredRequest) return ret;
const headerLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() !== 'cookie') {
ret[header.name] = header.value || '';
}
if (!headerLink) return ret;
for await (const [key, value] of headersDb.sublevel(headerLink).iterator()) {
ret[key] = value;
}
return ret;
}
@@ -572,62 +515,3 @@ export async function getAppState() {
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
};
}
export async function resetDB() {
return mutex.runExclusive(async () => {
return Promise.all([
localStorageDb.clear(),
sessionStorageDb.clear(),
requestDb.clear(),
]);
});
}
export async function getDBSizeByRoot(
rootDB: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of rootDB.keys({ keyEncoding: 'utf8' })) {
const link = sublevel.split('!')[1];
const sub = rootDB.sublevel(link);
for await (const [key, value] of sub.iterator()) {
size += key.length + value.length;
}
}
resolve(size);
});
}
export async function getRecursiveDBSize(
db: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of db.keys({ keyEncoding: 'utf8' })) {
const parts = sublevel.split('!');
if (parts.length === 1) {
const value = await db.get(parts[0]);
size += parts[0].length + (value ? JSON.stringify(value).length : 0);
} else {
const sub = db.sublevel(parts[1]);
size +=
(await getRecursiveDBSize(
sub as unknown as AbstractSublevel<Level, any, any, any>,
)) + parts[1].length;
}
}
resolve(size);
});
}
export async function getDBSize(): Promise<number> {
const sizes = await Promise.all([
getDBSizeByRoot(localStorageDb),
getDBSizeByRoot(sessionStorageDb),
getRecursiveDBSize(requestDb),
]);
return sizes.reduce((a, b) => a + b, 0);
}

View File

@@ -1,10 +1,10 @@
import { BackgroundActiontype } from './rpc';
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 { getRequestLog, upsertRequestLog } from './db';
import { getHeadersByHost, setCookies, setHeaders } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
) => {
@@ -12,22 +12,40 @@ export const onSendHeaders = (
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const { origin, pathname } = urlify(details.url) || {};
const link = [origin, pathname].join('');
if (link && details.requestHeaders) {
upsertRequestLog({
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
updatedAt: Date.now(),
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
value.split(';').forEach((cookieStr) => {
const index = cookieStr.indexOf('=');
if (index !== -1) {
const cookieName = cookieStr.slice(0, index).trim();
const cookieValue = cookieStr.slice(index + 1);
setCookies(link, cookieName, cookieValue);
}
});
} else {
setHeaders(link, name, value);
}
});
}
cache.set(requestId, {
...existing,
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
});
}
});
};
@@ -41,30 +59,24 @@ export const onBeforeRequest = (
if (method === 'OPTIONS') return;
if (requestBody) {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
if (requestBody.raw && requestBody.raw[0]?.bytes) {
try {
await upsertRequestLog({
cache.set(requestId, {
...existing,
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
'utf-8',
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
});
} catch (e) {
console.error(e);
}
} else if (requestBody.formData) {
await upsertRequestLog({
formData: Object.fromEntries(
Object.entries(requestBody.formData).map(([key, value]) => [
key,
Array.isArray(value) ? value : [value],
]),
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
cache.set(requestId, {
...existing,
formData: requestBody.formData,
});
}
}
@@ -79,7 +91,12 @@ export const onResponseStarted = (
if (method === 'OPTIONS') return;
await upsertRequestLog({
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],
...existing,
method: details.method,
type: details.type,
url: details.url,
@@ -87,15 +104,9 @@ export const onResponseStarted = (
tabId: tabId,
requestId: requestId,
responseHeaders,
updatedAt: Date.now(),
});
};
const newLog = await getRequestLog(requestId);
if (!newLog) {
console.error('Request log not found', requestId);
return;
}
cache.set(requestId, newLog);
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,

View File

@@ -1,11 +1,7 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import {
getAppState,
removePlugin,
removeRequestLogsByTabId,
setDefaultPluginsInstalled,
} from './db';
import { getAppState, removePlugin, setDefaultPluginsInstalled } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
@@ -34,7 +30,7 @@ import { installPlugin } from './plugins/utils';
);
browser.tabs.onRemoved.addListener((tabId) => {
removeRequestLogsByTabId(tabId);
deleteCacheByTabId(tabId);
});
const { defaultPluginsInstalled } = await getAppState();

View File

@@ -1,4 +1,5 @@
import browser from 'webextension-polyfill';
import { clearCache, getCacheByTabId } from './cache';
import { addRequestHistory, setRequests } from '../../reducers/history';
import {
addNotaryRequest,
@@ -23,8 +24,6 @@ import {
setLocalStorage,
setSessionStorage,
setNotaryRequestProgress,
getRequestLogsByTabId,
clearAllRequestLogs,
} from './db';
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
import {
@@ -55,7 +54,6 @@ import {
sendMessage,
sendPairedMessage,
} from './ws';
import { parseHttpMessage } from '../../utils/parser';
import { mapStringToRange, subtractRanges } from 'tlsn-js';
import { PresentationJSON } from 'tlsn-js/build/types';
@@ -143,23 +141,6 @@ export type RequestLog = {
[k: string]: string[];
};
responseHeaders?: browser.WebRequest.HttpHeaders;
updatedAt: number;
};
export type UpsertRequestLog = {
requestId: string;
tabId: number;
method?: string;
type?: string;
url?: string;
initiator?: string | null;
requestHeaders?: browser.WebRequest.HttpHeaders;
requestBody?: string;
formData?: {
[k: string]: string[];
};
responseHeaders?: browser.WebRequest.HttpHeaders;
updatedAt: number;
};
export enum RequestProgress {
@@ -231,7 +212,7 @@ export const initRPC = () => {
case BackgroundActiontype.get_requests:
return handleGetRequests(request, sendResponse);
case BackgroundActiontype.clear_requests:
clearAllRequestLogs().then(() => pushToRedux(setRequests([])));
clearCache();
return sendResponse();
case BackgroundActiontype.get_prove_requests:
return handleGetProveRequests(request, sendResponse);
@@ -356,7 +337,6 @@ export const initRPC = () => {
pluginHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.get_p2p_state:
getP2PState();
return;
@@ -371,7 +351,10 @@ function handleGetRequests(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
getRequestLogsByTabId(request.data).then(sendResponse);
const cache = getCacheByTabId(request.data);
const keys = cache.keys() || [];
const data = keys.map((key) => cache.get(key));
sendResponse(data);
return true;
}
@@ -1049,17 +1032,53 @@ async function handleRunPluginByURLRequest(request: BackgroundAction) {
});
const defer = deferredPromise();
const { origin, position, url, params } = request.data;
const {
origin,
position,
url,
params,
skipConfirmation,
verifierApiUrl,
proxyApiUrl,
headers,
} = request.data;
if (skipConfirmation) {
try {
browser.runtime.onMessage.addListener(async (req: any) => {
if (req.type !== SidePanelActionTypes.execute_plugin_response) return;
if (req.data.url !== url) return;
if (req.data.error) defer.reject(req.data.error);
if (req.data.proof) defer.resolve(req.data.proof);
});
const now = Date.now();
const id = charwise.encode(now).toString('hex');
await browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_request,
data: {
id,
url,
params,
headers,
verifierApiUrl,
proxyApiUrl,
maxRecvData: await getMaxRecv(),
maxSentData: await getMaxSent(),
},
});
return defer.promise;
} catch (error) {
defer.reject(error);
return defer.promise;
}
}
// 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?url=${url}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}&params=${encodeURIComponent(JSON.stringify(params) || '')}`,
position.left,

View File

@@ -69,7 +69,6 @@ export const connectSession = async () => {
if (state.socket) return;
const rendezvousAPI = await getRendezvousApi();
const socket = new WebSocket(rendezvousAPI);
socket.onopen = () => {
@@ -317,26 +316,9 @@ export const connectSession = async () => {
break;
}
};
socket.onerror = (error) => {
console.error('Error connecting to websocket:', error);
socket.onerror = () => {
console.error('Error connecting to websocket');
pushToRedux(setConnected(false));
pushToRedux(
setP2PError(
'Failed to connect to rendezvous server. Please check your connection and server URL.',
),
);
};
socket.onclose = (event) => {
console.log('WebSocket connection closed:', event.code, event.reason);
pushToRedux(setConnected(false));
if (event.code !== 1000 && event.code !== 1001) {
pushToRedux(
setP2PError(
`WebSocket connection lost: ${event.reason || 'Unknown error'}`,
),
);
}
};
};
@@ -454,8 +436,8 @@ export const endProofRequest = async (data: {
proof: VerifierOutput;
}) => {
const transcript = new Transcript({
sent: data.proof.transcript?.sent || [],
recv: data.proof.transcript?.recv || [],
sent: data.proof.transcript.sent,
recv: data.proof.transcript.recv,
});
state.presentation = {

View File

@@ -1,5 +1,5 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { PresentationJSON } from 'tlsn-js/build/types';
import { PresentationJSON } from '../../utils/types';
const client = new RPCClient();
@@ -44,6 +44,27 @@ class TLSN {
return resp;
}
async runPluginWithVerifier(
url: string,
verifierOptions: {
verifierApiUrl: string;
proxyApiUrl: string;
headers?: { [key: string]: string };
},
params?: Record<string, string>,
) {
const resp = await client.call(ContentScriptTypes.run_plugin_by_url, {
url,
params,
skipConfirmation: true, // Signal to skip confirmation window
verifierApiUrl: verifierOptions.verifierApiUrl,
proxyApiUrl: verifierOptions.proxyApiUrl,
headers: verifierOptions.headers,
});
return resp;
}
}
const connect = async () => {

View File

@@ -1,5 +1,14 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise';
export interface RunPluginByUrlData {
url: string;
params?: Record<string, string>;
skipConfirmation?: boolean;
verifierApiUrl?: string;
proxyApiUrl?: string;
headers?: { [key: string]: string };
}
export enum ContentScriptTypes {
notarize = 'tlsn/cs/notarize',
run_plugin_by_url = 'tlsn/cs/run_plugin_by_url',

View File

@@ -16,13 +16,15 @@ import {
} from 'tlsn-js';
import { convertNotaryWsToHttp, devlog, urlify } from '../../utils/misc';
import * as Comlink from 'comlink';
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
import { OffscreenActionTypes } from './types';
import { PresentationJSON } from 'tlsn-js/build/types';
import { PresentationJSON } from '../../utils/types';
import { waitForEvent } from '../utils';
import {
setNotaryRequestError,
setNotaryRequestStatus,
} from '../Background/db';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
const { init, Prover, Presentation, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
@@ -124,7 +126,7 @@ export const onCreatePresentationRequest = async (request: any) => {
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: { ...commit, server_identity: false },
reveal: commit,
})) as TPresentation;
const json = await presentation.json();
browser.runtime.sendMessage({
@@ -133,6 +135,11 @@ export const onCreatePresentationRequest = async (request: any) => {
id,
proof: {
...json,
meta: {
...json.meta,
notaryUrl,
websocketProxyUrl,
},
},
},
});
@@ -296,7 +303,6 @@ export const startP2PProver = async (request: any) => {
},
});
await proofRequestStart;
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
@@ -345,7 +351,7 @@ export const startP2PProver = async (request: any) => {
};
const endRequest = waitForEvent(OffscreenActionTypes.end_p2p_proof_request);
await prover.reveal({ ...commit, server_identity: false });
await prover.reveal(commit);
await endRequest;
};
@@ -363,7 +369,7 @@ async function createProof(options: {
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<PresentationJSON> {
}): Promise<PresentationJSONa7> {
const {
url,
method = 'GET',
@@ -395,18 +401,13 @@ async function createProof(options: {
updateRequestProgress(id, RequestProgress.SettingUpProver);
await prover.setup(sessionUrl);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
updateRequestProgress(id, RequestProgress.SendingRequest);
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
updateRequestProgress(id, RequestProgress.ReadingTranscript);
const transcript = await prover.transcript();
@@ -436,19 +437,25 @@ async function createProof(options: {
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: { ...commit, server_identity: false },
reveal: commit,
})) as TPresentation;
const json = await presentation.json();
return {
...json,
meta: {
...json,
notaryUrl: notaryUrl,
websocketProxyUrl: websocketProxyUrl,
},
};
}
async function createProver(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
verifierApiUrl?: string;
proxyApiUrl?: string;
notaryUrl?: string;
method?: Method;
headers?: {
[name: string]: string;
@@ -465,13 +472,13 @@ async function createProver(options: {
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
verifierApiUrl,
proxyApiUrl,
id,
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
try {
const prover: TProver = await handleProgress(
id,
@@ -489,8 +496,17 @@ async function createProver(options: {
const sessionUrl = await handleProgress(
id,
RequestProgress.GettingSession,
() => notary.sessionUrl(maxSentData, maxRecvData),
'Error getting session from Notary',
async () => {
if (verifierApiUrl) {
return verifierApiUrl;
} else {
const notary = NotaryServer.from(
options.notaryUrl || (await getNotaryApi()),
);
return notary.sessionUrl(maxSentData, maxRecvData);
}
},
'Error getting session from Verifier/Notary',
);
await handleProgress(
@@ -500,17 +516,19 @@ async function createProver(options: {
'Error setting up prover',
);
const proxyUrl = proxyApiUrl || (await getProxyApi());
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
prover.sendRequest(proxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
'Error sending request',
);
return prover;
@@ -533,29 +551,32 @@ async function verifyProof(proof: PresentationJSON): Promise<{
};
switch (proof.version) {
case '0.1.0-alpha.12':
result = await verify(proof);
break;
default:
case undefined:
case '0.1.0-alpha.7':
case '0.1.0-alpha.8':
case '0.1.0-alpha.9':
result = {
sent: 'version not supported',
recv: 'version not supported',
};
break;
case '0.1.0-alpha.10':
result = await verify(proof);
break;
}
return result!;
}
async function verify(proof: PresentationJSON) {
if (proof.version !== '0.1.0-alpha.12') {
if (proof.version !== '0.1.0-alpha.10') {
throw new Error('wrong version');
}
const presentation: TPresentation = await new Presentation(proof.data);
const verifierOutput = await presentation.verify();
const transcript = new Transcript({
sent: verifierOutput.transcript?.sent || [],
recv: verifierOutput.transcript?.recv || [],
sent: verifierOutput.transcript.sent,
recv: verifierOutput.transcript.recv,
});
const vk = await presentation.verifyingKey();
const verifyingKey = Buffer.from(vk.data).toString('hex');
@@ -594,75 +615,6 @@ function updateRequestProgress(
});
}
function getWebsocketErrorMessage(
lowerError: string,
fallbackMessage: string,
): string {
const isWebsocketError =
lowerError.includes('websocket') ||
lowerError.includes('proxy') ||
lowerError.includes('connection') ||
lowerError.includes('network') ||
lowerError.includes('prover error') ||
lowerError.includes('io error') ||
lowerError.includes('certificate') ||
lowerError.includes('cert') ||
lowerError.includes('ssl') ||
lowerError.includes('tls');
if (!isWebsocketError) {
return fallbackMessage;
}
const errorPatterns = [
{
patterns: ['protocol', 'must use ws://', 'must use wss://'],
message:
'Invalid websocket proxy URL protocol. Please use ws:// or wss:// protocol in your websocket proxy URL settings.',
},
{
patterns: [
'not allowed',
'not whitelisted',
'forbidden',
'unauthorized',
'permission denied',
'access denied',
],
message:
'Target domain not allowed by websocket proxy. Please check if the website domain is supported by your proxy service.',
},
{
patterns: ['dns', 'resolve'],
message:
'Cannot resolve websocket proxy domain. Please check your websocket proxy URL in settings.',
},
{
patterns: ['timeout'],
message:
'Websocket proxy connection timeout. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['refused', 'unreachable'],
message:
'Cannot reach websocket proxy server. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['cert', 'certificate', 'certnotvalidforname'],
message:
'Cannot connect to websocket proxy server. Please check your websocket proxy URL in settings and ensure it points to a valid websocket proxy service.',
},
];
for (const { patterns, message } of errorPatterns) {
if (patterns.some((pattern) => lowerError.includes(pattern))) {
return message;
}
}
return 'Websocket proxy connection failed. Please check your websocket proxy URL in settings and ensure the server is accessible.';
}
async function handleProgress<T>(
id: string,
progress: RequestProgress,
@@ -673,17 +625,12 @@ async function handleProgress<T>(
updateRequestProgress(id, progress);
return await action();
} catch (error: any) {
const specificError = error?.message || '';
const lowerError = specificError.toLowerCase();
const finalErrorMessage = getWebsocketErrorMessage(
lowerError,
errorMessage,
);
updateRequestProgress(id, RequestProgress.Error, finalErrorMessage);
updateRequestProgress(id, RequestProgress.Error, errorMessage);
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(id, finalErrorMessage);
await setNotaryRequestError(
id,
errorMessage || error.message || 'Unknown error',
);
throw error;
}
}

View File

@@ -44,9 +44,8 @@ export default function SidePanel(): ReactElement {
switch (type) {
case SidePanelActionTypes.execute_plugin_request: {
const pluginIdentifier = data.pluginUrl || data.pluginHash;
setConfig(await getPluginConfigByUrl(pluginIdentifier));
setUrl(pluginIdentifier);
setConfig(await getPluginConfigByUrl(data.pluginUrl));
setUrl(data.pluginUrl);
setParams(data.pluginParams);
setStarted(true);
break;
@@ -93,7 +92,6 @@ export default function SidePanel(): ReactElement {
</button>
</div>
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
url={url}
@@ -154,10 +152,7 @@ function PluginBody({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
error:
notaryRequest.errorMessage ||
notaryRequest.error ||
'Notarization failed',
error: notaryRequest.error,
},
});
}
@@ -253,7 +248,7 @@ function StepContent(
? JSON.stringify(lastResponse)
: JSON.stringify(parameterValues),
);
const val = JSON.parse(out!.string());
const val = JSON.parse(out.string());
if (val && prover) {
setNotarizationId(val);
} else {
@@ -262,7 +257,7 @@ function StepContent(
setResponse(val, index);
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unknown error');
setError(e?.message || 'Unkonwn error');
} finally {
setPending(false);
}
@@ -354,18 +349,12 @@ function StepContent(
btnContent = (
<div className="flex flex-col gap-2">
{notaryRequest?.progress === RequestProgress.Error && (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
<div className="flex flex-row items-center gap-2 text-red-600">
<Icon fa="fa-solid fa-triangle-exclamation" size={1} />
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
)}
{notaryRequest?.progress !== RequestProgress.Error && (
@@ -408,18 +397,7 @@ function StepContent(
{!!description && (
<div className="text-slate-500 text-sm">{description}</div>
)}
{!!error && (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<div className="text-red-500 text-sm">{error}</div>
</div>
</div>
)}
{!!error && <div className="text-red-500 text-sm">{error}</div>}
{btnContent}
</div>
</div>

View File

@@ -11,23 +11,17 @@ import History from '../History';
import './index.scss';
import Requests from '../Requests';
import { fetchPluginHashes } from '../../utils/rpc';
import { PluginList } from '../../components/PluginList';
import { getDeveloperMode } from '../../utils/storage';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network' | 'plugins'>(
props.tab || 'history',
);
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [developerMode, setDeveloperMode] = useState(false);
useEffect(() => {
fetchPluginHashes();
getDeveloperMode().then(setDeveloperMode);
}, []);
useEffect(() => {
@@ -77,24 +71,10 @@ export default function Home(props: {
>
History
</TabSelector>
{developerMode && (
<TabSelector
onClick={() => setTab('plugins')}
selected={tab === 'plugins'}
>
Plugins
</TabSelector>
)}
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && <Requests shouldFix={shouldFix} />}
{tab === 'plugins' && (
<PluginList
className="p-2 overflow-y-auto"
showAddButton={developerMode}
/>
)}
</div>
</div>
);

View File

@@ -19,8 +19,6 @@ import {
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
getDeveloperMode,
DEVELOPER_MODE_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -34,7 +32,6 @@ import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
import { LoggingLevel } from 'tlsn-js';
import { version } from '../../../package.json';
import { getDBSize, resetDB } from '../../entries/Background/db';
export default function Options(): ReactElement {
const [notary, setNotary] = useState(NOTARY_API);
@@ -43,28 +40,16 @@ export default function Options(): ReactElement {
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
const [developerMode, setDeveloperMode] = useState(false);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
const [advanced, setAdvanced] = useState(false);
const [showReloadModal, setShowReloadModal] = useState(false);
const [dbSize, setDbSize] = useState(0);
const [isCalculatingDbSize, setIsCalculatingDbSize] = useState(false);
useEffect(() => {
(async () => {
setIsCalculatingDbSize(true);
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
})();
}, []);
useEffect(() => {
(async () => {
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
setDeveloperMode(await getDeveloperMode());
})();
}, []);
@@ -89,7 +74,6 @@ export default function Options(): ReactElement {
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
await set(DEVELOPER_MODE_LS_KEY, developerMode.toString());
setDirty(false);
},
[
@@ -99,7 +83,6 @@ export default function Options(): ReactElement {
maxReceived,
loggingLevel,
rendezvous,
developerMode,
shouldReload,
],
);
@@ -120,13 +103,6 @@ export default function Options(): ReactElement {
browser.tabs.create({ url });
}, []);
const onCleanCache = useCallback(async () => {
setIsCalculatingDbSize(true);
await resetDB();
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
{showReloadModal && (
@@ -163,8 +139,6 @@ export default function Options(): ReactElement {
proxy={proxy}
setProxy={setProxy}
setDirty={setDirty}
developerMode={developerMode}
setDeveloperMode={setDeveloperMode}
/>
<div className="justify-left px-2 pt-3 gap-2">
<button className="font-bold" onClick={onAdvanced}>
@@ -218,15 +192,6 @@ export default function Options(): ReactElement {
>
Join our Discord
</button>
<button className="button" onClick={onCleanCache}>
<span>Clean Cache (</span>
{isCalculatingDbSize ? (
<i className="fa-solid fa-spinner fa-spin"></i>
) : (
<span>{(dbSize / 1024 / 1024).toFixed(2)} MB</span>
)}
<span>)</span>
</button>
</div>
</div>
);
@@ -263,18 +228,8 @@ function NormalOptions(props: {
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
developerMode: boolean;
setDeveloperMode: (value: boolean) => void;
}) {
const {
notary,
setNotary,
proxy,
setProxy,
setDirty,
developerMode,
setDeveloperMode,
} = props;
const { notary, setNotary, proxy, setProxy, setDirty } = props;
return (
<div>
@@ -306,33 +261,6 @@ function NormalOptions(props: {
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
</div>
<div className="flex flex-row items-center py-3 px-2 gap-2">
<div className="font-semibold">Developer Mode</div>
<div className="relative inline-block w-9 h-5">
<input
type="checkbox"
id="developer-mode"
checked={developerMode}
onChange={(e) => {
setDeveloperMode(e.target.checked);
setDirty(true);
}}
className="sr-only"
/>
<label
htmlFor="developer-mode"
className={`block h-5 rounded-full cursor-pointer transition-all duration-300 ease-in-out ${
developerMode ? 'bg-blue-500' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-sm transform transition-all duration-300 ease-in-out ${
developerMode ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</label>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -8,7 +8,6 @@ export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export const RENDEZVOUS_API_LS_KEY = 'rendezvous-api';
export const DEVELOPER_MODE_LS_KEY = 'developer-mode';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -44,8 +43,3 @@ export async function getLoggingFilter(): Promise<LoggingLevel> {
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}
export async function getDeveloperMode(): Promise<boolean> {
const value = await get(DEVELOPER_MODE_LS_KEY, 'false');
return value === 'true';
}

10
src/utils/types.ts Normal file
View File

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

View File

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