Interactive verifier demo (#35)

* Add interactive verifier demo
* Serve and load plugin from local file
* Show screenname at the end + removed attestation code
* No more need for tlsn-js
* Updated Docker container
* Use environment variable to enable/disable POAPs
* Improved UI
* use LRU for tracking sessions

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
This commit is contained in:
tsukino
2025-08-26 02:16:25 -07:00
committed by GitHub
parent 87e1723628
commit 3f89b56fcc
32 changed files with 2014 additions and 3087 deletions

View File

@@ -37,7 +37,10 @@ export default function Button(props: Props): ReactElement {
{...btnProps}
>
{loading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
<>
<span>Running TLSNotary plugin...</span>
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
</>
) : (
children
)}

View File

@@ -5,10 +5,9 @@ import Step from '@mui/material/Step';
import Box from '@mui/material/Box';
import StepLabel from '@mui/material/StepLabel';
import classNames from 'classnames';
import type { PresentationJSON } from 'tlsn-js/build/types';
import Button from '../Button';
import ConfettiExplosion, { ConfettiProps } from 'react-confetti-explosion';
import { formatDataPreview } from '../../utils/utils';
import OverviewSvg from '../../../static/overview_prover_verifier.svg';
const steps = ['Connect Extension', 'Run Plugin'];
@@ -16,9 +15,8 @@ export default function Steps(): ReactElement {
const [extensionInstalled, setExtensionInstalled] = useState(false);
const [step, setStep] = useState<number>(0);
const [client, setClient] = useState<any>(null);
const [pluginData, setPluginData] = useState<PresentationJSON | null>(null);
const [sessionId, setSessionId] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [transcript, setTranscript] = useState<any>(null);
const [screenName, setScreenName] = useState<string>('');
const [exploding, setExploding] = useState<boolean>(false);
@@ -53,17 +51,6 @@ export default function Steps(): ReactElement {
};
}, []);
useEffect(() => {
if (transcript) {
const match = transcript.recv.match(/"screen_name":"([^"]+)"/);
const screenName = match ? match[1] : null;
setScreenName(screenName);
if (screenName) {
setExploding(true);
}
}
}, [transcript]);
async function handleConnect() {
try {
//@ts-ignore
@@ -77,21 +64,24 @@ export default function Steps(): ReactElement {
async function handleRunPlugin() {
try {
setLoading(true);
const pluginData = await client.runPlugin(
'https://raw.githubusercontent.com/tlsnotary/tlsn-extension/cc3264f058ad2ebb0791830a1217fdd8bffd543f/src/assets/plugins/twitter_profile.wasm',
const _sessionId = await client.runPlugin(
window.location.origin + '/twitter_profile.tlsn.wasm',
);
setPluginData(pluginData);
console.log(pluginData);
const response = await fetch('/verify-attestation', {
setSessionId(_sessionId);
console.log('Session ID:', _sessionId);
const response = await fetch('/check-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ attestation: pluginData }),
body: JSON.stringify({ session_id: _sessionId }),
});
console.log('Check session response:', response);
if (response.status === 200) {
const data = await response.json();
setTranscript(data.presentationObj);
console.log('Response: Plugin data:', data);
setScreenName(data.screen_name);
setStep(1);
} else {
console.log(await response.text());
@@ -108,7 +98,7 @@ export default function Steps(): ReactElement {
{extensionInstalled ? (
<>
<div className="flex flex-row items-center gap-2 text-slate-600 font-bold pb-2">
Connected{' '}
Extension Connected{' '}
<div
className={classNames(
'rounded-full h-[10px] w-[10px] border-[2px]',
@@ -119,61 +109,197 @@ export default function Steps(): ReactElement {
)}
></div>
</div>
<Box className="w-full max-w-3xl mt-6 pb-4">
<Stepper activeStep={step} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
<div className="flex gap-3">
{step === 0 && (
<button onClick={handleConnect} className="button">
Connect
</button>
)}
{step === 1 && !pluginData && (
<div className="flex flex-col items-center justify-center gap-2">
<ul className="flex flex-col items-center justify-center gap-1">
<li className="text-base font-light">
This will open a new tab to Twitter/X and the sidebar for
the extension
</li>
<li className="text-base font-light">
Click through the steps in the sidebar
</li>
<li className="text-base font-light">
Don't close the sidebar or refresh the page until
notarization is finished
</li>
<li className="text-base font-light">
If successful, the attestation and verified data will be
displayed below
</li>
</ul>
<Button onClick={handleRunPlugin} loading={loading}>
Run Plugin
</Button>
{step === 1 && !sessionId && (
<div className="flex flex-col items-center justify-center gap-6 max-w-4xl">
<div className="text-center space-y-4 w-full flex flex-col items-center">
<h2 className="text-2xl font-semibold text-gray-900">
Ready to Prove Your Twitter Identity
</h2>
<p className="text-lg text-gray-600 max-w-2xl">
Click the button below to start the verification process
</p>
<Button
onClick={handleRunPlugin}
loading={loading}
className="bg-blue-600 hover:bg-blue-700 !text-white px-12 py-4 text-xl font-semibold min-w-[300px] shadow-lg rounded-lg transition-all duration-200"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
<span>Processing...</span>
</>
) : (
<>
🔐 Prove Twitter Screen Name
</>
)}
</Button>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 w-full">
<h3 className="text-lg font-semibold text-blue-900 mb-4 text-center">
What happens when you click "Prove Twitter Screen Name"?
</h3>
<div className="space-y-4 text-gray-700">
<div className="flex items-start gap-3">
<div className="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-semibold flex-shrink-0 mt-0.5">
1
</div>
<p className="text-base leading-relaxed">
The TLSNotary extension will open a popup, asking permission to run the plugin and send the unredacted data (just the screen name) to the verifier server.
</p>
</div>
<div className="flex items-start gap-3">
<div className="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-semibold flex-shrink-0 mt-0.5">
2
</div>
<div>
<p className="text-base leading-relaxed mb-2">
If you accept, the extension will open X/Twitter in a new tab with a sidebar showing these steps:
</p>
<div className="ml-4 space-y-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-300 rounded-full"></div>
<span className="text-sm text-gray-600">Go to your Twitter profile</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-300 rounded-full"></div>
<span className="text-sm text-gray-600">Log in if you haven't yet</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-300 rounded-full"></div>
<span className="text-sm text-gray-600">The extension proves your Twitter handle to the verifier server</span>
</div>
</div>
<p className="text-base leading-relaxed mb-2">
Click on the buttons in the sidebar to proceed.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="bg-blue-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-semibold flex-shrink-0 mt-0.5">
3
</div>
<p className="text-base leading-relaxed">
{process.env.POAP === 'true' ? (
<>
If successful, your screen name will be shown here and you can claim a POAP.
</>
) : (
<>
If successful, your screen name will be shown here.
</>
)}
</p>
</div>
</div>
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
💡 <strong>Tip:</strong> When step 3 is running, you can close the Twitter/X tab, but don't close the sidebar.
</p>
</div>
</div>
</div>
)}
{step === 1 && pluginData && screenName && (
<div className="flex flex-col items-center justify-center gap-2">
<h3 className="text-lg font-semibold text-center">
Optional: Claim Your POAP
</h3>
<ClaimPoap screen_name={screenName} exploding={exploding} />
{step === 1 && sessionId && screenName && (
<div className="flex flex-col items-center justify-center gap-6 max-w-4xl w-full">
{/* Success Header with Animation */}
<div className="text-center space-y-4 animate-fade-in">
<div className="bg-green-100 border-2 border-green-300 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-4 animate-bounce">
<span className="text-4xl"></span>
</div>
<h2 className="text-3xl font-bold text-green-800 mb-2">
🎉 Verification Successful! 🎉
</h2>
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 rounded-xl p-6 shadow-lg">
<h3 className="text-xl font-semibold text-gray-900 mb-2">
Successfully verified your Twitter identity
</h3>
<div className="bg-white rounded-lg p-4 border border-green-200">
<p className="text-lg text-gray-700">
Screen name: <span className="font-bold text-green-700 text-xl">@{screenName}</span>
</p>
</div>
</div>
</div>
{/* POAP Section */}
{process.env.POAP === 'true' && (
<div className="bg-gradient-to-r from-yellow-50 to-amber-50 border-2 border-yellow-300 rounded-xl p-8 w-full shadow-lg">
<div className="text-center space-y-4">
<div className="bg-yellow-100 border-2 border-yellow-300 rounded-full w-16 h-16 flex items-center justify-center mx-auto">
<span className="text-3xl">🎁</span>
</div>
<h3 className="text-2xl font-bold text-yellow-800">
Claim Your Reward!
</h3>
<p className="text-lg text-yellow-700 max-w-2xl mx-auto">
You've successfully proven your Twitter identity! Now claim your exclusive POAP token as proof of this achievement.
</p>
<div className="pt-4">
<ClaimPoap sessionId={sessionId} exploding={exploding} />
</div>
</div>
</div>
)}
{/* What's Next Section */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 w-full">
<h4 className="text-lg font-semibold text-blue-900 mb-3 text-center">
What just happened?
</h4>
<div className="grid md:grid-cols-2 gap-4 text-sm text-blue-800">
<div className="flex items-start gap-2">
<span className="text-blue-600">🔒</span>
<div>
<p className="font-semibold">Privacy Preserved</p>
<p>Your sensitive data stayed private - only your screen name was verified</p>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-600">🛡</span>
<div>
<p className="font-semibold">Cryptographic Proof</p>
<p>TLSNotary created a verifiable proof without exposing your credentials to the verifier</p>
</div>
</div>
</div>
</div>
{/* Try Again Button */}
<div className="text-center">
<button
onClick={() => {
setSessionId('');
setScreenName('');
setStep(1);
}}
className="bg-gray-500 hover:bg-gray-600 text-white px-8 py-3 rounded-lg font-semibold transition-colors"
>
🔄 Try Again
</button>
</div>
</div>
)}
</div>
{pluginData && (
<DisplayPluginData
step={step}
pluginData={pluginData}
transcript={transcript}
/>
)}
</>
) : (
<InstallExtensionPrompt />
@@ -182,66 +308,12 @@ export default function Steps(): ReactElement {
);
}
function DisplayPluginData({
step,
pluginData,
transcript,
}: {
step: number;
pluginData: any;
transcript: any;
}): ReactElement {
const [tab, setTab] = useState<'sent' | 'recv'>('sent');
return (
<div className="flex justify-center items-center space-x-4 mt-8">
<div className="w-96">
<div className="p-2 bg-gray-200 border-t rounded-t-md text-center text-lg font-semibold">
Attestation
</div>
<div className="p-4 bg-gray-100 border rounded-b-md h-96 text-left overflow-auto">
<pre className="text-sm text-gray-700 whitespace-pre-wrap text-[12px]">
{formatDataPreview(pluginData)}
</pre>
</div>
</div>
<div className="w-96">
<div className="p-2 bg-gray-200 border-t rounded-t-md text-center text-lg font-semibold">
Presentation
</div>
<div className="bg-gray-100 border rounded-b-md h-96 overflow-auto">
<div className="flex border-b">
<button
onClick={() => setTab('sent')}
className={`p-2 w-1/2 text-center ${tab === 'sent' ? 'bg-slate-500 text-white' : 'bg-white text-black'}`}
>
Sent
</button>
<button
onClick={() => setTab('recv')}
className={`p-2 w-1/2 text-center ${tab === 'recv' ? 'bg-slate-500 text-white' : 'bg-white text-black'}`}
>
Received
</button>
</div>
<div className="p-4 text-left">
<pre className="text-[10px] text-gray-700 whitespace-pre-wrap">
{transcript &&
(tab === 'sent' ? transcript.sent : transcript.recv)}
</pre>
</div>
</div>
</div>
</div>
);
}
function ClaimPoap({
screen_name,
exploding,
sessionId,
}: {
screen_name: string;
exploding: boolean;
sessionId?: string;
}): ReactElement {
const [poapLink, setPoapLink] = useState<string>('');
const [error, setError] = useState<string | null>(null);
@@ -251,13 +323,13 @@ function ClaimPoap({
setLoading(true);
setError(null);
try {
if (!screen_name) return;
if (!sessionId) return;
const response = await fetch('/poap-claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ screenName: screen_name }),
body: JSON.stringify({ sessionId }),
});
if (response.status === 200) {
const data = await response.json();
@@ -301,36 +373,131 @@ function ClaimPoap({
}
function InstallExtensionPrompt() {
const handleRefresh = () => {
window.location.reload();
};
return (
<div className="flex flex-col justify-center items-center gap-2">
<div className="flex flex-col justify center items-center gap-2 pb-4">
<h1 className="text-base font-light">
<div className="flex flex-col items-center gap-8 max-w-4xl mx-auto p-6">
{/* Header Section */}
<div className="text-center space-y-4">
<h1 className="text-3xl font-bold text-gray-900">
Welcome to the TLSNotary Plugin Demo!
</h1>
<p className="text-base font-light">
This demo shows how TLSNotary can be used to verify private user data
in a webapp.
</p>
<p className="text-base font-light">
In this demo you'll prove that you own a Twitter/X account to the
webserver.
</p>
<p className="text-base font-light">
The webserver will verify your attestation and give a POAP in return (
<span className="font-semibold">while supplies last</span>)
<p className="text-xl text-gray-600 leading-relaxed">
Verify private user data in web applications using zero-knowledge proofs
</p>
<img className="mx-auto max-w-full h-auto mt-4" src={OverviewSvg} alt="TLSNotary Prover-Verifier Overview" />
</div>
{/* Demo Description */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-8 w-full">
<h2 className="text-2xl font-semibold text-blue-900 mb-6 text-center">
How this demo works
</h2>
<div className="grid md:grid-cols-3 gap-6">
<div className="text-center">
<div className="bg-blue-500 text-white rounded-full w-12 h-12 flex items-center justify-center mx-auto mb-4 text-lg font-bold">
1
</div>
<h3 className="font-semibold text-gray-900 mb-2">Connect Extension</h3>
<p className="text-gray-600 text-sm">
Install and connect the TLSNotary browser extension
</p>
</div>
<div className="text-center">
<div className="bg-blue-500 text-white rounded-full w-12 h-12 flex items-center justify-center mx-auto mb-4 text-lg font-bold">
2
</div>
<h3 className="font-semibold text-gray-900 mb-2">Prove Ownership</h3>
<p className="text-gray-600 text-sm">
Prove you own a Twitter/X account without revealing sensitive data
</p>
</div>
<div className="text-center">
<div className="bg-blue-500 text-white rounded-full w-12 h-12 flex items-center justify-center mx-auto mb-4 text-lg font-bold">
3
</div>
<h3 className="font-semibold text-gray-900 mb-2">
{process.env.POAP === 'true' ? 'Get Rewarded' : 'Verification Complete'}
</h3>
<p className="text-gray-600 text-sm">
{process.env.POAP === 'true'
? 'Receive a POAP token as proof of verification'
: 'Your Twitter screen name is verified'
}
</p>
</div>
</div>
{process.env.POAP === 'true' && (
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-center text-yellow-800">
🎁 <strong>Special offer:</strong> Get a POAP (Proof of Attendance Protocol) token after verification!{' '}<br />
<span className="font-semibold">(while supplies last)</span>
</p>
</div>
)}
</div>
{/* Installation Section */}
<div className="bg-white border-2 border-gray-200 rounded-xl p-8 w-full shadow-sm">
<div className="text-center space-y-6">
<div className="space-y-2">
<h2 className="text-xl font-semibold text-gray-900">
Get Started
</h2>
<p className="text-gray-600">
Install the TLSNotary extension to begin the verification process
</p>
{/* Add the manual refresh notice */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mt-4">
<p className="text-sm text-blue-800">
<strong>Note:</strong> This page cannot automatically detect when the extension is installed.
You'll need to refresh the page after installation. We've added a refresh button below for your convenience.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="https://chromewebstore.google.com/detail/tlsn-extension/gcfkkledipjbgdbimfpijgbkhajiaaph"
target="_blank"
className="button bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 text-lg font-semibold min-w-[200px]"
>
📥 Install Extension
</a>
<button
onClick={handleRefresh}
className="button bg-gray-100 hover:bg-gray-200 text-gray-700 px-8 py-3 text-lg font-semibold min-w-[200px]"
>
🔄 Refresh Page
</button>
</div>
</div>
</div>
{/* Additional Info */}
<div className="text-center space-y-4 text-gray-600 max-w-2xl">
<h3 className="text-lg font-semibold text-gray-900">
What is TLSNotary?
</h3>
<p className="text-sm leading-relaxed">
TLSNotary enables privacy-preserving verification of web data. Instead of sharing your actual data,
you can prove specific facts about it using cryptographic proofs, keeping your sensitive information private.
</p>
<div className="flex items-center justify-center gap-6 text-xs text-gray-500 pt-4">
<a href="https://tlsnotary.org" className="hover:text-blue-600 transition-colors">Learn More</a>
<a href="https://tlsnotary.org/docs/intro" className="hover:text-blue-600 transition-colors">Documentation</a>
<a href="https://github.com/tlsnotary" className="hover:text-blue-600 transition-colors">GitHub</a>
</div>
</div>
<p className="font-bold">Please install the extension to proceed </p>
<p className="font-bold">
You will need to refresh your browser after installing the extension
</p>
<a
href="https://chromewebstore.google.com/detail/tlsn-extension/gcfkkledipjbgdbimfpijgbkhajiaaph"
target="_blank"
className="button"
>
Install TLSN Extension
</a>
</div>
);
}

View File

@@ -1,14 +1,8 @@
import 'isomorphic-fetch';
import type {} from 'redux-thunk/extend-redux';
import * as React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './pages/App';
import { Provider } from 'react-redux';
import configureAppStore from './store';
// @ts-ignore
const store = configureAppStore(window.__PRELOADED_STATE__);
// @ts-ignore
delete window.__PRELOADED_STATE__;
@@ -16,11 +10,14 @@ delete window.__PRELOADED_STATE__;
(async () => {
hydrateRoot(
document.getElementById('root')!,
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
<BrowserRouter
future={{
v7_startTransition: true,
v7_relativeSplatPath: true,
}}
>
<App />
</BrowserRouter>
);
})();

View File

@@ -2,9 +2,4 @@
@tailwind components;
@tailwind utilities;
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
@import "~@fortawesome/fontawesome-free/css/all.css";

View File

@@ -1,51 +0,0 @@
import { Attestation, AttestedData } from '../utils/types';
enum ActionType {
SET_ATTESTATION = 'attestation/SET_ATTESTATION',
}
export type Action<payload = any> = {
type: ActionType;
payload: payload;
error?: boolean;
meta?: any;
};
type AttestationData = {
raw: Attestation;
};
export type State = {
raw: Attestation;
};
export const initState: State = {
raw: {
version: '0.1.0-alpha.12',
data: '',
meta: {
notaryUrl: '',
websocketProxyUrl: '',
pluginUrl: '',
},
},
};
export const setAttestation = (
attestation: AttestationData,
): Action<AttestationData> => ({
type: ActionType.SET_ATTESTATION,
payload: attestation,
});
export default function attestation(state = initState, action: Action): State {
switch (action.type) {
case ActionType.SET_ATTESTATION:
return {
...state,
raw: action.payload,
};
default:
return state;
}
}

View File

@@ -1,29 +0,0 @@
import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import attestation from './attestation';
const rootReducer = combineReducers({
attestation,
});
export type AppRootState = ReturnType<typeof rootReducer>;
const createStoreWithMiddleware =
process.env.NODE_ENV === 'development'
? applyMiddleware(
thunk,
createLogger({
collapsed: true,
}),
)(createStore)
: applyMiddleware(thunk)(createStore);
function configureAppStore(preloadedState?: AppRootState) {
const { attestation } = preloadedState || {};
return createStoreWithMiddleware(rootReducer, {
attestation,
});
}
export default configureAppStore;

View File

@@ -1,20 +0,0 @@
export interface AttestedData {
version: '0.1.0-alpha.12';
time: number;
sent: string;
recv: string;
notaryUrl: string;
notaryKey: string;
websocketProxyUrl?: string;
verifierKey?: string;
}
export type Attestation = {
version: '0.1.0-alpha.12';
data: string;
meta: {
notaryUrl: string;
websocketProxyUrl: string;
pluginUrl?: string;
};
};

View File

@@ -1,19 +0,0 @@
import type { PresentationJSON } from 'tlsn-js/build/types';
export const formatDataPreview = (data: PresentationJSON) => {
if (!data) return '';
return Object.entries(data)
.map(([key, value]) => {
if (typeof value === 'object' && value !== null) {
return `${key}: ${JSON.stringify(value, null, 2)}`;
} else if (key === 'data') {
const maxLength = 160;
const previewData = value.toString().substring(0, maxLength);
const formattedData = previewData.match(/.{1,20}/g)?.join('\n');
return `${key}: ${formattedData}... ${value.length} more`;
} else {
return `${key}: ${value}`;
}
})
.join('\n');
};

View File

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