mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-09 15:18:09 -05:00
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:
@@ -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
4
packages/demo/.env
Normal file
@@ -0,0 +1,4 @@
|
||||
# Verifier Configuration
|
||||
VITE_VERIFIER_HOST=localhost:7047
|
||||
VITE_VERIFIER_PROTOCOL=http
|
||||
VITE_PROXY_PROTOCOL=ws
|
||||
4
packages/demo/.env.production
Normal file
4
packages/demo/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# Production environment variables
|
||||
VITE_VERIFIER_HOST=verifier.tlsnotary.org
|
||||
VITE_VERIFIER_PROTOCOL=https
|
||||
VITE_PROXY_PROTOCOL=wss
|
||||
3
packages/demo/.gitignore
vendored
3
packages/demo/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*.wasm
|
||||
generated/
|
||||
dist/
|
||||
public/plugins/
|
||||
52
packages/demo/ADDING_PLUGINS.md
Normal file
52
packages/demo/ADDING_PLUGINS.md
Normal 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!
|
||||
@@ -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
|
||||
|
||||
42
packages/demo/build-plugins.js
Normal file
42
packages/demo/build-plugins.js
Normal 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');
|
||||
510
packages/demo/index.html.backup
Normal file
510
packages/demo/index.html.backup
Normal 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>
|
||||
@@ -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
867
packages/demo/src/App.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
28
packages/demo/src/components/CollapsibleSection.tsx
Normal file
28
packages/demo/src/components/CollapsibleSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
packages/demo/src/components/StatusBar.tsx
Normal file
80
packages/demo/src/components/StatusBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
11
packages/demo/src/config.ts
Normal file
11
packages/demo/src/config.ts
Normal 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}`,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
212
packages/demo/src/plugins/spotify.plugin.ts
Normal file
212
packages/demo/src/plugins/spotify.plugin.ts
Normal 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,
|
||||
};
|
||||
246
packages/demo/src/plugins/swissbank.plugin.ts
Normal file
246
packages/demo/src/plugins/swissbank.plugin.ts
Normal 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,
|
||||
};
|
||||
266
packages/demo/src/plugins/twitter.plugin.ts
Normal file
266
packages/demo/src/plugins/twitter.plugin.ts
Normal 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,
|
||||
};
|
||||
@@ -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 { };
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/plugins/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
|
||||
15
packages/demo/tsconfig.plugins.json
Normal file
15
packages/demo/tsconfig.plugins.json
Normal 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
97
packages/plugin-sdk/src/globals.d.ts
vendored
Normal 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 { };
|
||||
Reference in New Issue
Block a user