mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-22 13:38:07 -05:00
Compare commits
5 Commits
new-tutori
...
discord
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571631d78c | ||
|
|
99a466db02 | ||
|
|
2b72884192 | ||
|
|
9b22e2af37 | ||
|
|
bbe6e23d5f |
17
.github/workflows/demo.yml
vendored
17
.github/workflows/demo.yml
vendored
@@ -20,9 +20,26 @@ env:
|
||||
should_publish: ${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.')) || github.ref == 'refs/heads/staging' }}
|
||||
|
||||
jobs:
|
||||
test_verifier:
|
||||
name: test verifier server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Run verifier tests
|
||||
working-directory: packages/verifier
|
||||
run: cargo test
|
||||
|
||||
build_and_publish_demo_verifier_server:
|
||||
name: build and publish demo verifier server image
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_verifier
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -591,7 +591,8 @@ logger.setLevel(LogLevel.WARN);
|
||||
Docker-based demo environment for testing plugins:
|
||||
|
||||
**Files:**
|
||||
- `twitter.js`, `swissbank.js` - Example plugin files
|
||||
- `src/plugins/*.plugin.ts` - Plugin source files (TypeScript)
|
||||
- `public/plugins/*.js` - Built plugin files (generated by `build-plugins.js`)
|
||||
- `docker-compose.yml` - Docker services configuration
|
||||
- `nginx.conf` - Reverse proxy configuration
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const plugins = ['twitter', 'swissbank', 'spotify'];
|
||||
const plugins = ['twitter', 'swissbank', 'spotify', 'duolingo', 'discord_dm', 'discord_profile'];
|
||||
|
||||
// Build URLs from environment variables (matching config.ts pattern)
|
||||
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';
|
||||
|
||||
@@ -28,4 +28,31 @@ export const plugins: Record<string, Plugin> = {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
duolingo: {
|
||||
name: 'Duolingo',
|
||||
description: 'Prove your Duolingo language learning progress and achievements',
|
||||
logo: '🦉',
|
||||
file: '/plugins/duolingo.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
// discord_dm: {
|
||||
// name: 'Discord DM',
|
||||
// description: 'Prove your Discord direct messages',
|
||||
// logo: '💬',
|
||||
// file: '/plugins/discord_dm.js',
|
||||
// parseResult: (json) => {
|
||||
// return json.results[json.results.length - 1].value;
|
||||
// },
|
||||
// },
|
||||
discord_profile: {
|
||||
name: 'Discord Profile',
|
||||
description: 'Prove your Discord profile information',
|
||||
logo: '💬',
|
||||
file: '/plugins/discord_profile.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
342
packages/demo/src/plugins/discord_dm.plugin.ts
Normal file
342
packages/demo/src/plugins/discord_dm.plugin.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// @ts-ignore - These will be replaced at build time by Vite's define option
|
||||
const VERIFIER_URL = VITE_VERIFIER_URL;
|
||||
// @ts-ignore
|
||||
const PROXY_URL_BASE = VITE_PROXY_URL;
|
||||
|
||||
const api = 'discord.com';
|
||||
const ui = 'https://discord.com/channels/@me';
|
||||
|
||||
const config = {
|
||||
name: 'Discord DM Plugin',
|
||||
description: 'This plugin will prove your Discord direct messages.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'discord.com',
|
||||
pathname: '/api/v9/users/@me/channels',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'discord.com',
|
||||
pathname: '/api/v9/channels/*/messages',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://discord.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
function getRelevantHeaderValues() {
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header =>
|
||||
header.url.includes(`https://${api}/api/v9/users/@me`) ||
|
||||
header.url.includes(`https://${api}/api/v9/channels`)
|
||||
);
|
||||
});
|
||||
|
||||
const authorization = header?.requestHeaders.find(header => header.name === 'authorization')?.value;
|
||||
|
||||
return { authorization };
|
||||
}
|
||||
|
||||
async function fetchDMs() {
|
||||
const { authorization } = getRelevantHeaderValues();
|
||||
|
||||
if (!authorization) return [];
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const response = await fetch(`https://${api}/api/v9/users/@me/channels`, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
const channels = await response.json();
|
||||
|
||||
// Filter only DM channels (type 1)
|
||||
return channels.filter((channel: any) => channel.type === 1).map((channel: any) => ({
|
||||
id: channel.id,
|
||||
name: channel.recipients?.[0]?.username || 'Unknown User',
|
||||
avatar: channel.recipients?.[0]?.avatar,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching DMs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const selectedDMId = useState('selectedDMId', '');
|
||||
|
||||
if (isRequestPending || !selectedDMId) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const { authorization } = getRelevantHeaderValues();
|
||||
|
||||
const headers = {
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{
|
||||
url: `https://${api}/api/v9/channels/${selectedDMId}/messages?limit=50`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 8000,
|
||||
maxSentData: 2000,
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].content' } },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].author.username' } },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].timestamp' } },
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
setState('isRequestPending', false);
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function selectDM(dmId: string) {
|
||||
setState('selectedDMId', dmId);
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { authorization } = getRelevantHeaderValues();
|
||||
const header_has_necessary_values = !!authorization;
|
||||
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const selectedDMId = useState('selectedDMId', '');
|
||||
const dmList = useState('dmList', []);
|
||||
|
||||
useEffect(() => {
|
||||
openWindow(ui);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (header_has_necessary_values && dmList.length === 0) {
|
||||
fetchDMs().then(dms => setState('dmList', dms));
|
||||
}
|
||||
}, [header_has_necessary_values]);
|
||||
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#5865F2',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['💬']);
|
||||
}
|
||||
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
right: '8px',
|
||||
width: '320px',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
zIndex: '999999',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
}
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['Discord DM Proof']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer',
|
||||
padding: '0',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
onclick: 'minimizeUI',
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Step 1: Login Status
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header_has_necessary_values ? '#d4edda' : '#f8d7da',
|
||||
color: header_has_necessary_values ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header_has_necessary_values ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header_has_necessary_values ? '✓ Discord token detected' : '⚠ No Discord token detected'
|
||||
]),
|
||||
|
||||
// Step 2: DM Selection
|
||||
header_has_necessary_values && dmList.length > 0 ? (
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
}
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '8px',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
}
|
||||
}, ['Select a DM:']),
|
||||
div({
|
||||
style: {
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'white',
|
||||
}
|
||||
}, dmList.map((dm: any) =>
|
||||
div({
|
||||
style: {
|
||||
padding: '10px 12px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
backgroundColor: selectedDMId === dm.id ? '#e3f2fd' : 'transparent',
|
||||
transition: 'background-color 0.2s',
|
||||
},
|
||||
onclick: () => selectDM(dm.id),
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
fontWeight: selectedDMId === dm.id ? '600' : '400',
|
||||
color: '#333',
|
||||
}
|
||||
}, [dm.name])
|
||||
])
|
||||
))
|
||||
])
|
||||
) : null,
|
||||
|
||||
// Step 3: Notarize Button
|
||||
header_has_necessary_values && selectedDMId ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : header_has_necessary_values && dmList.length === 0 ? (
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Loading DMs...'])
|
||||
) : !header_has_necessary_values ? (
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to Discord to continue'])
|
||||
) : null
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
fetchDMs,
|
||||
selectDM,
|
||||
config,
|
||||
};
|
||||
@@ -1,24 +1,40 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// @ts-ignore - These will be replaced at build time by Vite's define option
|
||||
const VERIFIER_URL = VITE_VERIFIER_URL;
|
||||
// @ts-ignore
|
||||
const PROXY_URL_BASE = VITE_PROXY_URL;
|
||||
const api = 'discord.com';
|
||||
const ui = `https://${api}/channels/@me`;
|
||||
|
||||
const config = {
|
||||
name: 'Swiss Bank Prover',
|
||||
description: 'This plugin will prove your Swiss Bank account balance.',
|
||||
name: 'Discord Profile Plugin',
|
||||
description: 'This plugin will prove your Discord username and ID.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'swissbank.tlsnotary.org',
|
||||
pathname: '/balances',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
host: api,
|
||||
pathname: '/api/v9/users/@me',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://swissbank.tlsnotary.org/*',
|
||||
`https://${api}/*`,
|
||||
],
|
||||
};
|
||||
|
||||
const host = 'swissbank.tlsnotary.org';
|
||||
const ui_path = '/account';
|
||||
const path = '/balances';
|
||||
const url = `https://${host}${path}`;
|
||||
function getRelevantHeaderValues() {
|
||||
const [header] = useHeaders(headers => {
|
||||
// console.log('All captured headers:', headers);
|
||||
// Find the first header that contains an 'authorization' request header, regardless of URL
|
||||
return [headers.find(h =>
|
||||
h.requestHeaders.some(rh => rh.name === 'Authorization')
|
||||
)];
|
||||
});
|
||||
|
||||
const authorization = header?.requestHeaders.find(h => h.name === 'Authorization')?.value;
|
||||
return { authorization };
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
@@ -26,52 +42,35 @@ async function onClick() {
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
const [header] = useHeaders(headers => {
|
||||
console.log('Intercepted headers:', headers);
|
||||
return headers.filter(header => header.url.includes(`https://${host}`));
|
||||
});
|
||||
|
||||
const { authorization } = getRelevantHeaderValues();
|
||||
|
||||
const headers = {
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
Host: host,
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{
|
||||
url: url,
|
||||
url: `https://${api}/api/v9/users/@me`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=swissbank.tlsnotary.org',
|
||||
// proxyUrl: 'ws://localhost:55688',
|
||||
maxRecvData: 460, // Maximum bytes to receive from server (response size limit)
|
||||
maxSentData: 180,// Maximum bytes to send to server (request size limit)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 2000,
|
||||
maxSentData: 1000,
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL', },
|
||||
{ type: 'RECV', part: 'START_LINE', action: 'REVEAL', },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'account_id' }, },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'accounts.CHF' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:\s*"[^"]+"' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"275_000_000"' }, },
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'username' } },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'id' } },
|
||||
]
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
@@ -82,23 +81,21 @@ function expandUI() {
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(
|
||||
headers => headers
|
||||
.filter(header => header.url.includes(`https://${host}${ui_path}`))
|
||||
);
|
||||
const { authorization } = getRelevantHeaderValues();
|
||||
|
||||
console.log('🚀🚀🚀🚀🚀🚀🚀 Authorization Header:', authorization);
|
||||
|
||||
const header_has_necessary_values = !!authorization;
|
||||
|
||||
const hasNecessaryHeader = header?.requestHeaders.some(h => h.name === 'Cookie');
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
// Run once on plugin load
|
||||
useEffect(() => {
|
||||
openWindow(`https://${host}${ui_path}`);
|
||||
openWindow(ui);
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
@@ -108,7 +105,7 @@ function main() {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#4CAF50',
|
||||
backgroundColor: '#5865F2',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
@@ -120,17 +117,15 @@ function main() {
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}, ['💬']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
right: '8px',
|
||||
width: '280px',
|
||||
width: '320px',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
@@ -140,10 +135,9 @@ function main() {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -156,7 +150,7 @@ function main() {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['Swiss Bank Prover']),
|
||||
}, ['Discord Profile Proof']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
@@ -175,51 +169,45 @@ function main() {
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether cookie is detected
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: header_has_necessary_values ? '#d4edda' : '#f8d7da',
|
||||
color: header_has_necessary_values ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header_has_necessary_values ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected'
|
||||
header_has_necessary_values ? '✓ Discord token detected' : '⚠ Please login to Discord'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
hasNecessaryHeader ? (
|
||||
// Show prove button when not pending
|
||||
header_has_necessary_values ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
@@ -229,7 +217,7 @@ function main() {
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to continue'])
|
||||
}, ['Please login to Discord to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -241,4 +229,4 @@ export default {
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,41 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// @ts-ignore - These will be replaced at build time by Vite's define option
|
||||
const VERIFIER_URL = VITE_VERIFIER_URL;
|
||||
// @ts-ignore
|
||||
const PROXY_URL_BASE = VITE_PROXY_URL;
|
||||
|
||||
const api = 'www.duolingo.com';
|
||||
const ui = 'https://www.duolingo.com/';
|
||||
|
||||
const config = {
|
||||
name: 'Spotify Top Artist',
|
||||
description: 'This plugin will prove your top artist on Spotify.',
|
||||
name: 'Duolingo Plugin',
|
||||
description: 'This plugin will prove your email and current streak on Duolingo.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'www.duolingo.com',
|
||||
pathname: '/2023-05-23/users/*',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://www.duolingo.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
const api = 'api.spotify.com';
|
||||
const ui = 'https://developer.spotify.com/';
|
||||
const top_artist_path = '/v1/me/top/artists?time_range=medium_term&limit=1';
|
||||
function getRelevantHeaderValues() {
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}/2023-05-23/users`));
|
||||
});
|
||||
|
||||
const authorization = header?.requestHeaders.find(header => header.name === 'Authorization')?.value;
|
||||
|
||||
const traceId = header?.requestHeaders.find(header => header.name === 'X-Amzn-Trace-Id')?.value;
|
||||
const user_id = traceId?.split('=')[1];
|
||||
|
||||
return { authorization, user_id };
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
@@ -15,41 +44,29 @@ async function onClick() {
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}`));
|
||||
});
|
||||
|
||||
// console.log('Intercepted Spotify API request header:', header);
|
||||
const { authorization, user_id } = getRelevantHeaderValues();
|
||||
|
||||
const headers = {
|
||||
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
url: `https://${api}${top_artist_path}`, // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
url: `https://${api}/2023-05-23/users/${user_id}?fields=longestStreak,username`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.spotify.com',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 2400,
|
||||
maxSentData: 600,
|
||||
maxSentData: 1200,
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL', },
|
||||
{ type: 'RECV', part: 'START_LINE', action: 'REVEAL', },
|
||||
{
|
||||
type: 'RECV', part: 'HEADERS', action: 'REVEAL', params: { key: 'date', },
|
||||
},
|
||||
{
|
||||
type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'items[0].name', },
|
||||
// type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'items[0].external_urls.spotify', },
|
||||
},
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'longestStreak', }, },
|
||||
]
|
||||
}
|
||||
);
|
||||
@@ -65,8 +82,8 @@ function minimizeUI() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
|
||||
// const [header] = useHeaders(headers => { return headers.filter(headers => headers.url.includes('https://api.spotify.com')) });
|
||||
const { authorization, user_id } = getRelevantHeaderValues();
|
||||
const header_has_necessary_values = authorization && user_id;
|
||||
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
@@ -84,7 +101,7 @@ function main() {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#1DB954',
|
||||
backgroundColor: '#58CC02',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
@@ -96,7 +113,7 @@ function main() {
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🎵']);
|
||||
}, ['🦉']);
|
||||
}
|
||||
|
||||
return div({
|
||||
@@ -116,7 +133,7 @@ function main() {
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -129,7 +146,7 @@ function main() {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['Spotify Top Artist']),
|
||||
}, ['Duolingo Streak']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
@@ -159,38 +176,34 @@ function main() {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: header_has_necessary_values ? '#d4edda' : '#f8d7da',
|
||||
color: header_has_necessary_values ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header_has_necessary_values ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
header_has_necessary_values ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
header_has_necessary_values ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
@@ -200,7 +213,7 @@ function main() {
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to Spotify to continue'])
|
||||
}, ['Please login to Duolingo to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -1,361 +0,0 @@
|
||||
// =============================================================================
|
||||
// PLUGIN CONFIGURATION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The config object defines plugin metadata displayed to users.
|
||||
* This information appears in the plugin selection UI.
|
||||
*/
|
||||
const config = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// PROOF GENERATION CALLBACK
|
||||
// =============================================================================
|
||||
/**
|
||||
* This function is triggered when the user clicks the "Prove" button.
|
||||
* It extracts authentication headers from intercepted requests and generates
|
||||
* a TLSNotary proof using the unified prove() API.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Get the intercepted X.com API request headers
|
||||
* 2. Extract authentication headers (Cookie, CSRF token, OAuth token, etc.)
|
||||
* 3. Call prove() with the request configuration and reveal handlers
|
||||
* 4. prove() internally:
|
||||
* - Creates a prover connection to the verifier
|
||||
* - Sends the HTTP request through the TLS prover
|
||||
* - Captures the TLS transcript (sent/received bytes)
|
||||
* - Parses the transcript with byte-level range tracking
|
||||
* - Applies selective reveal handlers to show only specified data
|
||||
* - Generates and returns the cryptographic proof
|
||||
* 5. Return the proof result to the caller via done()
|
||||
*/
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
// Step 1: Get the intercepted header from the X.com API request
|
||||
// useHeaders() provides access to all intercepted HTTP request headers
|
||||
// We filter for the specific X.com API endpoint we want to prove
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
|
||||
});
|
||||
|
||||
// Step 2: Extract authentication headers from the intercepted request
|
||||
// These headers are required to authenticate with the X.com API
|
||||
const headers = {
|
||||
// Cookie: Session authentication token
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
|
||||
// X-CSRF-Token: Cross-Site Request Forgery protection token
|
||||
'x-csrf-token': header.requestHeaders.find(header => header.name === 'x-csrf-token')?.value,
|
||||
|
||||
// X-Client-Transaction-ID: Request tracking identifier
|
||||
'x-client-transaction-id': header.requestHeaders.find(header => header.name === 'x-client-transaction-id')?.value,
|
||||
|
||||
// Host: Target server hostname
|
||||
Host: 'api.x.com',
|
||||
|
||||
// Authorization: OAuth bearer token for API authentication
|
||||
authorization: header.requestHeaders.find(header => header.name === 'authorization')?.value,
|
||||
|
||||
// Accept-Encoding: Must be 'identity' for TLSNotary (no compression)
|
||||
// TLSNotary requires uncompressed data to verify byte-for-byte
|
||||
'Accept-Encoding': 'identity',
|
||||
|
||||
// Connection: Use 'close' to complete the connection after one request
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
// Step 3: Generate TLS proof using the unified prove() API
|
||||
// This single function handles the entire proof generation pipeline
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
// REQUEST OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Defines the HTTP request to be proven
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json', // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PROVER OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Configures the TLS proof generation process
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
// Must be running locally or accessible at this address
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
|
||||
// Proxy URL: WebSocket proxy that relays TLS data to the target server
|
||||
// The token parameter specifies which server to connect to
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
|
||||
|
||||
// Maximum bytes to receive from server (response size limit)
|
||||
maxRecvData: 4000,
|
||||
|
||||
// Maximum bytes to send to server (request size limit)
|
||||
maxSentData: 2000,
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
handlers: [
|
||||
// Reveal the request start line (GET /path HTTP/1.1)
|
||||
// This proves the HTTP method and path were sent
|
||||
{
|
||||
type: 'SENT', // Direction: data sent to server
|
||||
part: 'START_LINE', // Part: HTTP request line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the response start line (HTTP/1.1 200 OK)
|
||||
// This proves the server responded with status code 200
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'START_LINE', // Part: HTTP response line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the 'date' header from the response
|
||||
// This proves when the server generated the response
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'HEADERS', // Part: HTTP headers
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
key: 'date', // Specific header to reveal
|
||||
},
|
||||
},
|
||||
|
||||
// Reveal the 'screen_name' field from the JSON response body
|
||||
// This proves the X.com username without revealing other profile data
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'BODY', // Part: HTTP response body
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
type: 'json', // Body format: JSON
|
||||
path: 'screen_name', // JSON field to reveal (top-level only)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN UI FUNCTION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The main() function is called reactively whenever plugin state changes.
|
||||
* It returns a DOM structure that is rendered as the plugin UI.
|
||||
*
|
||||
* React-like Hooks Used:
|
||||
* - useHeaders(): Subscribes to intercepted HTTP request headers
|
||||
* - useEffect(): Runs side effects when dependencies change
|
||||
*
|
||||
* UI Flow:
|
||||
* 1. Check if X.com API request headers have been intercepted
|
||||
* 2. If not intercepted yet: Show "Please login" message
|
||||
* 3. If intercepted: Show "Profile detected" with a "Prove" button
|
||||
* 4. On first render: Open X.com in a new window to trigger login
|
||||
*/
|
||||
function main() {
|
||||
// Subscribe to intercepted headers for the X.com API endpoint
|
||||
// This will reactively update whenever new headers matching the filter arrive
|
||||
const [header] = useHeaders(headers => headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json')));
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
// Run once on plugin load: Open X.com in a new window
|
||||
// The empty dependency array [] means this runs only once
|
||||
// The opened window's requests will be intercepted by the plugin
|
||||
useEffect(() => {
|
||||
openWindow('https://x.com');
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#4CAF50',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
right: '8px',
|
||||
width: '280px',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
zIndex: '999999',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
}
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['X Profile Prover']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer',
|
||||
padding: '0',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
onclick: 'minimizeUI',
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether profile is detected
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Profile detected' : '⚠ No profile detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to x.com to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PLUGIN EXPORTS
|
||||
// =============================================================================
|
||||
/**
|
||||
* All plugins must export an object with these properties:
|
||||
* - main: The reactive UI rendering function
|
||||
* - onClick: Click handler callback for buttons
|
||||
* - config: Plugin metadata
|
||||
*/
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
@@ -158,7 +158,7 @@ function makeUseHeaders(
|
||||
|
||||
// Validate that filterFn returned an array
|
||||
if (result === undefined) {
|
||||
throw new Error(`useHeaders: filter function returned undefined. expect an erray`);
|
||||
throw new Error(`useHeaders: filter function returned undefined. expect an array`);
|
||||
}
|
||||
if (!Array.isArray(result)) {
|
||||
throw new Error(`useHeaders: filter function must return an array, got ${typeof result}. `);
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn main() {
|
||||
// Build router with routes
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/info", get(info_handler))
|
||||
.route("/session", get(session_ws_handler))
|
||||
.route("/verifier", get(verifier_ws_handler))
|
||||
.route("/proxy", get(proxy_ws_handler))
|
||||
@@ -75,6 +76,7 @@ async fn main() {
|
||||
|
||||
info!("Server listening on http://{}", addr);
|
||||
info!("Health endpoint: http://{}/health", addr);
|
||||
info!("Info endpoint: http://{}/info", addr);
|
||||
info!("Session WebSocket endpoint: ws://{}/session", addr);
|
||||
info!(
|
||||
"Verifier WebSocket endpoint: ws://{}/verifier?sessionId=<id>",
|
||||
@@ -352,6 +354,28 @@ async fn health_handler() -> impl IntoResponse {
|
||||
"ok"
|
||||
}
|
||||
|
||||
/// Info response structure
|
||||
#[derive(Debug, Serialize)]
|
||||
struct InfoResponse {
|
||||
/// Package version from Cargo.toml
|
||||
version: &'static str,
|
||||
/// Git commit hash (from GIT_HASH env var, set by CI)
|
||||
git_hash: String,
|
||||
/// TLSNotary library version
|
||||
tlsn_version: &'static str,
|
||||
}
|
||||
|
||||
/// Info endpoint handler - returns server information as JSON
|
||||
pub(crate) async fn info_handler() -> impl IntoResponse {
|
||||
let git_hash = std::env::var("GIT_HASH").unwrap_or_else(|_| "dev".to_string());
|
||||
|
||||
axum::Json(InfoResponse {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
git_hash,
|
||||
tlsn_version: "0.1.0-alpha.14",
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket session handler for extension
|
||||
pub(crate) async fn session_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
|
||||
@@ -28,9 +28,11 @@ use tracing::info;
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
use tlsn::{
|
||||
config::ProtocolConfig,
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig},
|
||||
config::{
|
||||
prove::ProveConfig, prover::ProverConfig, tls::TlsClientConfig, tls_commit::TlsCommitConfig,
|
||||
},
|
||||
prover::Prover,
|
||||
Session,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -156,11 +158,11 @@ async fn webhook_handler(
|
||||
// ============================================================================
|
||||
|
||||
async fn start_verifier_server(webhook_port: u16, verifier_port: u16) -> JoinHandle<()> {
|
||||
// Create config with webhook for swapi.dev
|
||||
// Create config with webhook for raw.githubusercontent.com
|
||||
let config_yaml = format!(
|
||||
r#"
|
||||
webhooks:
|
||||
"swapi.dev":
|
||||
"raw.githubusercontent.com":
|
||||
url: "http://127.0.0.1:{}"
|
||||
headers: {{}}
|
||||
"#,
|
||||
@@ -176,14 +178,9 @@ webhooks:
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", axum::routing::get(|| async { "ok" }))
|
||||
.route(
|
||||
"/session",
|
||||
axum::routing::get(crate::session_ws_handler),
|
||||
)
|
||||
.route(
|
||||
"/verifier",
|
||||
axum::routing::get(crate::verifier_ws_handler),
|
||||
)
|
||||
.route("/info", axum::routing::get(crate::info_handler))
|
||||
.route("/session", axum::routing::get(crate::session_ws_handler))
|
||||
.route("/verifier", axum::routing::get(crate::verifier_ws_handler))
|
||||
.route("/proxy", axum::routing::get(crate::proxy_ws_handler))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(app_state);
|
||||
@@ -385,15 +382,23 @@ async fn connect_wss(
|
||||
|
||||
/// Helper function that performs MPC-TLS and HTTP request with a given proxy stream
|
||||
async fn run_prover_with_stream<S>(
|
||||
prover: tlsn::prover::Prover<tlsn::prover::state::Setup>,
|
||||
prover: Prover,
|
||||
tls_commit_config: TlsCommitConfig,
|
||||
tls_client_config: TlsClientConfig,
|
||||
proxy_stream: S,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error + Send + Sync>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
// 5. Pass proxy connection into the prover for TLS
|
||||
// 5. Start the TLS commitment protocol
|
||||
let prover = prover
|
||||
.commit(tls_commit_config)
|
||||
.await
|
||||
.map_err(|e| format!("Commitment failed: {}", e))?;
|
||||
|
||||
// 6. Pass proxy connection into the prover for TLS
|
||||
let (mpc_tls_connection, prover_fut) = prover
|
||||
.connect(proxy_stream)
|
||||
.connect(tls_client_config, proxy_stream)
|
||||
.await
|
||||
.map_err(|e| format!("TLS connect failed: {}", e))?;
|
||||
|
||||
@@ -405,18 +410,19 @@ where
|
||||
// Spawn the prover task
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// 6. HTTP handshake
|
||||
let (mut request_sender, connection) = hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
// 7. HTTP handshake
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
|
||||
tokio::spawn(connection);
|
||||
|
||||
// 7. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /api/films/1/");
|
||||
// 8. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json");
|
||||
let request = Request::builder()
|
||||
.uri("/api/films/1/")
|
||||
.header("Host", "swapi.dev")
|
||||
.uri("/tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json")
|
||||
.header("Host", "raw.githubusercontent.com")
|
||||
.header("Accept", "application/json")
|
||||
.header("Connection", "close")
|
||||
.method("GET")
|
||||
@@ -431,7 +437,7 @@ where
|
||||
info!("[Prover] Response status: {}", response.status());
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// 8. Wait for prover task to complete
|
||||
// 9. Wait for prover task to complete
|
||||
let mut prover = prover_task
|
||||
.await
|
||||
.map_err(|e| format!("Prover task panicked: {}", e))?
|
||||
@@ -446,24 +452,26 @@ where
|
||||
recv.len()
|
||||
);
|
||||
|
||||
// 9. Build reveal configuration (reveal everything)
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
builder.server_identity();
|
||||
builder
|
||||
// 10. Build proof configuration (reveal everything including server identity)
|
||||
let mut prove_config = ProveConfig::builder(prover.transcript());
|
||||
prove_config.server_identity();
|
||||
prove_config
|
||||
.reveal_sent(&(0..sent.len()))
|
||||
.map_err(|e| format!("reveal_sent failed: {}", e))?;
|
||||
builder
|
||||
prove_config
|
||||
.reveal_recv(&(0..recv.len()))
|
||||
.map_err(|e| format!("reveal_recv failed: {}", e))?;
|
||||
let prove_config = prove_config
|
||||
.build()
|
||||
.map_err(|e| format!("build proof failed: {}", e))?;
|
||||
|
||||
let config = builder.build().unwrap();
|
||||
|
||||
// 10. Send proof to verifier
|
||||
// 11. Send proof to verifier
|
||||
info!("[Prover] Sending proof to verifier");
|
||||
prover
|
||||
.prove(&config)
|
||||
.prove(&prove_config)
|
||||
.await
|
||||
.map_err(|e| format!("prove failed: {}", e))?;
|
||||
|
||||
prover
|
||||
.close()
|
||||
.await
|
||||
@@ -491,50 +499,134 @@ async fn run_prover(
|
||||
// WsStream implements tokio::io::AsyncRead/AsyncWrite when inner implements futures_io traits
|
||||
let verifier_stream = WsStream::new(verifier_ws);
|
||||
|
||||
// 2. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.server_name(ServerName::Dns("swapi.dev".try_into().unwrap()))
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
// 2. Create session with verifier stream
|
||||
let session = Session::new(verifier_stream);
|
||||
let (driver, mut handle) = session.split();
|
||||
|
||||
// Spawn the session driver in the background
|
||||
let driver_task = tokio::spawn(driver);
|
||||
|
||||
// 3. Create TLS commit config for MPC protocol
|
||||
use tlsn::config::tls_commit::{mpc::MpcTlsConfig, TlsCommitProtocolConfig};
|
||||
let mpc_config = MpcTlsConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap();
|
||||
.map_err(|e| format!("Failed to build MPC TLS config: {}", e))?;
|
||||
|
||||
let tls_commit_config = TlsCommitConfig::builder()
|
||||
.protocol(TlsCommitProtocolConfig::Mpc(mpc_config))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS commit config: {}", e))?;
|
||||
|
||||
// 4. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build prover config: {}", e))?;
|
||||
|
||||
info!("[Prover] Setting up MPC-TLS with verifier");
|
||||
|
||||
// 3. Create prover and perform setup with verifier
|
||||
// tlsn expects futures_io traits, so we don't need compat() - WsStream already provides them
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_stream)
|
||||
.await
|
||||
.map_err(|e| format!("Prover setup failed: {}", e))?;
|
||||
// 5. Create prover via handle
|
||||
let prover = handle
|
||||
.new_prover(prover_config)
|
||||
.map_err(|e| format!("Failed to create prover: {}", e))?;
|
||||
|
||||
// 6. Create TLS client config with server name and root certs
|
||||
use tlsn::{connection::ServerName, webpki::RootCertStore};
|
||||
let tls_client_config = TlsClientConfig::builder()
|
||||
.server_name(ServerName::Dns(
|
||||
"raw.githubusercontent.com".try_into().unwrap(),
|
||||
))
|
||||
.root_store(RootCertStore::mozilla())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS client config: {}", e))?;
|
||||
|
||||
info!("[Prover] Connecting to proxy at {}", proxy_url);
|
||||
|
||||
// 4. Connect to proxy WebSocket (ws:// or wss://)
|
||||
if proxy_url.starts_with("wss://") {
|
||||
// 7. Connect to proxy WebSocket (ws:// or wss://) and run prover
|
||||
let result = if proxy_url.starts_with("wss://") {
|
||||
let proxy_ws = connect_wss(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (wss)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
} else {
|
||||
let proxy_ws = connect_ws(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (ws)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
}
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
};
|
||||
|
||||
// 8. Close the session handle
|
||||
handle.close();
|
||||
|
||||
// 9. Wait for the driver to complete
|
||||
driver_task
|
||||
.await
|
||||
.map_err(|e| format!("Driver task failed: {}", e))?
|
||||
.map_err(|e| format!("Session driver error: {}", e))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Test
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Test the /health endpoint
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_swapi() {
|
||||
async fn health() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 1, VERIFIER_PORT + 1).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/health", VERIFIER_PORT + 1))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.text().await.unwrap(), "ok");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
/// Test the /info endpoint returns expected JSON structure
|
||||
#[tokio::test]
|
||||
async fn info() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 2, VERIFIER_PORT + 2).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/info", VERIFIER_PORT + 2))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let info: Value = resp.json().await.expect("Failed to parse JSON");
|
||||
|
||||
// Verify required fields exist
|
||||
info.get("version").expect("Missing version field");
|
||||
info.get("git_hash").expect("Missing git_hash field");
|
||||
info.get("tlsn_version")
|
||||
.expect("Missing tlsn_version field");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_github() {
|
||||
// Initialize tracing for debugging
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
@@ -573,7 +665,10 @@ async fn test_webhook_integration_with_swapi() {
|
||||
"ws://127.0.0.1:{}/verifier?sessionId={}",
|
||||
VERIFIER_PORT, session_id
|
||||
);
|
||||
let proxy_url = format!("ws://127.0.0.1:{}/proxy?token=swapi.dev", VERIFIER_PORT);
|
||||
let proxy_url = format!(
|
||||
"ws://127.0.0.1:{}/proxy?token=raw.githubusercontent.com",
|
||||
VERIFIER_PORT
|
||||
);
|
||||
|
||||
let prover_handle = tokio::spawn(async move {
|
||||
run_prover(verifier_ws_url, proxy_url, MAX_SENT_DATA, MAX_RECV_DATA).await
|
||||
@@ -628,11 +723,11 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// 8. Verify results contain expected data
|
||||
assert!(!results.is_empty(), "Should have handler results");
|
||||
|
||||
// Check that response contains Star Wars data
|
||||
// Check that response contains expected JSON data
|
||||
let recv_str = String::from_utf8_lossy(&recv_transcript);
|
||||
assert!(
|
||||
recv_str.contains("A New Hope") || recv_str.contains("Star Wars"),
|
||||
"Response should contain Star Wars film data: {}",
|
||||
recv_str.contains("software engineer") || recv_str.contains("Anytown"),
|
||||
"Response should contain expected JSON data: {}",
|
||||
&recv_str[..recv_str.len().min(500)]
|
||||
);
|
||||
|
||||
@@ -651,8 +746,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
|
||||
// Verify webhook payload structure
|
||||
assert_eq!(
|
||||
payload["server_name"], "swapi.dev",
|
||||
"server_name should be swapi.dev"
|
||||
payload["server_name"], "raw.githubusercontent.com",
|
||||
"server_name should be raw.githubusercontent.com"
|
||||
);
|
||||
assert!(payload["results"].is_array(), "results should be an array");
|
||||
assert!(
|
||||
@@ -691,8 +786,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// Verify transcript contains expected content
|
||||
let webhook_recv = payload["transcript"]["recv"].as_str().unwrap();
|
||||
assert!(
|
||||
webhook_recv.contains("A New Hope") || webhook_recv.contains("title"),
|
||||
"Webhook transcript should contain Star Wars film data"
|
||||
webhook_recv.contains("software engineer") || webhook_recv.contains("Anytown"),
|
||||
"Webhook transcript should contain expected JSON data"
|
||||
);
|
||||
|
||||
info!("All assertions passed!");
|
||||
|
||||
Reference in New Issue
Block a user