Compare commits

..

4 Commits

Author SHA1 Message Date
Hendrik Eeckhaut
de9faea4c9 Added correct Chrome Web Store url 2026-01-23 11:48:19 +01:00
Hendrik Eeckhaut
877527aeca Fix: correctly set git hash in verfier's info 2026-01-23 11:48:19 +01:00
Hendrik Eeckhaut
52cc68937b perf(spotify-plugin): cache auth token to avoid repeated header filtering (#230) 2026-01-22 10:31:41 +01:00
Hendrik Eeckhaut
a04b3c671a Fix: git hash in deployed version (#229) 2026-01-21 11:27:22 +01:00
13 changed files with 47 additions and 618 deletions

View File

@@ -69,6 +69,8 @@ jobs:
push: ${{ env.should_publish == 'true' }}
tags: ${{ steps.meta-prover-server.outputs.tags }}
labels: ${{ steps.meta-prover-server.outputs.labels }}
build-args: |
GIT_HASH=${{ github.sha }}
build_and_publish_demo_frontend:
name: build and publish demo frontend image
@@ -105,3 +107,4 @@ jobs:
build-args: |
VITE_VERIFIER_HOST=demo-staging.tlsnotary.org
VITE_SSL=true
GIT_HASH=${{ github.sha }}

View File

@@ -4,6 +4,7 @@ FROM node:20-alpine AS builder
# Accept build arguments with defaults
ARG VITE_VERIFIER_HOST=localhost:7047
ARG VITE_SSL=false
ARG GIT_HASH=local
WORKDIR /app
@@ -17,6 +18,7 @@ COPY . .
# Build with environment variables
ENV VITE_VERIFIER_HOST=${VITE_VERIFIER_HOST}
ENV VITE_SSL=${VITE_SSL}
ENV GIT_HASH=${GIT_HASH}
RUN npm run build
# Runtime stage

View File

@@ -5,7 +5,7 @@ import fs from 'fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const plugins = ['twitter', 'swissbank', 'spotify', 'duolingo', 'discord_dm', 'discord_profile'];
const plugins = ['twitter', 'swissbank', 'spotify', 'duolingo'];
// Build URLs from environment variables (matching config.ts pattern)
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';

View File

@@ -9,6 +9,7 @@ services:
- "7047:7047"
environment:
- RUST_LOG=info
- GIT_HASH=${GIT_HASH:-dev}
restart: unless-stopped
demo-static:
@@ -17,6 +18,7 @@ services:
args:
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
VITE_SSL: ${VITE_SSL:-false}
GIT_HASH: ${GIT_HASH:-dev}
restart: unless-stopped
nginx:

View File

@@ -290,7 +290,7 @@ export function App() {
>
View source on GitHub
</a>
<span className="footer-version">v{__GIT_COMMIT_HASH__}</span>
<span className="footer-version">{__GIT_COMMIT_HASH__}</span>
</footer>
</div>
);

View File

@@ -71,7 +71,7 @@ export function StatusBar({
{!extensionOk && (
<div>
TLSNotary extension not detected.{' '}
<a href="chrome://extensions/" target="_blank" rel="noopener noreferrer">
<a href="https://chromewebstore.google.com/detail/tlsnotary/gnoglgpcamodhflknhmafmjdahcejcgg?authuser=2&hl=en" target="_blank" rel="noopener noreferrer">
Install extension
</a>
{' '}then <strong>refresh this page</strong>.

View File

@@ -37,22 +37,4 @@ export const plugins: Record<string, Plugin> = {
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;
},
},
};

View File

@@ -1,342 +0,0 @@
/// <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,
};

View File

@@ -1,232 +0,0 @@
/// <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: 'Discord Profile Plugin',
description: 'This plugin will prove your Discord username and ID.',
requests: [
{
method: 'GET',
host: api,
pathname: '/api/v9/users/@me',
verifierUrl: VERIFIER_URL,
},
],
urls: [
`https://${api}/*`,
],
};
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);
if (isRequestPending) 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/users/@me`,
method: 'GET',
headers: headers,
},
{
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + api,
maxRecvData: 2000,
maxSentData: 1000,
handlers: [
{ 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' } },
]
}
);
done(JSON.stringify(resp));
}
function expandUI() {
setState('isMinimized', false);
}
function minimizeUI() {
setState('isMinimized', true);
}
function main() {
const { authorization } = getRelevantHeaderValues();
console.log('🚀🚀🚀🚀🚀🚀🚀 Authorization Header:', authorization);
const header_has_necessary_values = !!authorization;
const isMinimized = useState('isMinimized', false);
const isRequestPending = useState('isRequestPending', false);
useEffect(() => {
openWindow(ui);
}, []);
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 Profile 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',
}
}, [
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' : '⚠ Please login to Discord'
]),
header_has_necessary_values ? (
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'])
) : (
div({
style: {
textAlign: 'center',
color: '#666',
padding: '12px',
backgroundColor: '#fff3cd',
borderRadius: '6px',
border: '1px solid #ffeaa7',
}
}, ['Please login to Discord to continue'])
)
])
]);
}
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};

View File

@@ -33,12 +33,16 @@ async function onClick() {
setState('isRequestPending', true);
const [header] = useHeaders(headers => {
return headers.filter(header => header.url.includes(`https://${api}`));
});
// Use cached authorization token from state
const authToken = useState('authToken', null);
if (!authToken) {
setState('isRequestPending', false);
return;
}
const headers = {
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
authorization: authToken,
Host: api,
'Accept-Encoding': 'identity',
Connection: 'close',
@@ -79,10 +83,23 @@ function minimizeUI() {
}
function main() {
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
const isMinimized = useState('isMinimized', false);
const isRequestPending = useState('isRequestPending', false);
const authToken = useState('authToken', null);
// Only search for auth token if not already cached
if (!authToken) {
const token = useHeaders(h => h.filter(x => x.url.startsWith(`https://${api}`)))
.flatMap(h => h.requestHeaders)
.find((h: { name: string; value?: string }) => h.name === 'Authorization')
?.value;
if (token) {
setState('authToken', token);
console.log('Auth Token found:', token);
}
}
useEffect(() => {
openWindow(ui);
@@ -172,16 +189,16 @@ function main() {
marginBottom: '16px',
padding: '12px',
borderRadius: '6px',
backgroundColor: header ? '#d4edda' : '#f8d7da',
color: header ? '#155724' : '#721c24',
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
backgroundColor: authToken ? '#d4edda' : '#f8d7da',
color: authToken ? '#155724' : '#721c24',
border: `1px solid ${authToken ? '#c3e6cb' : '#f5c6cb'}`,
fontWeight: '500',
},
}, [
header ? '✓ Api token detected' : '⚠ No API token detected'
authToken ? '✓ Api token detected' : '⚠ No API token detected'
]),
header ? (
authToken ? (
button({
style: {
width: '100%',
@@ -194,7 +211,7 @@ function main() {
fontSize: '15px',
transition: 'all 0.2s ease',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
opacity: isRequestPending ? 0.5 : 1,
opacity: isRequestPending ? '0.5' : '1',
cursor: isRequestPending ? 'not-allowed' : 'pointer',
},
onclick: 'onClick',

View File

@@ -1,19 +1,12 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { execSync } from 'child_process';
// Get git commit hash at build time
const getGitCommitHash = () => {
try {
return execSync('git rev-parse --short HEAD').toString().trim();
} catch {
return 'unknown';
}
};
// Get git commit hash from GIT_HASH env var (set by CI/Docker) or fallback to 'local'
const gitHash = process.env.GIT_HASH || 'local';
export default defineConfig({
define: {
__GIT_COMMIT_HASH__: JSON.stringify(getGitCommitHash()),
__GIT_COMMIT_HASH__: JSON.stringify(gitHash),
},
plugins: [react()],
build: {

View File

@@ -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 array`);
throw new Error(`useHeaders: filter function returned undefined. expect an erray`);
}
if (!Array.isArray(result)) {
throw new Error(`useHeaders: filter function must return an array, got ${typeof result}. `);

View File

@@ -34,6 +34,10 @@ FROM debian:bookworm-slim
WORKDIR /app
# Accept build argument for git hash and set as environment variable
ARG GIT_HASH=local
ENV GIT_HASH=${GIT_HASH}
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \