feat(demo): migrate to React + TypeScript with Vite build system

- Convert demo from vanilla HTML/JS to React + TypeScript + Vite
- Add modern UI with collapsible sections, status bar, and plugin cards
- Convert plugins (twitter, swissbank, spotify) to TypeScript
- Add plugin build script with environment variable injection at build time
- Create globals.d.ts in plugin-sdk for plugin runtime type definitions
This commit is contained in:
Hendrik Eeckhaut
2026-01-05 17:27:06 +01:00
parent de13655ad4
commit e12eca63de
28 changed files with 2954 additions and 591 deletions

View File

@@ -23,7 +23,7 @@
"serve:test": "npm run serve:test --workspace=extension",
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.13 --no-logging",
"demo": "serve -l 8080 packages/demo",
"demo": "npm run dev --workspace=@tlsnotary/demo",
"tutorial": "serve -l 8080 packages/tutorial",
"docker:up": "cd packages/demo && ./start.sh -d",
"docker:down": "cd packages/demo && docker-compose down"

4
packages/demo/.env Normal file
View File

@@ -0,0 +1,4 @@
# Verifier Configuration
VITE_VERIFIER_HOST=localhost:7047
VITE_VERIFIER_PROTOCOL=http
VITE_PROXY_PROTOCOL=ws

View File

@@ -0,0 +1,4 @@
# Production environment variables
VITE_VERIFIER_HOST=verifier.tlsnotary.org
VITE_VERIFIER_PROTOCOL=https
VITE_PROXY_PROTOCOL=wss

View File

@@ -1,2 +1,3 @@
*.wasm
generated/
dist/
public/plugins/

View File

@@ -0,0 +1,52 @@
# Adding New Plugins
Adding new plugins to the demo is straightforward. Just update the `plugins.ts` file:
## Example: Adding a GitHub Plugin
```typescript
// packages/demo/src/plugins.ts
export const plugins: Record<string, Plugin> = {
// ... existing plugins ...
github: {
name: 'GitHub Profile',
description: 'Prove your GitHub contributions and profile information',
logo: '🐙', // or use emoji: '💻', '⚡', etc.
file: '/github.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
},
},
};
```
## Plugin Properties
| Property | Type | Description |
| ------------- | -------- | ------------------------------------------------------- |
| `name` | string | Display name shown in the card header |
| `description` | string | Brief description of what the plugin proves |
| `logo` | string | Emoji or character to display as the plugin icon |
| `file` | string | Path to the plugin JavaScript file |
| `parseResult` | function | Function to extract the result from the plugin response |
## Tips
- **Logo**: Use emojis for visual appeal (🔒, 🎮, 📧, 💰, etc.)
- **Description**: Keep it concise (1-2 lines) explaining what data is proven
- **File**: Place the plugin JS file in `/packages/demo/` directory
- **Name**: Use short, recognizable names
## Card Display
The plugin will automatically render as a card with:
- Large logo at the top
- Plugin name as heading
- Description text below
- "Run Plugin" button at the bottom
- Hover effects and animations
- Running state with spinner
No additional UI code needed!

View File

@@ -1,28 +1,25 @@
# Build stage - Build React app
FROM node:20-alpine AS react-builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
# Plugin generation stage
FROM rust:latest AS plugin-builder
# Build stage
FROM node:20-alpine AS builder
# Accept build arguments with defaults
ARG VERIFIER_HOST=localhost:7047
ARG SSL=false
ARG VITE_VERIFIER_URL=http://localhost:7047
ARG VITE_PROXY_URL=ws://localhost:7047/proxy?token=
WORKDIR /app
COPY *.js *.sh /app/
# Pass build args as environment variables to generate.sh
RUN VERIFIER_HOST="${VERIFIER_HOST}" SSL="${SSL}" ./generate.sh
# Copy package files and install dependencies
COPY package.json ./
RUN npm install
# Copy source files
COPY . .
# Build with environment variables
ENV VITE_VERIFIER_URL=${VITE_VERIFIER_URL}
ENV VITE_PROXY_URL=${VITE_PROXY_URL}
RUN npm run build
# Runtime stage
FROM nginx:alpine
COPY --from=react-builder /app/dist /usr/share/nginx/html
COPY --from=plugin-builder /app/generated/*.js /usr/share/nginx/html/
COPY --from=builder /app/dist /usr/share/nginx/html

View File

@@ -0,0 +1,42 @@
import { build } from 'vite';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const plugins = ['twitter', 'swissbank', 'spotify'];
// Build each plugin separately as plain ES module
for (const plugin of plugins) {
await build({
configFile: false,
build: {
lib: {
entry: path.resolve(__dirname, `src/plugins/${plugin}.plugin.ts`),
formats: ['es'],
fileName: () => `${plugin}.js`,
},
outDir: 'public/plugins',
emptyOutDir: false,
sourcemap: false,
minify: false,
rollupOptions: {
output: {
exports: 'default',
},
},
},
define: {
VITE_VERIFIER_URL: JSON.stringify(
process.env.VITE_VERIFIER_URL || 'http://localhost:7047'
),
VITE_PROXY_URL: JSON.stringify(
process.env.VITE_PROXY_URL || 'ws://localhost:7047/proxy?token='
),
},
});
console.log(`✓ Built ${plugin}.js`);
}
console.log('✓ All plugins built successfully');

View File

@@ -0,0 +1,510 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>TLSNotary Plugin test page</title>
<style>
.result {
background: #e8f5e8;
border: 2px solid #28a745;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
font-size: 18px;
display: inline-block;
}
.debug {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 12px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.plugin-buttons {
margin: 20px 0;
}
.plugin-buttons button {
margin-right: 10px;
padding: 10px 20px;
font-size: 16px;
}
.check-item {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
}
.check-item.checking {
background: #f0f8ff;
border-color: #007bff;
}
.check-item.success {
background: #f0f8f0;
border-color: #28a745;
}
.check-item.error {
background: #fff0f0;
border-color: #dc3545;
}
.status {
font-weight: bold;
margin-left: 10px;
}
.status.checking {
color: #007bff;
}
.status.success {
color: #28a745;
}
.status.error {
color: #dc3545;
}
.warning-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.warning-box h3 {
margin-top: 0;
color: #856404;
}
.console-section {
margin: 20px 0;
border: 1px solid #dee2e6;
border-radius: 8px;
background: #1e1e1e;
overflow: hidden;
}
.console-header {
background: #2d2d2d;
color: #fff;
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #3d3d3d;
}
.console-title {
font-weight: 600;
font-size: 14px;
}
.console-output {
max-height: 300px;
overflow-y: auto;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #d4d4d4;
}
.console-entry {
margin: 4px 0;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.console-entry.info {
color: #4fc3f7;
}
.console-entry.success {
color: #4caf50;
}
.console-entry.error {
color: #f44336;
}
.console-entry.warning {
color: #ff9800;
}
.console-timestamp {
color: #888;
margin-right: 8px;
}
.console-message {
color: inherit;
}
.btn-console {
background: #007bff;
color: white;
border: none;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-console:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>TLSNotary Plugin Demo</h1>
<p>
This page demonstrates TLSNotary plugins. Choose a plugin to test below.
</p>
<!-- Browser compatibility warning -->
<div id="browser-warning" class="warning-box" style="display: none;">
<h3>⚠️ Browser Compatibility</h3>
<p><strong>Unsupported Browser Detected</strong></p>
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
<p>Please switch to a supported browser to continue.</p>
</div>
<!-- System checks -->
<div>
<strong>System Checks:</strong>
<div id="check-browser" class="check-item checking">
🌐 Browser: <span class="status checking">Checking...</span>
</div>
<div id="check-extension" class="check-item checking">
🔌 Extension: <span class="status checking">Checking...</span>
</div>
<div id="check-verifier" class="check-item checking">
✅ Verifier: <span class="status checking">Checking...</span>
<div id="verifier-instructions" style="display: none; margin-top: 10px; font-size: 14px;">
<p>Start the verifier server:</p>
<code>cd packages/verifier; cargo run --release</code>
<button onclick="checkVerifier()" style="margin-left: 10px; padding: 5px 10px;">Check Again</button>
</div>
</div>
</div>
<div style="margin-top: 20px;">
<strong>Steps:</strong>
<ol>
<li>Click one of the plugin "Run" buttons below.</li>
<li>The plugin will open a new browser window with the target website.</li>
<li>Log in to the website if you are not already logged in.</li>
<li>A TLSNotary overlay will appear in the bottom right corner.</li>
<li>Click the <strong>Prove</strong> button in the overlay to start the proving process.</li>
<li>After successful proving, you can close the browser window and the results will appear on this page.</li>
</ol>
</div>
<div class="plugin-buttons" id="buttonContainer"></div>
<!-- Console Section -->
<div class="console-section">
<div class="console-header">
<div class="console-title">Console Output</div>
<div style="display: flex; gap: 10px;">
<button class="btn-console" onclick="openExtensionLogs()" style="background: #6c757d;">View Extension
Logs</button>
<button class="btn-console" onclick="clearConsole()">Clear</button>
</div>
</div>
<div class="console-output" id="consoleOutput">
<div class="console-entry info">
<span class="console-timestamp">[INFO]</span>
<span class="console-message">💡 TLSNotary proving logs will appear here in real-time. You can also view them in
the extension console by clicking "View Extension Logs" above.</span>
</div>
</div>
</div>
<script>
console.log('Testing TLSNotary plugins...');
let allChecksPass = false;
// Console functionality
function addConsoleEntry(message, type = 'info') {
const consoleOutput = document.getElementById('consoleOutput');
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `console-entry ${type}`;
const timestampSpan = document.createElement('span');
timestampSpan.className = 'console-timestamp';
timestampSpan.textContent = `[${timestamp}]`;
const messageSpan = document.createElement('span');
messageSpan.className = 'console-message';
messageSpan.textContent = message;
entry.appendChild(timestampSpan);
entry.appendChild(messageSpan);
consoleOutput.appendChild(entry);
// Auto-scroll to bottom
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
function clearConsole() {
const consoleOutput = document.getElementById('consoleOutput');
consoleOutput.innerHTML = '';
addConsoleEntry('Console cleared', 'info');
// Re-add the tip
const tipEntry = document.createElement('div');
tipEntry.className = 'console-entry info';
tipEntry.innerHTML = '<span class="console-timestamp">[INFO]</span><span class="console-message">💡 TLSNotary proving logs will appear here in real-time.</span>';
consoleOutput.insertBefore(tipEntry, consoleOutput.firstChild);
}
function openExtensionLogs() {
// Open extensions page
window.open('chrome://extensions/', '_blank');
addConsoleEntry('Opening chrome://extensions/ - Find TLSNotary extension → click "service worker" → find "offscreen.html" → click "inspect"', 'info');
}
// Listen for logs from offscreen document
window.addEventListener('message', (event) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === 'TLSN_OFFSCREEN_LOG') {
addConsoleEntry(event.data.message, event.data.level);
}
});
// Initialize console with welcome message
window.addEventListener('load', () => {
addConsoleEntry('TLSNotary Plugin Demo initialized', 'success');
});
// Check browser compatibility
function checkBrowserCompatibility() {
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
const isEdge = /Edg/.test(navigator.userAgent);
const isBrave = navigator.brave && typeof navigator.brave.isBrave === 'function';
const isChromium = /Chromium/.test(navigator.userAgent);
const isChromeBasedBrowser = isChrome || isEdge || isBrave || isChromium;
const checkDiv = document.getElementById('check-browser');
const warningDiv = document.getElementById('browser-warning');
const statusSpan = checkDiv.querySelector('.status');
if (isChromeBasedBrowser) {
checkDiv.className = 'check-item success';
statusSpan.className = 'status success';
statusSpan.textContent = '✅ Chrome-based browser detected';
return true;
} else {
checkDiv.className = 'check-item error';
statusSpan.className = 'status error';
statusSpan.textContent = '❌ Unsupported browser';
warningDiv.style.display = 'block';
return false;
}
}
// Check extension
async function checkExtension() {
const checkDiv = document.getElementById('check-extension');
const statusSpan = checkDiv.querySelector('.status');
// Wait a bit for tlsn to load if page just loaded
await new Promise(resolve => setTimeout(resolve, 1000));
if (typeof window.tlsn !== 'undefined') {
checkDiv.className = 'check-item success';
statusSpan.className = 'status success';
statusSpan.textContent = '✅ Extension installed';
return true;
} else {
checkDiv.className = 'check-item error';
statusSpan.className = 'status error';
statusSpan.innerHTML = '❌ Extension not found - <a href="chrome://extensions/" target="_blank">Install extension</a>';
return false;
}
}
// Check verifier server
async function checkVerifier() {
const checkDiv = document.getElementById('check-verifier');
const statusSpan = checkDiv.querySelector('.status');
const instructions = document.getElementById('verifier-instructions');
statusSpan.textContent = 'Checking...';
statusSpan.className = 'status checking';
checkDiv.className = 'check-item checking';
try {
const response = await fetch('http://localhost:7047/health');
if (response.ok && await response.text() === 'ok') {
checkDiv.className = 'check-item success';
statusSpan.className = 'status success';
statusSpan.textContent = '✅ Verifier running';
instructions.style.display = 'none';
return true;
} else {
throw new Error('Unexpected response');
}
} catch (error) {
checkDiv.className = 'check-item error';
statusSpan.className = 'status error';
statusSpan.textContent = '❌ Verifier not running';
instructions.style.display = 'block';
return false;
}
}
// Run all checks
async function runAllChecks() {
const browserOk = checkBrowserCompatibility();
if (!browserOk) {
allChecksPass = false;
return;
}
const extensionOk = await checkExtension();
const verifierOk = await checkVerifier();
allChecksPass = extensionOk && verifierOk;
updateButtonState();
}
// Update button state based on checks
function updateButtonState() {
const container = document.getElementById('buttonContainer');
const buttons = container.querySelectorAll('button');
buttons.forEach(button => {
button.disabled = !allChecksPass;
if (!allChecksPass) {
button.title = 'Please complete all system checks first';
} else {
button.title = '';
}
});
}
const plugins = {
twitter: {
name: 'Twitter profile Plugin',
file: 'twitter.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
}
},
swissbank: {
name: 'Swiss Bank Plugin',
file: 'swissbank.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
}
},
spotify: {
name: 'Spotify Plugin',
file: 'spotify.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
}
}
};
async function runPlugin(pluginKey) {
const plugin = plugins[pluginKey];
const button = document.getElementById(`${pluginKey}Button`);
try {
addConsoleEntry(`🎬 Starting ${plugin.name} plugin...`, 'info');
console.log(`Running ${plugin.name} plugin...`);
button.disabled = true;
button.textContent = 'Running...';
const startTime = performance.now();
const pluginCode = await fetch(plugin.file).then(r => r.text());
addConsoleEntry('🔧 Executing plugin code...', 'info');
const result = await window.tlsn.execCode(pluginCode);
const executionTime = (performance.now() - startTime).toFixed(2);
const json = JSON.parse(result);
// Create result div
const resultDiv = document.createElement('div');
resultDiv.className = 'result';
resultDiv.innerHTML = plugin.parseResult(json);
document.body.appendChild(resultDiv);
// Create header
const header = document.createElement('h3');
header.textContent = `${plugin.name} Results:`;
document.body.appendChild(header);
// Create debug div
const debugDiv = document.createElement('div');
debugDiv.className = 'debug';
debugDiv.textContent = JSON.stringify(json.results, null, 2);
document.body.appendChild(debugDiv);
addConsoleEntry(`✅ ${plugin.name} completed successfully in ${executionTime}ms`, 'success');
// Remove the button after successful execution
button.remove();
} catch (err) {
console.error(err);
// Create error div
const errorDiv = document.createElement('pre');
errorDiv.style.color = 'red';
errorDiv.textContent = err.message;
document.body.appendChild(errorDiv);
button.textContent = `Run ${plugin.name}`;
button.disabled = false;
}
}
window.addEventListener('tlsn_loaded', () => {
console.log('TLSNotary client loaded, showing plugin buttons...');
const container = document.getElementById('buttonContainer');
Object.entries(plugins).forEach(([key, plugin]) => {
const button = document.createElement('button');
button.id = `${key}Button`;
button.textContent = `Run ${plugin.name}`;
button.onclick = () => runPlugin(key);
container.appendChild(button);
});
// Update button states after creating them
updateButtonState();
});
// Run checks on page load
window.addEventListener('load', () => {
setTimeout(() => {
runAllChecks();
}, 500);
});
</script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"dev": "npm run build:plugins && vite",
"build": "npm run build:plugins && vite build",
"build:plugins": "node build-plugins.js",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},

867
packages/demo/src/App.css Normal file
View File

@@ -0,0 +1,867 @@
:root {
/* Color Palette */
--primary: #4f46e5;
--primary-dark: #4338ca;
--primary-light: #818cf8;
--secondary: #10b981;
--secondary-dark: #059669;
--error: #ef4444;
--error-light: #fee2e2;
--warning: #f59e0b;
--warning-light: #fef3c7;
--success: #10b981;
--success-light: #d1fae5;
/* Neutrals */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Spacing */
--spacing-xs: 0.5rem;
--spacing-sm: 0.75rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--gray-900);
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-xl) var(--spacing-lg);
}
/* Header Section */
.hero-section {
text-align: center;
padding: var(--spacing-2xl) 0;
margin-bottom: var(--spacing-xl);
}
.hero-title {
font-size: 3rem;
font-weight: 800;
margin: 0 0 var(--spacing-md) 0;
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-weight: 400;
}
/* Content Card */
.content-card {
background: white;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
padding: var(--spacing-2xl);
margin-bottom: var(--spacing-xl);
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--gray-900);
margin: 0 0 var(--spacing-lg) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.section-title::before {
content: '';
width: 4px;
height: 1.5rem;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
border-radius: 2px;
}
/* Steps Section */
.steps-section {
background: var(--gray-50);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.steps-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--gray-900);
margin: 0 0 var(--spacing-md) 0;
}
.steps-list {
margin: 0;
padding-left: var(--spacing-lg);
color: var(--gray-700);
line-height: 1.8;
}
.steps-list li {
margin-bottom: var(--spacing-sm);
}
.steps-list li strong {
color: var(--primary);
font-weight: 600;
}
/* Plugin Grid */
.plugin-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-lg);
margin: var(--spacing-xl) 0;
}
.plugin-card {
background: white;
border: 2px solid var(--gray-200);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
position: relative;
overflow: hidden;
}
.plugin-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.plugin-card:hover {
border-color: var(--primary);
box-shadow: var(--shadow-lg);
transform: translateY(-4px);
}
.plugin-card:hover::before {
opacity: 1;
}
.plugin-logo {
font-size: 3.5rem;
text-align: center;
margin-bottom: var(--spacing-sm);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.plugin-info {
flex: 1;
text-align: center;
}
.plugin-name {
margin: 0 0 var(--spacing-sm) 0;
font-size: 1.375rem;
font-weight: 700;
color: var(--gray-900);
}
.plugin-description {
margin: 0;
font-size: 0.9375rem;
color: var(--gray-600);
line-height: 1.6;
}
.plugin-run-btn {
width: 100%;
padding: 0.875rem 1.5rem;
font-size: 1rem;
font-weight: 600;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
box-shadow: var(--shadow-sm);
}
.plugin-run-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.plugin-run-btn:active:not(:disabled) {
transform: translateY(0);
}
.plugin-run-btn:disabled {
background: var(--gray-300);
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* System Checks */
.checks-section {
background: var(--gray-50);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.checks-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--gray-900);
margin: 0 0 var(--spacing-md) 0;
}
.check-item {
margin: var(--spacing-sm) 0;
padding: var(--spacing-md);
border-radius: var(--radius-md);
border: 1px solid var(--gray-200);
background: white;
display: flex;
align-items: center;
gap: var(--spacing-sm);
transition: all 0.2s ease;
}
.check-item.checking {
background: #eff6ff;
border-color: var(--primary-light);
}
.check-item.success {
background: var(--success-light);
border-color: var(--secondary);
}
.check-item.error {
background: var(--error-light);
border-color: var(--error);
}
.status {
font-weight: 600;
margin-left: auto;
}
.status.checking {
color: var(--primary);
}
.status.success {
color: var(--secondary);
}
.status.error {
color: var(--error);
}
/* Warning Box */
.warning-box {
background: var(--warning-light);
border: 2px solid var(--warning);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin: var(--spacing-lg) 0;
}
.warning-box h3 {
margin-top: 0;
color: #92400e;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* Console Section */
.console-section {
margin: var(--spacing-xl) 0;
border-radius: var(--radius-lg);
background: var(--gray-900);
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.console-header {
background: var(--gray-800);
color: white;
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--gray-700);
}
.console-title {
font-weight: 600;
font-size: 0.9375rem;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.console-title::before {
content: '⚡';
font-size: 1.125rem;
}
.console-output {
max-height: 350px;
overflow-y: auto;
padding: var(--spacing-md);
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Consolas', monospace;
font-size: 0.8125rem;
color: #d4d4d4;
line-height: 1.6;
}
.console-output::-webkit-scrollbar {
width: 8px;
}
.console-output::-webkit-scrollbar-track {
background: var(--gray-800);
}
.console-output::-webkit-scrollbar-thumb {
background: var(--gray-600);
border-radius: 4px;
}
.console-output::-webkit-scrollbar-thumb:hover {
background: var(--gray-500);
}
.console-entry {
margin: 0.375rem 0;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
display: flex;
gap: var(--spacing-sm);
}
.console-entry.info {
color: #60a5fa;
}
.console-entry.success {
color: #34d399;
}
.console-entry.error {
color: #f87171;
}
.console-entry.warning {
color: #fbbf24;
}
.console-timestamp {
color: var(--gray-500);
flex-shrink: 0;
font-size: 0.75rem;
}
.console-message {
color: inherit;
}
.btn-console {
background: var(--gray-700);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-console:hover {
background: var(--gray-600);
}
/* Plugin Results */
.result {
background: linear-gradient(135deg, var(--success-light) 0%, #d1fae5 100%);
border: 2px solid var(--secondary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin: var(--spacing-lg) 0;
font-size: 1.125rem;
box-shadow: var(--shadow-sm);
}
.result h3 {
margin-top: 0;
color: var(--gray-900);
font-size: 1.5rem;
font-weight: 700;
}
.debug {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin: var(--spacing-lg) 0;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Consolas', monospace;
font-size: 0.8125rem;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
color: var(--gray-800);
}
/* Responsive Design */
@media (max-width: 768px) {
.app-container {
padding: var(--spacing-lg) var(--spacing-md);
}
.hero-title {
font-size: 2rem;
}
.hero-subtitle {
font-size: 1rem;
}
.content-card {
padding: var(--spacing-lg);
}
.plugin-grid {
grid-template-columns: 1fr;
}
.console-header {
flex-direction: column;
gap: var(--spacing-sm);
align-items: stretch;
}
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.content-card {
animation: fadeIn 0.6s ease-out;
}
.plugin-card {
animation: fadeIn 0.6s ease-out backwards;
}
.plugin-card:nth-child(1) {
animation-delay: 0.1s;
}
.plugin-card:nth-child(2) {
animation-delay: 0.2s;
}
.plugin-card:nth-child(3) {
animation-delay: 0.3s;
}
.plugin-card:nth-child(4) {
animation-delay: 0.4s;
}
/* Status Bar */
.status-bar {
background: white;
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-md);
border-left: 4px solid var(--secondary);
transition: all 0.3s ease;
}
.status-bar.status-issues {
border-left-color: var(--warning);
}
.status-bar-content {
display: flex;
align-items: center;
gap: var(--spacing-lg);
flex-wrap: wrap;
}
.status-indicator {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-weight: 600;
font-size: 1.125rem;
}
.status-icon {
font-size: 1.5rem;
}
.status-items {
display: flex;
gap: var(--spacing-sm);
flex: 1;
flex-wrap: wrap;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 500;
background: var(--gray-100);
color: var(--gray-700);
}
.status-badge.ok {
background: var(--success-light);
color: var(--secondary-dark);
}
.status-badge.error {
background: var(--error-light);
color: var(--error);
}
.status-actions {
display: flex;
gap: var(--spacing-sm);
}
.btn-recheck,
.btn-details {
padding: 0.5rem 1rem;
border-radius: var(--radius-sm);
border: none;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-recheck {
background: var(--primary);
color: white;
}
.btn-recheck:hover {
background: var(--primary-dark);
}
.btn-details {
background: var(--gray-200);
color: var(--gray-700);
}
.btn-details:hover {
background: var(--gray-300);
}
.status-help {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--gray-200);
font-size: 0.875rem;
color: var(--gray-700);
}
.status-help>div {
margin: var(--spacing-xs) 0;
}
.status-help code {
background: var(--gray-100);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
font-family: 'SF Mono', 'Monaco', monospace;
font-size: 0.75rem;
}
.status-help a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.status-help a:hover {
text-decoration: underline;
}
/* Collapsible Section */
.collapsible-section {
margin: var(--spacing-lg) 0;
border: 1px solid var(--gray-200);
border-radius: var(--radius-lg);
overflow: hidden;
background: white;
box-shadow: var(--shadow-sm);
}
.collapsible-header {
width: 100%;
padding: var(--spacing-md) var(--spacing-lg);
background: var(--gray-50);
border: none;
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
transition: all 0.2s ease;
font-size: 1rem;
font-weight: 600;
color: var(--gray-900);
text-align: left;
}
.collapsible-header:hover {
background: var(--gray-100);
}
.collapsible-icon {
font-size: 0.75rem;
color: var(--gray-600);
transition: transform 0.2s ease;
}
.collapsible-title {
flex: 1;
}
.collapsible-content {
padding: var(--spacing-lg);
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Alert Box */
.alert-box {
background: #eff6ff;
border: 1px solid var(--primary-light);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--primary-dark);
}
.alert-icon {
font-size: 1.25rem;
}
/* Info Content */
.info-content {
color: var(--gray-700);
line-height: 1.7;
}
.info-content p {
margin: var(--spacing-sm) 0;
}
.info-content p:first-child {
margin-top: 0;
}
.info-content p:last-child {
margin-bottom: 0;
}
/* Steps list in collapsible */
.collapsible-content .steps-list {
margin: 0;
padding-left: var(--spacing-xl);
}
/* Plugin Card Completed State */
.plugin-card--completed {
border-color: var(--secondary);
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 30%);
}
.plugin-card--completed::before {
opacity: 1;
background: linear-gradient(90deg, var(--secondary) 0%, var(--success-light) 100%);
}
.plugin-header {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
}
.plugin-badge {
display: inline-block;
background: var(--success-light);
color: var(--secondary-dark);
font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
margin-left: var(--spacing-sm);
vertical-align: middle;
}
/* Plugin Result Section */
.plugin-result {
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--gray-200);
}
.plugin-result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-sm);
}
.plugin-result-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--secondary-dark);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.plugin-result-content {
background: var(--gray-50);
border-radius: var(--radius-md);
padding: var(--spacing-md);
font-size: 0.9375rem;
line-height: 1.6;
color: var(--gray-800);
}
.plugin-result-content strong {
color: var(--primary-dark);
}
.plugin-raw-toggle {
background: none;
border: none;
color: var(--gray-500);
font-size: 0.8125rem;
cursor: pointer;
padding: var(--spacing-xs) 0;
margin-top: var(--spacing-sm);
transition: color 0.2s ease;
}
.plugin-raw-toggle:hover {
color: var(--primary);
}
.plugin-raw-data {
background: var(--gray-800);
color: var(--gray-100);
border-radius: var(--radius-md);
padding: var(--spacing-md);
font-size: 0.8125rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
overflow-x: auto;
margin-top: var(--spacing-sm);
max-height: 300px;
overflow-y: auto;
}

View File

@@ -2,257 +2,293 @@ import { useState, useEffect, useCallback } from 'react';
import { SystemChecks } from './components/SystemChecks';
import { ConsoleOutput } from './components/Console';
import { PluginButtons } from './components/PluginButtons';
import { PluginResult } from './components/PluginResult';
import { StatusBar } from './components/StatusBar';
import { CollapsibleSection } from './components/CollapsibleSection';
import { plugins } from './plugins';
import { checkBrowserCompatibility, checkExtension, checkVerifier, formatTimestamp } from './utils';
import { ConsoleEntry, CheckStatus, PluginResult as PluginResultType } from './types';
import './App.css';
interface PluginResultData {
pluginName: string;
resultHtml: string;
debugJson: string;
resultHtml: string;
debugJson: string;
}
export function App() {
const [consoleEntries, setConsoleEntries] = useState<ConsoleEntry[]>([
{
timestamp: formatTimestamp(),
message:
'💡 TLSNotary proving logs will appear here in real-time. You can also view them in the extension console by clicking "View Extension Logs" above.',
type: 'info',
},
]);
const [browserCheck, setBrowserCheck] = useState<{ status: CheckStatus; message: string }>({
status: 'checking',
message: 'Checking...',
});
const [extensionCheck, setExtensionCheck] = useState<{ status: CheckStatus; message: string }>({
status: 'checking',
message: 'Checking...',
});
const [verifierCheck, setVerifierCheck] = useState<{
status: CheckStatus;
message: string;
showInstructions: boolean;
}>({
status: 'checking',
message: 'Checking...',
showInstructions: false,
});
const [showBrowserWarning, setShowBrowserWarning] = useState(false);
const [allChecksPass, setAllChecksPass] = useState(false);
const [runningPlugins, setRunningPlugins] = useState<Set<string>>(new Set());
const [completedPlugins, setCompletedPlugins] = useState<Set<string>>(new Set());
const [pluginResults, setPluginResults] = useState<PluginResultData[]>([]);
const addConsoleEntry = useCallback((message: string, type: ConsoleEntry['type'] = 'info') => {
setConsoleEntries((prev) => [
...prev,
{
timestamp: formatTimestamp(),
message,
type,
},
const [consoleEntries, setConsoleEntries] = useState<ConsoleEntry[]>([
{
timestamp: formatTimestamp(),
message:
'💡 TLSNotary proving logs will appear here in real-time. You can also view them in the extension console by clicking "View Extension Logs" above.',
type: 'info',
},
]);
}, []);
const handleClearConsole = useCallback(() => {
setConsoleEntries([
{
timestamp: formatTimestamp(),
message: 'Console cleared',
type: 'info',
},
{
timestamp: formatTimestamp(),
message: '💡 TLSNotary proving logs will appear here in real-time.',
type: 'info',
},
]);
}, []);
const [browserCheck, setBrowserCheck] = useState<{ status: CheckStatus; message: string }>({
status: 'checking',
message: 'Checking...',
});
const handleOpenExtensionLogs = useCallback(() => {
window.open('chrome://extensions/', '_blank');
addConsoleEntry(
'Opening chrome://extensions/ - Find TLSNotary extension → click "service worker" → find "offscreen.html" → click "inspect"',
'info'
);
}, [addConsoleEntry]);
const [extensionCheck, setExtensionCheck] = useState<{ status: CheckStatus; message: string }>({
status: 'checking',
message: 'Checking...',
});
const runAllChecks = useCallback(async () => {
// Browser check
const browserOk = checkBrowserCompatibility();
if (browserOk) {
setBrowserCheck({ status: 'success', message: '✅ Chrome-based browser detected' });
setShowBrowserWarning(false);
} else {
setBrowserCheck({ status: 'error', message: '❌ Unsupported browser' });
setShowBrowserWarning(true);
setAllChecksPass(false);
return;
}
const [verifierCheck, setVerifierCheck] = useState<{
status: CheckStatus;
message: string;
showInstructions: boolean;
}>({
status: 'checking',
message: 'Checking...',
showInstructions: false,
});
// Extension check
const extensionOk = await checkExtension();
if (extensionOk) {
setExtensionCheck({ status: 'success', message: '✅ Extension installed' });
} else {
setExtensionCheck({ status: 'error', message: '❌ Extension not found' });
}
const [showBrowserWarning, setShowBrowserWarning] = useState(false);
const [allChecksPass, setAllChecksPass] = useState(false);
const [runningPlugins, setRunningPlugins] = useState<Set<string>>(new Set());
const [pluginResults, setPluginResults] = useState<Record<string, PluginResultData>>({});
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [consoleExpanded, setConsoleExpanded] = useState(false);
// Verifier check
const verifierOk = await checkVerifier();
if (verifierOk) {
setVerifierCheck({ status: 'success', message: '✅ Verifier running', showInstructions: false });
} else {
setVerifierCheck({ status: 'error', message: '❌ Verifier not running', showInstructions: true });
}
setAllChecksPass(extensionOk && verifierOk);
}, []);
const handleRecheckVerifier = useCallback(async () => {
setVerifierCheck({ status: 'checking', message: 'Checking...', showInstructions: false });
const verifierOk = await checkVerifier();
if (verifierOk) {
setVerifierCheck({ status: 'success', message: '✅ Verifier running', showInstructions: false });
const extensionOk = extensionCheck.status === 'success';
setAllChecksPass(extensionOk && verifierOk);
} else {
setVerifierCheck({ status: 'error', message: '❌ Verifier not running', showInstructions: true });
setAllChecksPass(false);
}
}, [extensionCheck.status]);
const handleRunPlugin = useCallback(
async (pluginKey: string) => {
const plugin = plugins[pluginKey];
if (!plugin) return;
setRunningPlugins((prev) => new Set(prev).add(pluginKey));
addConsoleEntry(`🎬 Starting ${plugin.name} plugin...`, 'info');
try {
const startTime = performance.now();
const pluginCode = await fetch(plugin.file).then((r) => r.text());
addConsoleEntry('🔧 Executing plugin code...', 'info');
const result = await window.tlsn!.execCode(pluginCode);
const executionTime = (performance.now() - startTime).toFixed(2);
const json: PluginResultType = JSON.parse(result);
setPluginResults((prev) => [
...prev,
{
pluginName: plugin.name,
resultHtml: plugin.parseResult(json),
debugJson: JSON.stringify(json.results, null, 2),
},
const addConsoleEntry = useCallback((message: string, type: ConsoleEntry['type'] = 'info') => {
setConsoleEntries((prev) => [
...prev,
{
timestamp: formatTimestamp(),
message,
type,
},
]);
}, []);
addConsoleEntry(`${plugin.name} completed successfully in ${executionTime}ms`, 'success');
const handleClearConsole = useCallback(() => {
setConsoleEntries([
{
timestamp: formatTimestamp(),
message: 'Console cleared',
type: 'info',
},
{
timestamp: formatTimestamp(),
message: '💡 TLSNotary proving logs will appear here in real-time.',
type: 'info',
},
]);
}, []);
setCompletedPlugins((prev) => new Set(prev).add(pluginKey));
} catch (err) {
console.error(err);
addConsoleEntry(`❌ Error: ${err instanceof Error ? err.message : String(err)}`, 'error');
} finally {
setRunningPlugins((prev) => {
const newSet = new Set(prev);
newSet.delete(pluginKey);
return newSet;
});
}
},
[addConsoleEntry]
);
const handleOpenExtensionLogs = useCallback(() => {
window.open('chrome://extensions/', '_blank');
addConsoleEntry(
'Opening chrome://extensions/ - Find TLSNotary extension → click "service worker" → find "offscreen.html" → click "inspect"',
'info'
);
}, [addConsoleEntry]);
// Listen for tlsn_loaded event
useEffect(() => {
const handleTlsnLoaded = () => {
console.log('TLSNotary client loaded');
addConsoleEntry('TLSNotary client loaded', 'success');
};
const runAllChecks = useCallback(async () => {
// Browser check
const browserOk = checkBrowserCompatibility();
if (browserOk) {
setBrowserCheck({ status: 'success', message: '✅ Chrome-based browser detected' });
setShowBrowserWarning(false);
} else {
setBrowserCheck({ status: 'error', message: '❌ Unsupported browser' });
setShowBrowserWarning(true);
setAllChecksPass(false);
return;
}
window.addEventListener('tlsn_loaded', handleTlsnLoaded);
return () => window.removeEventListener('tlsn_loaded', handleTlsnLoaded);
}, [addConsoleEntry]);
// Extension check
const extensionOk = await checkExtension();
if (extensionOk) {
setExtensionCheck({ status: 'success', message: '✅ Extension installed' });
} else {
setExtensionCheck({ status: 'error', message: '❌ Extension not found' });
}
// Listen for offscreen logs
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
// Verifier check
const verifierOk = await checkVerifier();
if (verifierOk) {
setVerifierCheck({ status: 'success', message: '✅ Verifier running', showInstructions: false });
} else {
setVerifierCheck({ status: 'error', message: '❌ Verifier not running', showInstructions: true });
}
if (event.data?.type === 'TLSN_OFFSCREEN_LOG') {
addConsoleEntry(event.data.message, event.data.level);
}
};
setAllChecksPass(extensionOk && verifierOk);
}, []);
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [addConsoleEntry]);
const handleRecheckVerifier = useCallback(async () => {
setVerifierCheck({ status: 'checking', message: 'Checking...', showInstructions: false });
const verifierOk = await checkVerifier();
if (verifierOk) {
setVerifierCheck({ status: 'success', message: '✅ Verifier running', showInstructions: false });
const extensionOk = extensionCheck.status === 'success';
setAllChecksPass(extensionOk && verifierOk);
} else {
setVerifierCheck({ status: 'error', message: '❌ Verifier not running', showInstructions: true });
setAllChecksPass(false);
}
}, [extensionCheck.status]);
// Run checks on mount
useEffect(() => {
addConsoleEntry('TLSNotary Plugin Demo initialized', 'success');
setTimeout(() => {
runAllChecks();
}, 500);
}, [runAllChecks, addConsoleEntry]);
const handleRunPlugin = useCallback(
async (pluginKey: string) => {
const plugin = plugins[pluginKey];
if (!plugin) return;
return (
<div>
<h1>TLSNotary Plugin Demo</h1>
<p>This page demonstrates TLSNotary plugins. Choose a plugin to test below.</p>
setRunningPlugins((prev) => new Set(prev).add(pluginKey));
setConsoleExpanded(true);
<SystemChecks
checks={{
browser: browserCheck,
extension: extensionCheck,
verifier: verifierCheck,
}}
onRecheckVerifier={handleRecheckVerifier}
showBrowserWarning={showBrowserWarning}
/>
try {
const startTime = performance.now();
const pluginCode = await fetch(plugin.file).then((r) => r.text());
<div style={{ marginTop: '20px' }}>
<strong>Steps:</strong>
<ol>
<li>Click one of the plugin "Run" buttons below.</li>
<li>The plugin will open a new browser window with the target website.</li>
<li>Log in to the website if you are not already logged in.</li>
<li>A TLSNotary overlay will appear in the bottom right corner.</li>
<li>
Click the <strong>Prove</strong> button in the overlay to start the proving process.
</li>
<li>
After successful proving, you can close the browser window and the results will appear on this page.
</li>
</ol>
</div>
addConsoleEntry('🔧 Executing plugin code...', 'info');
const result = await window.tlsn!.execCode(pluginCode);
const executionTime = (performance.now() - startTime).toFixed(2);
<PluginButtons
plugins={plugins}
runningPlugins={runningPlugins}
completedPlugins={completedPlugins}
allChecksPass={allChecksPass}
onRunPlugin={handleRunPlugin}
/>
const json: PluginResultType = JSON.parse(result);
<ConsoleOutput
entries={consoleEntries}
onClear={handleClearConsole}
onOpenExtensionLogs={handleOpenExtensionLogs}
/>
setPluginResults((prev) => ({
...prev,
[pluginKey]: {
resultHtml: plugin.parseResult(json),
debugJson: JSON.stringify(json.results, null, 2),
},
}));
{pluginResults.map((result, index) => (
<PluginResult key={index} {...result} />
))}
</div>
);
addConsoleEntry(`${plugin.name} completed successfully in ${executionTime}ms`, 'success');
} catch (err) {
console.error(err);
addConsoleEntry(`❌ Error: ${err instanceof Error ? err.message : String(err)}`, 'error');
} finally {
setRunningPlugins((prev) => {
const newSet = new Set(prev);
newSet.delete(pluginKey);
return newSet;
});
}
},
[addConsoleEntry]
);
// Listen for tlsn_loaded event
useEffect(() => {
const handleTlsnLoaded = () => {
console.log('TLSNotary client loaded');
addConsoleEntry('TLSNotary client loaded', 'success');
};
window.addEventListener('tlsn_loaded', handleTlsnLoaded);
return () => window.removeEventListener('tlsn_loaded', handleTlsnLoaded);
}, [addConsoleEntry]);
// Listen for offscreen logs
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === 'TLSN_OFFSCREEN_LOG') {
addConsoleEntry(event.data.message, event.data.level);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [addConsoleEntry]);
// Run checks on mount
useEffect(() => {
addConsoleEntry('TLSNotary Plugin Demo initialized', 'success');
setTimeout(() => {
runAllChecks();
}, 500);
}, [runAllChecks, addConsoleEntry]);
return (
<div className="app-container">
<div className="hero-section">
<h1 className="hero-title">TLSNotary Plugin Demo</h1>
<p className="hero-subtitle">
Prove your data with cryptographic verification
</p>
</div>
<StatusBar
browserOk={browserCheck.status === 'success'}
extensionOk={extensionCheck.status === 'success'}
verifierOk={verifierCheck.status === 'success'}
onRecheckVerifier={handleRecheckVerifier}
onShowDetails={() => setShowDetailsModal(!showDetailsModal)}
/>
{showDetailsModal && (
<div className="content-card" style={{ marginTop: 'var(--spacing-lg)' }}>
<div className="checks-section">
<div className="checks-title">System Status Details</div>
<SystemChecks
checks={{
browser: browserCheck,
extension: extensionCheck,
verifier: verifierCheck,
}}
onRecheckVerifier={handleRecheckVerifier}
showBrowserWarning={showBrowserWarning}
/>
</div>
</div>
)}
<div className="content-card">
<h2 className="section-title">Available Plugins</h2>
{!allChecksPass && (
<div className="alert-box">
<span className="alert-icon"></span>
<span>Complete system setup above to run plugins</span>
</div>
)}
<PluginButtons
plugins={plugins}
runningPlugins={runningPlugins}
pluginResults={pluginResults}
allChecksPass={allChecksPass}
onRunPlugin={handleRunPlugin}
/>
<CollapsibleSection title="How to Use" defaultExpanded={false}>
<ol className="steps-list">
<li>Select a plugin from the cards above</li>
<li>Click the <strong>Run Plugin</strong> button</li>
<li>A new browser window will open with the target website</li>
<li>Log in to the website if needed</li>
<li>A TLSNotary overlay will appear in the bottom right corner</li>
<li>Click the <strong>Prove</strong> button to start verification</li>
<li>Results will appear below when complete</li>
</ol>
</CollapsibleSection>
<CollapsibleSection title="What is TLSNotary?" defaultExpanded={false}>
<div className="info-content">
<p>
TLSNotary is a protocol that allows you to create cryptographic proofs of data from any
website. These proofs can be verified by anyone without revealing sensitive information.
</p>
<p>
Each plugin demonstrates how to prove specific data from popular services like Twitter,
Spotify, and online banking platforms.
</p>
</div>
</CollapsibleSection>
</div>
<CollapsibleSection title="Console Output" expanded={consoleExpanded}>
<ConsoleOutput
entries={consoleEntries}
onClear={handleClearConsole}
onOpenExtensionLogs={handleOpenExtensionLogs}
/>
</CollapsibleSection>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react';
interface CollapsibleSectionProps {
title: string;
defaultExpanded?: boolean;
expanded?: boolean;
children: React.ReactNode;
}
export function CollapsibleSection({ title, defaultExpanded = false, expanded, children }: CollapsibleSectionProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
useEffect(() => {
if (expanded !== undefined) {
setIsExpanded(expanded);
}
}, [expanded]);
return (
<div className="collapsible-section">
<button className="collapsible-header" onClick={() => setIsExpanded(!isExpanded)}>
<span className="collapsible-icon">{isExpanded ? '▼' : '▶'}</span>
<span className="collapsible-title">{title}</span>
</button>
{isExpanded && <div className="collapsible-content">{children}</div>}
</div>
);
}

View File

@@ -1,45 +1,45 @@
import './styles.css';
interface ConsoleEntryProps {
timestamp: string;
message: string;
type: 'info' | 'success' | 'error' | 'warning';
timestamp: string;
message: string;
type: 'info' | 'success' | 'error' | 'warning';
}
export function ConsoleEntry({ timestamp, message, type }: ConsoleEntryProps) {
return (
<div className={`console-entry ${type}`}>
<span className="console-timestamp">[{timestamp}]</span>
<span className="console-message">{message}</span>
</div>
);
return (
<div className={`console-entry ${type}`}>
<span className="console-timestamp">[{timestamp}]</span>
<span className="console-message">{message}</span>
</div>
);
}
interface ConsoleOutputProps {
entries: ConsoleEntryProps[];
onClear: () => void;
onOpenExtensionLogs: () => void;
entries: ConsoleEntryProps[];
onClear: () => void;
onOpenExtensionLogs: () => void;
}
export function ConsoleOutput({ entries, onClear, onOpenExtensionLogs }: ConsoleOutputProps) {
return (
<div className="console-section">
<div className="console-header">
<div className="console-title">Console Output</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button className="btn-console" onClick={onOpenExtensionLogs} style={{ background: '#6c757d' }}>
View Extension Logs
</button>
<button className="btn-console" onClick={onClear}>
Clear
</button>
return (
<div className="console-section">
<div className="console-header">
<div className="console-title">Console Output</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button className="btn-console" onClick={onOpenExtensionLogs} style={{ background: '#6c757d' }}>
View Extension Logs
</button>
<button className="btn-console" onClick={onClear}>
Clear
</button>
</div>
</div>
<div className="console-output" id="consoleOutput">
{entries.map((entry, index) => (
<ConsoleEntry key={index} {...entry} />
))}
</div>
</div>
</div>
<div className="console-output" id="consoleOutput">
{entries.map((entry, index) => (
<ConsoleEntry key={index} {...entry} />
))}
</div>
</div>
);
);
}

View File

@@ -1,38 +1,100 @@
import { useState } from 'react';
import { Plugin } from '../types';
import './styles.css';
interface PluginResultData {
resultHtml: string;
debugJson: string;
}
interface PluginButtonsProps {
plugins: Record<string, Plugin>;
runningPlugins: Set<string>;
completedPlugins: Set<string>;
allChecksPass: boolean;
onRunPlugin: (pluginKey: string) => void;
plugins: Record<string, Plugin>;
runningPlugins: Set<string>;
pluginResults: Record<string, PluginResultData>;
allChecksPass: boolean;
onRunPlugin: (pluginKey: string) => void;
}
export function PluginButtons({
plugins,
runningPlugins,
completedPlugins,
allChecksPass,
onRunPlugin,
plugins,
runningPlugins,
pluginResults,
allChecksPass,
onRunPlugin,
}: PluginButtonsProps) {
return (
<div className="plugin-buttons">
{Object.entries(plugins).map(([key, plugin]) => {
if (completedPlugins.has(key)) return null;
const [expandedRawData, setExpandedRawData] = useState<Set<string>>(new Set());
const isRunning = runningPlugins.has(key);
return (
<button
key={key}
disabled={!allChecksPass || isRunning}
onClick={() => onRunPlugin(key)}
title={!allChecksPass ? 'Please complete all system checks first' : ''}
>
{isRunning ? 'Running...' : `Run ${plugin.name}`}
</button>
);
})}
</div>
);
const toggleRawData = (key: string) => {
setExpandedRawData((prev) => {
const newSet = new Set(prev);
if (newSet.has(key)) {
newSet.delete(key);
} else {
newSet.add(key);
}
return newSet;
});
};
return (
<div className="plugin-grid">
{Object.entries(plugins).map(([key, plugin]) => {
const isRunning = runningPlugins.has(key);
const result = pluginResults[key];
const hasResult = !!result;
return (
<div key={key} className={`plugin-card ${hasResult ? 'plugin-card--completed' : ''}`}>
<div className="plugin-header">
<div className="plugin-logo">{plugin.logo}</div>
<div className="plugin-info">
<h3 className="plugin-name">
{plugin.name}
{hasResult && <span className="plugin-badge"> Verified</span>}
</h3>
<p className="plugin-description">{plugin.description}</p>
</div>
</div>
<button
className="plugin-run-btn"
disabled={!allChecksPass || isRunning}
onClick={() => onRunPlugin(key)}
title={!allChecksPass ? 'Please complete all system checks first' : ''}
>
{isRunning ? (
<>
<span className="spinner"></span> Running...
</>
) : hasResult ? (
'↻ Run Again'
) : (
'▶ Run Plugin'
)}
</button>
{hasResult && (
<div className="plugin-result">
<div className="plugin-result-header">
<span className="plugin-result-title">Result</span>
</div>
<div
className="plugin-result-content"
dangerouslySetInnerHTML={{ __html: result.resultHtml }}
/>
<button
className="plugin-raw-toggle"
onClick={() => toggleRawData(key)}
>
{expandedRawData.has(key) ? '▼ Hide Raw Data' : '▶ Show Raw Data'}
</button>
{expandedRawData.has(key) && (
<pre className="plugin-raw-data">{result.debugJson}</pre>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,17 +0,0 @@
import './styles.css';
interface PluginResultProps {
pluginName: string;
resultHtml: string;
debugJson: string;
}
export function PluginResult({ pluginName, resultHtml, debugJson }: PluginResultProps) {
return (
<>
<h3>{pluginName} Results:</h3>
<div className="result" dangerouslySetInnerHTML={{ __html: resultHtml }} />
<div className="debug">{debugJson}</div>
</>
);
}

View File

@@ -0,0 +1,80 @@
interface StatusBarProps {
browserOk: boolean;
extensionOk: boolean;
verifierOk: boolean;
onRecheckVerifier: () => void;
onShowDetails: () => void;
}
export function StatusBar({
browserOk,
extensionOk,
verifierOk,
onRecheckVerifier,
onShowDetails,
}: StatusBarProps) {
const allOk = browserOk && extensionOk && verifierOk;
const someIssues = !allOk;
return (
<div className={`status-bar ${allOk ? 'status-ready' : 'status-issues'}`}>
<div className="status-bar-content">
<div className="status-indicator">
{allOk ? (
<>
<span className="status-icon"></span>
<span className="status-text">System Ready</span>
</>
) : (
<>
<span className="status-icon"></span>
<span className="status-text">Setup Required</span>
</>
)}
</div>
<div className="status-items">
<div className={`status-badge ${browserOk ? 'ok' : 'error'}`}>
Browser: {browserOk ? '✓' : '✗'}
</div>
<div className={`status-badge ${extensionOk ? 'ok' : 'error'}`}>
Extension: {extensionOk ? '✓' : '✗'}
</div>
<div className={`status-badge ${verifierOk ? 'ok' : 'error'}`}>
Verifier: {verifierOk ? '✓' : '✗'}
</div>
</div>
<div className="status-actions">
{!verifierOk && (
<button className="btn-recheck" onClick={onRecheckVerifier}>
Recheck
</button>
)}
<button className="btn-details" onClick={onShowDetails}>
Details
</button>
</div>
</div>
{someIssues && (
<div className="status-help">
{!browserOk && <div>Please use a Chrome-based browser (Chrome, Edge, Brave)</div>}
{!extensionOk && (
<div>
TLSNotary extension not detected.{' '}
<a href="chrome://extensions/" target="_blank" rel="noopener noreferrer">
Install extension
</a>
</div>
)}
{!verifierOk && (
<div>
Verifier server not running. Start it with: <code>cd packages/verifier; cargo run --release</code>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -1,85 +1,85 @@
import { CheckStatus } from '../types';
import './styles.css';
interface CheckItemProps {
id: string;
icon: string;
label: string;
status: CheckStatus;
message: string;
showInstructions?: boolean;
onRecheck?: () => void;
id: string;
icon: string;
label: string;
status: CheckStatus;
message: string;
showInstructions?: boolean;
onRecheck?: () => void;
}
export function CheckItem({ icon, label, status, message, showInstructions, onRecheck }: CheckItemProps) {
return (
<div className={`check-item ${status}`}>
{icon} {label}: <span className={`status ${status}`}>{message}</span>
{showInstructions && (
<div style={{ marginTop: '10px', fontSize: '14px' }}>
<p>Start the verifier server:</p>
<code>cd packages/verifier; cargo run --release</code>
{onRecheck && (
<button onClick={onRecheck} style={{ marginLeft: '10px', padding: '5px 10px' }}>
Check Again
</button>
)}
return (
<div className={`check-item ${status}`}>
{icon} {label}: <span className={`status ${status}`}>{message}</span>
{showInstructions && (
<div style={{ marginTop: '10px', fontSize: '14px' }}>
<p>Start the verifier server:</p>
<code>cd packages/verifier; cargo run --release</code>
{onRecheck && (
<button onClick={onRecheck} style={{ marginLeft: '10px', padding: '5px 10px' }}>
Check Again
</button>
)}
</div>
)}
</div>
)}
</div>
);
);
}
interface SystemChecksProps {
checks: {
browser: { status: CheckStatus; message: string };
extension: { status: CheckStatus; message: string };
verifier: { status: CheckStatus; message: string; showInstructions: boolean };
};
onRecheckVerifier: () => void;
showBrowserWarning: boolean;
checks: {
browser: { status: CheckStatus; message: string };
extension: { status: CheckStatus; message: string };
verifier: { status: CheckStatus; message: string; showInstructions: boolean };
};
onRecheckVerifier: () => void;
showBrowserWarning: boolean;
}
export function SystemChecks({ checks, onRecheckVerifier, showBrowserWarning }: SystemChecksProps) {
return (
<>
{showBrowserWarning && (
<div className="warning-box">
<h3> Browser Compatibility</h3>
<p>
<strong>Unsupported Browser Detected</strong>
</p>
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
<p>Please switch to a supported browser to continue.</p>
</div>
)}
return (
<>
{showBrowserWarning && (
<div className="warning-box">
<h3> Browser Compatibility</h3>
<p>
<strong>Unsupported Browser Detected</strong>
</p>
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
<p>Please switch to a supported browser to continue.</p>
</div>
)}
<div>
<strong>System Checks:</strong>
<CheckItem
id="check-browser"
icon="🌐"
label="Browser"
status={checks.browser.status}
message={checks.browser.message}
/>
<CheckItem
id="check-extension"
icon="🔌"
label="Extension"
status={checks.extension.status}
message={checks.extension.message}
/>
<CheckItem
id="check-verifier"
icon="✅"
label="Verifier"
status={checks.verifier.status}
message={checks.verifier.message}
showInstructions={checks.verifier.showInstructions}
onRecheck={onRecheckVerifier}
/>
</div>
</>
);
<div>
<strong>System Checks:</strong>
<CheckItem
id="check-browser"
icon="🌐"
label="Browser"
status={checks.browser.status}
message={checks.browser.message}
/>
<CheckItem
id="check-extension"
icon="🔌"
label="Extension"
status={checks.extension.status}
message={checks.extension.message}
/>
<CheckItem
id="check-verifier"
icon="✅"
label="Verifier"
status={checks.verifier.status}
message={checks.verifier.message}
showInstructions={checks.verifier.showInstructions}
onRecheck={onRecheckVerifier}
/>
</div>
</>
);
}

View File

@@ -1,162 +0,0 @@
.result {
background: #e8f5e8;
border: 2px solid #28a745;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
font-size: 18px;
display: inline-block;
}
.debug {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 12px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.plugin-buttons {
margin: 20px 0;
}
.plugin-buttons button {
margin-right: 10px;
padding: 10px 20px;
font-size: 16px;
}
.check-item {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
}
.check-item.checking {
background: #f0f8ff;
border-color: #007bff;
}
.check-item.success {
background: #f0f8f0;
border-color: #28a745;
}
.check-item.error {
background: #fff0f0;
border-color: #dc3545;
}
.status {
font-weight: bold;
margin-left: 10px;
}
.status.checking {
color: #007bff;
}
.status.success {
color: #28a745;
}
.status.error {
color: #dc3545;
}
.warning-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.warning-box h3 {
margin-top: 0;
color: #856404;
}
.console-section {
margin: 20px 0;
border: 1px solid #dee2e6;
border-radius: 8px;
background: #1e1e1e;
overflow: hidden;
}
.console-header {
background: #2d2d2d;
color: #fff;
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #3d3d3d;
}
.console-title {
font-weight: 600;
font-size: 14px;
}
.console-output {
max-height: 300px;
overflow-y: auto;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #d4d4d4;
}
.console-entry {
margin: 4px 0;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.console-entry.info {
color: #4fc3f7;
}
.console-entry.success {
color: #4caf50;
}
.console-entry.error {
color: #f44336;
}
.console-entry.warning {
color: #ff9800;
}
.console-timestamp {
color: #888;
margin-right: 8px;
}
.console-message {
color: inherit;
}
.btn-console {
background: #007bff;
color: white;
border: none;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-console:hover {
background: #0056b3;
}

View File

@@ -0,0 +1,11 @@
// Environment configuration helper
// Reads from Vite's import.meta.env (populated from .env files)
const VERIFIER_HOST = (import.meta as any).env.VITE_VERIFIER_HOST || 'localhost:7047';
const VERIFIER_PROTOCOL = (import.meta as any).env.VITE_VERIFIER_PROTOCOL || 'http';
const PROXY_PROTOCOL = (import.meta as any).env.VITE_PROXY_PROTOCOL || 'ws';
export const config = {
verifierUrl: `${VERIFIER_PROTOCOL}://${VERIFIER_HOST}`,
getProxyUrl: (host: string) => `${PROXY_PROTOCOL}://${VERIFIER_HOST}/proxy?token=${host}`,
};

View File

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import { App } from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -2,22 +2,28 @@ import { Plugin } from './types';
export const plugins: Record<string, Plugin> = {
twitter: {
name: 'Twitter profile Plugin',
file: '/twitter.js',
name: 'Twitter Profile',
description: 'Prove your Twitter profile information with cryptographic verification',
logo: '𝕏',
file: '/plugins/twitter.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
},
},
swissbank: {
name: 'Swiss Bank Plugin',
file: '/swissbank.js',
name: 'Swiss Bank',
description: 'Verify your Swiss bank account balance securely and privately',
logo: '🏦',
file: '/plugins/swissbank.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
},
},
spotify: {
name: 'Spotify Plugin',
file: '/spotify.js',
name: 'Spotify',
description: 'Prove your Spotify listening history and music preferences',
logo: '🎵',
file: '/plugins/spotify.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
},

View File

@@ -0,0 +1,212 @@
/// <reference types="@tlsn/plugin-sdk/src/globals" />
const config = {
name: 'Spotify Top Artist',
description: 'This plugin will prove your top artist on Spotify.',
};
// @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 = 'api.spotify.com';
const ui = 'https://developer.spotify.com/';
const top_artist_path = '/v1/me/top/artists?time_range=medium_term&limit=1';
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
const [header] = useHeaders(headers => {
return headers.filter(header => header.url.includes(`https://${api}`));
});
const headers = {
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
Host: api,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: `https://${api}${top_artist_path}`,
method: 'GET',
headers: headers,
},
{
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + api,
maxRecvData: 2400,
maxSentData: 600,
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', },
},
]
}
);
done(JSON.stringify(resp));
}
function expandUI() {
setState('isMinimized', false);
}
function minimizeUI() {
setState('isMinimized', true);
}
function main() {
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
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: '#1DB954',
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: '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',
},
}, [
div({
style: {
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
padding: '12px 16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: 'white',
}
}, [
div({
style: {
fontWeight: '600',
fontSize: '16px',
}
}, ['Spotify Top Artist']),
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 ? '#d4edda' : '#f8d7da',
color: header ? '#155724' : '#721c24',
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
fontWeight: '500',
},
}, [
header ? '✓ Api token detected' : '⚠ No API token detected'
]),
header ? (
button({
style: {
width: '100%',
padding: '12px 24px',
borderRadius: '6px',
border: 'none',
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
color: 'white',
fontWeight: '600',
fontSize: '15px',
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'])
) : (
div({
style: {
textAlign: 'center',
color: '#666',
padding: '12px',
backgroundColor: '#fff3cd',
borderRadius: '6px',
border: '1px solid #ffeaa7',
}
}, ['Please login to Spotify to continue'])
)
])
]);
}
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};

View File

@@ -0,0 +1,246 @@
/// <reference types="@tlsn/plugin-sdk/src/globals" />
const config = {
name: 'Swiss Bank Prover',
description: 'This plugin will prove your Swiss Bank account balance.',
};
// Environment variables injected at build time
// @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 host = 'swissbank.tlsnotary.org';
const ui_path = '/account';
const path = '/balances';
const url = `https://${host}${path}`;
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
const [header] = useHeaders((headers: any[]) => {
console.log('Intercepted headers:', headers);
return headers.filter(header => header.url.includes(`https://${host}`));
});
const headers = {
'cookie': header.requestHeaders.find((header: any) => header.name === 'Cookie')?.value,
Host: host,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: url,
method: 'GET',
headers: headers,
},
{
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + 'swissbank.tlsnotary.org',
maxRecvData: 460,
maxSentData: 180,
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' },
},
],
}
);
done(JSON.stringify(resp));
}
function expandUI() {
setState('isMinimized', false);
}
function minimizeUI() {
setState('isMinimized', true);
}
function main() {
const [header] = useHeaders((headers: any[]) =>
headers.filter(header => header.url.includes(`https://${host}${ui_path}`))
);
const hasNecessaryHeader = header?.requestHeaders.some((h: any) => h.name === 'Cookie');
const isMinimized = useState('isMinimized', false);
const isRequestPending = useState('isRequestPending', false);
useEffect(() => {
openWindow(`https://${host}${ui_path}`);
}, []);
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',
},
['🔐']
);
}
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',
},
},
[
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',
},
},
['Swiss Bank 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',
},
['']
),
]
),
div(
{
style: {
padding: '20px',
backgroundColor: '#f8f9fa',
},
},
[
div(
{
style: {
marginBottom: '16px',
padding: '12px',
borderRadius: '6px',
backgroundColor: header ? '#d4edda' : '#f8d7da',
color: header ? '#155724' : '#721c24',
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
fontWeight: '500',
},
},
[hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected']
),
hasNecessaryHeader
? 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',
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']
)
: div(
{
style: {
textAlign: 'center',
color: '#666',
padding: '12px',
backgroundColor: '#fff3cd',
borderRadius: '6px',
border: '1px solid #ffeaa7',
},
},
['Please login to continue']
),
]
),
]
);
}
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};

View File

@@ -0,0 +1,266 @@
/// <reference types="@tlsn/plugin-sdk/src/globals" />
// =============================================================================
// 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.',
};
// Environment variables injected at build time
// @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;
// =============================================================================
// 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.
*/
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
const [header] = useHeaders((headers: any[]) => {
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
});
const headers = {
'cookie': header.requestHeaders.find((header: any) => header.name === 'Cookie')?.value,
'x-csrf-token': header.requestHeaders.find((header: any) => header.name === 'x-csrf-token')?.value,
'x-client-transaction-id': header.requestHeaders.find((header: any) => header.name === 'x-client-transaction-id')?.value,
Host: 'api.x.com',
authorization: header.requestHeaders.find((header: any) => header.name === 'authorization')?.value,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: headers,
},
{
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + 'api.x.com',
maxRecvData: 4000,
maxSentData: 2000,
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: 'screen_name',
},
},
],
}
);
done(JSON.stringify(resp));
}
function expandUI() {
setState('isMinimized', false);
}
function minimizeUI() {
setState('isMinimized', true);
}
// =============================================================================
// MAIN UI FUNCTION
// =============================================================================
function main() {
const [header] = useHeaders((headers: any[]) =>
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);
useEffect(() => {
openWindow('https://x.com');
}, []);
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',
},
['🔐']
);
}
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',
},
},
[
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',
},
['']
),
]
),
div(
{
style: {
padding: '20px',
backgroundColor: '#f8f9fa',
},
},
[
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']
),
header
? 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',
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']
)
: 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
// =============================================================================
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};

View File

@@ -1,43 +1,45 @@
export interface Plugin {
name: string;
file: string;
parseResult: (json: PluginResult) => string;
name: string;
description: string;
logo: string;
file: string;
parseResult: (json: PluginResult) => string;
}
export interface PluginResult {
results: Array<{
value: string;
}>;
results: Array<{
value: string;
}>;
}
export interface ConsoleEntry {
timestamp: string;
message: string;
type: 'info' | 'success' | 'error' | 'warning';
timestamp: string;
message: string;
type: 'info' | 'success' | 'error' | 'warning';
}
export type CheckStatus = 'checking' | 'success' | 'error';
export interface SystemCheck {
id: string;
label: string;
status: CheckStatus;
message: string;
showInstructions?: boolean;
id: string;
label: string;
status: CheckStatus;
message: string;
showInstructions?: boolean;
}
declare global {
interface Window {
tlsn?: {
execCode: (code: string) => Promise<string>;
};
}
interface Window {
tlsn?: {
execCode: (code: string) => Promise<string>;
};
}
interface Navigator {
brave?: {
isBrave: () => Promise<boolean>;
};
}
interface Navigator {
brave?: {
isBrave: () => Promise<boolean>;
};
}
}
export {};
export { };

View File

@@ -25,6 +25,9 @@
"include": [
"src"
],
"exclude": [
"src/plugins/**/*.ts"
],
"references": [
{
"path": "./tsconfig.node.json"

View File

@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"isolatedModules": false
},
"include": [
"src/plugins/**/*.ts",
"src/plugins/plugin-globals.d.ts"
],
"exclude": []
}

97
packages/plugin-sdk/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,97 @@
/**
* Global type declarations for TLSNotary plugin runtime environment
*
* These functions are injected at runtime by the plugin sandbox.
* Import this file in your plugin to get TypeScript support:
*
* /// <reference types="@tlsn/plugin-sdk/globals" />
*/
import type {
InterceptedRequest,
InterceptedRequestHeader,
Handler,
DomOptions,
DomJson,
} from './types';
declare global {
/**
* Create a div element
*/
function div(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
function div(children?: (DomJson | string)[]): DomJson;
/**
* Create a button element
*/
function button(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
function button(children?: (DomJson | string)[]): DomJson;
/**
* Get or initialize state value (React-like useState)
*/
function useState<T>(key: string, initialValue: T): T;
/**
* Update state value
*/
function setState<T>(key: string, value: T): void;
/**
* Run side effect when dependencies change (React-like useEffect)
*/
function useEffect(effect: () => void, deps: any[]): void;
/**
* Subscribe to intercepted HTTP headers
*/
function useHeaders(
filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[],
): [InterceptedRequestHeader | undefined];
/**
* Subscribe to intercepted HTTP requests
*/
function useRequests(
filter: (requests: InterceptedRequest[]) => InterceptedRequest[],
): [InterceptedRequest | undefined];
/**
* Open a new browser window for user interaction
*/
function openWindow(
url: string,
options?: {
width?: number;
height?: number;
showOverlay?: boolean;
},
): Promise<void>;
/**
* Generate a TLS proof for an HTTP request
*/
function prove(
requestOptions: {
url: string;
method: string;
headers: Record<string, string>;
body?: string;
},
proverOptions: {
verifierUrl: string;
proxyUrl: string;
maxRecvData?: number;
maxSentData?: number;
handlers: Handler[];
},
): Promise<any>;
/**
* Complete plugin execution and return result
*/
function done(result?: any): void;
}
export { };