mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-14 09:37:59 -05:00
Compare commits
12 Commits
duolingo
...
ts-plugin-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bfc197316 | ||
|
|
78cec7651b | ||
|
|
40bf2cec82 | ||
|
|
d6d80cf277 | ||
|
|
449d2843ea | ||
|
|
8d93ff679e | ||
|
|
2c1fc6daad | ||
|
|
1cdf314e8d | ||
|
|
d52c7eb43f | ||
|
|
7fc1af8501 | ||
|
|
eb18485361 | ||
|
|
5a3a844527 |
3083
package-lock.json
generated
3083
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-monorepo",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"private": true,
|
||||
"description": "TLSN Extension monorepo with plugin SDK",
|
||||
"license": "MIT",
|
||||
@@ -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"
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^1.0.0"
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"happy-dom": "20.0.11",
|
||||
"vite": "7.3.0",
|
||||
"webpack-dev-server": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { Logger, LogLevel, DEFAULT_LOG_LEVEL } from './index';
|
||||
|
||||
describe('Logger', () => {
|
||||
@@ -11,6 +11,10 @@ describe('Logger', () => {
|
||||
logger.init(DEFAULT_LOG_LEVEL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('LogLevel', () => {
|
||||
it('should have correct hierarchy values', () => {
|
||||
expect(LogLevel.DEBUG).toBe(0);
|
||||
|
||||
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,17 +1,25 @@
|
||||
# Build stage
|
||||
FROM rust:latest AS builder
|
||||
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 index.html *.ico *.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=builder /app/generated /usr/share/nginx/html
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
@@ -57,14 +57,29 @@ You can use the websocketproxy hosted by the TLSNotary team, or run your own pro
|
||||
|
||||
## 4. Launch the demo
|
||||
|
||||
### Development with React
|
||||
|
||||
This demo is built with React + TypeScript + Vite. To run it locally:
|
||||
|
||||
```bash
|
||||
cd packages/demo
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The demo will open at `http://localhost:3000` in your browser with the TLSNotary extension.
|
||||
|
||||
### Docker Setup
|
||||
|
||||
Run the demo with `npm run demo` from the repository root, or run it with docker using `npm run docker:up`.
|
||||
|
||||
### Manual Setup
|
||||
#### Manual Docker Setup
|
||||
|
||||
If you want to run the scripts manually:
|
||||
|
||||
```bash
|
||||
cd packages/demo
|
||||
npm run build # Build the React app first
|
||||
./generate.sh && ./start.sh
|
||||
```
|
||||
|
||||
@@ -72,14 +87,16 @@ The demo uses two scripts:
|
||||
- **`generate.sh`** - Generates plugin files with configured verifier URLs
|
||||
- **`start.sh`** - Starts Docker Compose services
|
||||
|
||||
### Environment Variables
|
||||
#### Environment Variables
|
||||
|
||||
Configure for different environments:
|
||||
```bash
|
||||
# Local development (default)
|
||||
npm run build
|
||||
./generate.sh && ./start.sh
|
||||
|
||||
# Production with SSL
|
||||
npm run build
|
||||
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
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');
|
||||
@@ -1,510 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TLSNotary Plugin Demo</title>
|
||||
</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>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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>
|
||||
26
packages/demo/package.json
Normal file
26
packages/demo/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@tlsnotary/demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"happy-dom": "20.0.11",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"webpack-dev-server": "5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
1228
packages/demo/src/App.css
Normal file
1228
packages/demo/src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
285
packages/demo/src/App.tsx
Normal file
285
packages/demo/src/App.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { SystemChecks } from './components/SystemChecks';
|
||||
import { ConsoleOutput } from './components/Console';
|
||||
import { PluginButtons } from './components/PluginButtons';
|
||||
import { StatusBar } from './components/StatusBar';
|
||||
import { CollapsibleSection } from './components/CollapsibleSection';
|
||||
import { HowItWorks } from './components/HowItWorks';
|
||||
import { WhyPlugins } from './components/WhyPlugins';
|
||||
import { BuildYourOwn } from './components/BuildYourOwn';
|
||||
import { plugins } from './plugins';
|
||||
import { checkBrowserCompatibility, checkExtension, checkVerifier, formatTimestamp } from './utils';
|
||||
import { ConsoleEntry, CheckStatus, PluginResult as PluginResultType } from './types';
|
||||
import './App.css';
|
||||
|
||||
interface PluginResultData {
|
||||
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 [pluginResults, setPluginResults] = useState<Record<string, PluginResultData>>({});
|
||||
const [consoleExpanded, setConsoleExpanded] = useState(false);
|
||||
|
||||
const addConsoleEntry = useCallback((message: string, type: ConsoleEntry['type'] = 'info') => {
|
||||
setConsoleEntries((prev) => [
|
||||
...prev,
|
||||
{
|
||||
timestamp: formatTimestamp(),
|
||||
message,
|
||||
type,
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
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 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 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;
|
||||
}
|
||||
|
||||
// Extension check
|
||||
const extensionOk = await checkExtension();
|
||||
if (extensionOk) {
|
||||
setExtensionCheck({ status: 'success', message: '✅ Extension installed' });
|
||||
} else {
|
||||
setExtensionCheck({ status: 'error', message: '❌ Extension not found' });
|
||||
}
|
||||
|
||||
// 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 handleRecheck = useCallback(async () => {
|
||||
// Recheck extension
|
||||
setExtensionCheck({ status: 'checking', message: 'Checking...' });
|
||||
const extensionOk = await checkExtension();
|
||||
if (extensionOk) {
|
||||
setExtensionCheck({ status: 'success', message: '✅ Extension installed' });
|
||||
} else {
|
||||
setExtensionCheck({ status: 'error', message: '❌ Extension not found' });
|
||||
}
|
||||
|
||||
// Recheck verifier
|
||||
setVerifierCheck({ status: 'checking', message: 'Checking...', showInstructions: false });
|
||||
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 handleRunPlugin = useCallback(
|
||||
async (pluginKey: string) => {
|
||||
const plugin = plugins[pluginKey];
|
||||
if (!plugin) return;
|
||||
|
||||
setRunningPlugins((prev) => new Set(prev).add(pluginKey));
|
||||
setConsoleExpanded(true);
|
||||
|
||||
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,
|
||||
[pluginKey]: {
|
||||
resultHtml: plugin.parseResult(json),
|
||||
debugJson: JSON.stringify(json.results, null, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
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">
|
||||
zkTLS in action — secure, private data verification from any website
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HowItWorks />
|
||||
|
||||
<StatusBar
|
||||
browserOk={browserCheck.status === 'success'}
|
||||
extensionOk={extensionCheck.status === 'success'}
|
||||
verifierOk={verifierCheck.status === 'success'}
|
||||
onRecheck={handleRecheck}
|
||||
detailsContent={
|
||||
<div className="checks-section">
|
||||
<div className="checks-title">System Status Details</div>
|
||||
<SystemChecks
|
||||
checks={{
|
||||
browser: browserCheck,
|
||||
extension: extensionCheck,
|
||||
verifier: verifierCheck,
|
||||
}}
|
||||
onRecheck={handleRecheck}
|
||||
showBrowserWarning={showBrowserWarning}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="content-card">
|
||||
<h2 className="section-title">Try It: Demo Plugins</h2>
|
||||
<p className="section-subtitle">
|
||||
Run a plugin to see TLSNotary in action. Click "View Source" to see how each plugin works.
|
||||
</p>
|
||||
|
||||
{!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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WhyPlugins />
|
||||
|
||||
<BuildYourOwn />
|
||||
|
||||
<CollapsibleSection title="Console Output" expanded={consoleExpanded}>
|
||||
<ConsoleOutput
|
||||
entries={consoleEntries}
|
||||
onClear={handleClearConsole}
|
||||
onOpenExtensionLogs={handleOpenExtensionLogs}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
packages/demo/src/components/BuildYourOwn.tsx
Normal file
62
packages/demo/src/components/BuildYourOwn.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
export function BuildYourOwn() {
|
||||
return (
|
||||
<div className="build-your-own">
|
||||
<div className="cta-content">
|
||||
<h2 className="cta-title">Ready to Build Your Own Plugin?</h2>
|
||||
<p className="cta-description">
|
||||
Create custom plugins to prove data from any website.
|
||||
Our SDK and documentation will help you get started in minutes.
|
||||
</p>
|
||||
|
||||
<div className="cta-buttons">
|
||||
<a
|
||||
href="https://tlsnotary.org/docs/extension/plugins"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cta-btn cta-btn-primary"
|
||||
>
|
||||
📚 Read the Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/tlsnotary/tlsn-extension/tree/main/packages/demo/src/plugins"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cta-btn cta-btn-secondary"
|
||||
>
|
||||
💻 View Plugin Sources
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="cta-resources">
|
||||
<h4 className="cta-resources-title">Resources</h4>
|
||||
<ul className="cta-resources-list">
|
||||
<li>
|
||||
<a href="https://github.com/tlsnotary/tlsn-extension" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Repository
|
||||
<span className="resource-desc">— Extension source code and examples</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://tlsnotary.org/docs/extension/plugins" target="_blank" rel="noopener noreferrer">
|
||||
TLSNotary Plugin Documentation
|
||||
<span className="resource-desc">— Complete protocol and API reference</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://tlsnotary.org" target="_blank" rel="noopener noreferrer">
|
||||
TLSNotary
|
||||
<span className="resource-desc">— TLSNotary landing page</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.com/invite/9XwESXtcN7" target="_blank" rel="noopener noreferrer">
|
||||
Discord Community
|
||||
<span className="resource-desc">— Get help and share your plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
45
packages/demo/src/components/Console.tsx
Normal file
45
packages/demo/src/components/Console.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
interface ConsoleEntryProps {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConsoleOutputProps {
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="console-output" id="consoleOutput">
|
||||
{entries.map((entry, index) => (
|
||||
<ConsoleEntry key={index} {...entry} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
packages/demo/src/components/HowItWorks.tsx
Normal file
51
packages/demo/src/components/HowItWorks.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<div className="how-it-works">
|
||||
<h2 className="how-it-works-title">How It Works</h2>
|
||||
<p className="how-it-works-subtitle">
|
||||
Experience cryptographic proof generation in three simple steps
|
||||
</p>
|
||||
|
||||
<div className="steps-container">
|
||||
<div className="step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-icon">🔌</div>
|
||||
<h3 className="step-title">Run a Plugin</h3>
|
||||
<p className="step-description">
|
||||
Select a plugin and click "Run". A new browser window opens to the target website.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="step-arrow">→</div>
|
||||
|
||||
<div className="step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-icon">🔐</div>
|
||||
<h3 className="step-title">Create Proof</h3>
|
||||
<p className="step-description">
|
||||
Log in if needed, then click "Prove". TLSNotary creates a cryptographic proof of your data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="step-arrow">→</div>
|
||||
|
||||
<div className="step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-icon">✅</div>
|
||||
<h3 className="step-title">Verify Result</h3>
|
||||
<p className="step-description">
|
||||
The proof is verified by the server. Only the data you chose to reveal is shared.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="how-it-works-note">
|
||||
<span className="note-icon">💡</span>
|
||||
<span>
|
||||
<strong>Your data stays private:</strong> Plugins run inside the TLSNotary extension's secure sandbox.
|
||||
Data flows through your browser — never through third-party servers.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
packages/demo/src/components/PluginButtons.tsx
Normal file
111
packages/demo/src/components/PluginButtons.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useState } from 'react';
|
||||
import { Plugin } from '../types';
|
||||
|
||||
interface PluginResultData {
|
||||
resultHtml: string;
|
||||
debugJson: string;
|
||||
}
|
||||
|
||||
interface PluginButtonsProps {
|
||||
plugins: Record<string, Plugin>;
|
||||
runningPlugins: Set<string>;
|
||||
pluginResults: Record<string, PluginResultData>;
|
||||
allChecksPass: boolean;
|
||||
onRunPlugin: (pluginKey: string) => void;
|
||||
}
|
||||
|
||||
export function PluginButtons({
|
||||
plugins,
|
||||
runningPlugins,
|
||||
pluginResults,
|
||||
allChecksPass,
|
||||
onRunPlugin,
|
||||
}: PluginButtonsProps) {
|
||||
const [expandedRawData, setExpandedRawData] = useState<Set<string>>(new Set());
|
||||
|
||||
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>
|
||||
|
||||
<div className="plugin-actions">
|
||||
<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>
|
||||
|
||||
<a
|
||||
href={plugin.file}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="plugin-source-btn"
|
||||
>
|
||||
<span>📄 View Source</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
98
packages/demo/src/components/StatusBar.tsx
Normal file
98
packages/demo/src/components/StatusBar.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface StatusBarProps {
|
||||
browserOk: boolean;
|
||||
extensionOk: boolean;
|
||||
verifierOk: boolean;
|
||||
onRecheck: () => void;
|
||||
detailsContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function StatusBar({
|
||||
browserOk,
|
||||
extensionOk,
|
||||
verifierOk,
|
||||
onRecheck,
|
||||
detailsContent,
|
||||
}: StatusBarProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
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={onRecheck}>
|
||||
Recheck
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`btn-details ${showDetails ? 'expanded' : ''}`}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<span className="btn-details-icon">{showDetails ? '▼' : '▶'}</span>
|
||||
<span>Details</span>
|
||||
</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>
|
||||
{' '}then <strong>refresh this page</strong>.
|
||||
</div>
|
||||
)}
|
||||
{!verifierOk && (
|
||||
<div>
|
||||
Verifier server not running. Start it with: <code>cd packages/verifier; cargo run --release</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showDetails && detailsContent && (
|
||||
<div className="status-details-content">
|
||||
{detailsContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
85
packages/demo/src/components/SystemChecks.tsx
Normal file
85
packages/demo/src/components/SystemChecks.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { CheckStatus } from '../types';
|
||||
|
||||
|
||||
interface CheckItemProps {
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SystemChecksProps {
|
||||
checks: {
|
||||
browser: { status: CheckStatus; message: string };
|
||||
extension: { status: CheckStatus; message: string };
|
||||
verifier: { status: CheckStatus; message: string; showInstructions: boolean };
|
||||
};
|
||||
onRecheck: () => void;
|
||||
showBrowserWarning: boolean;
|
||||
}
|
||||
|
||||
export function SystemChecks({ checks, onRecheck, 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>
|
||||
)}
|
||||
|
||||
<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={onRecheck}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
packages/demo/src/components/WhyPlugins.tsx
Normal file
39
packages/demo/src/components/WhyPlugins.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
export function WhyPlugins() {
|
||||
return (
|
||||
<div className="why-plugins">
|
||||
<h2 className="why-plugins-title">Why Plugins?</h2>
|
||||
<p className="why-plugins-subtitle">
|
||||
TLSNotary plugins provide a secure, flexible way to prove and verify web data
|
||||
</p>
|
||||
|
||||
<div className="benefits-grid">
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">🔒</div>
|
||||
<h3 className="benefit-title">Secure by Design</h3>
|
||||
<p className="benefit-description">
|
||||
Plugins run inside the TLSNotary extension's sandboxed environment.
|
||||
Your credentials and sensitive data never leave your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">👤</div>
|
||||
<h3 className="benefit-title">User-Controlled</h3>
|
||||
<p className="benefit-description">
|
||||
Data flows through the user's browser — not third-party servers.
|
||||
You choose exactly what data to reveal in each proof.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">⚡</div>
|
||||
<h3 className="benefit-title">Easy to Build</h3>
|
||||
<p className="benefit-description">
|
||||
Write plugins in JavaScript with a simple API.
|
||||
Intercept requests, create proofs, and build custom UIs with minimal code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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}`,
|
||||
};
|
||||
9
packages/demo/src/main.tsx
Normal file
9
packages/demo/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
31
packages/demo/src/plugins.ts
Normal file
31
packages/demo/src/plugins.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Plugin } from './types';
|
||||
|
||||
export const plugins: Record<string, Plugin> = {
|
||||
twitter: {
|
||||
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',
|
||||
description: 'Verify your Swiss bank account balance securely and privately. (Login: admin / admin)',
|
||||
logo: '🏦',
|
||||
file: '/plugins/swissbank.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
spotify: {
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
224
packages/demo/src/plugins/spotify.plugin.ts
Normal file
224
packages/demo/src/plugins/spotify.plugin.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// @ts-ignore - These will be replaced at build time by Vite's define option
|
||||
const VERIFIER_URL = VITE_VERIFIER_URL;
|
||||
// @ts-ignore
|
||||
const PROXY_URL_BASE = VITE_PROXY_URL;
|
||||
|
||||
const api = 'api.spotify.com';
|
||||
const ui = 'https://developer.spotify.com/';
|
||||
const top_artist_path = '/v1/me/top/artists?time_range=medium_term&limit=1';
|
||||
|
||||
const config = {
|
||||
name: 'Spotify Top Artist',
|
||||
description: 'This plugin will prove your top artist on Spotify.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.spotify.com',
|
||||
pathname: '/v1/me/top/artists',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://developer.spotify.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
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,
|
||||
};
|
||||
257
packages/demo/src/plugins/swissbank.plugin.ts
Normal file
257
packages/demo/src/plugins/swissbank.plugin.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// 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 config = {
|
||||
name: 'Swiss Bank Prover',
|
||||
description: 'This plugin will prove your Swiss Bank account balance.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'swissbank.tlsnotary.org',
|
||||
pathname: '/balances',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://swissbank.tlsnotary.org/*',
|
||||
],
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
278
packages/demo/src/plugins/twitter.plugin.ts
Normal file
278
packages/demo/src/plugins/twitter.plugin.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// 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;
|
||||
|
||||
// =============================================================================
|
||||
// PLUGIN CONFIGURATION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The config object defines plugin metadata displayed to users.
|
||||
* This information appears in the plugin selection UI.
|
||||
*/
|
||||
const config = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PROOF GENERATION CALLBACK
|
||||
// =============================================================================
|
||||
/**
|
||||
* This function is triggered when the user clicks the "Prove" button.
|
||||
* It extracts authentication headers from intercepted requests and generates
|
||||
* a TLSNotary proof using the unified prove() API.
|
||||
*/
|
||||
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,
|
||||
};
|
||||
45
packages/demo/src/types.ts
Normal file
45
packages/demo/src/types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
file: string;
|
||||
parseResult: (json: PluginResult) => string;
|
||||
}
|
||||
|
||||
export interface PluginResult {
|
||||
results: Array<{
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ConsoleEntry {
|
||||
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;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tlsn?: {
|
||||
execCode: (code: string) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
brave?: {
|
||||
isBrave: () => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
30
packages/demo/src/utils.ts
Normal file
30
packages/demo/src/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export function checkBrowserCompatibility(): boolean {
|
||||
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);
|
||||
|
||||
return isChrome || isEdge || isBrave || isChromium;
|
||||
}
|
||||
|
||||
export async function checkExtension(): Promise<boolean> {
|
||||
// Wait a bit for tlsn to load if page just loaded
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return typeof window.tlsn !== 'undefined';
|
||||
}
|
||||
|
||||
export async function checkVerifier(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://localhost:7047/health');
|
||||
if (response.ok && (await response.text()) === 'ok') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimestamp(): string {
|
||||
return new Date().toLocaleTimeString();
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
const config = {
|
||||
name: 'Swiss Bank Prover',
|
||||
description: 'This plugin will prove your Swiss Bank account balance.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'swissbank.tlsnotary.org',
|
||||
pathname: '/balances',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://swissbank.tlsnotary.org/*',
|
||||
],
|
||||
};
|
||||
|
||||
const host = 'swissbank.tlsnotary.org';
|
||||
|
||||
36
packages/demo/tsconfig.json
Normal file
36
packages/demo/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/plugins/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
packages/demo/tsconfig.node.json
Normal file
12
packages/demo/tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
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": []
|
||||
}
|
||||
@@ -8,6 +8,17 @@
|
||||
const config = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
||||
14
packages/demo/vite.config.ts
Normal file
14
packages/demo/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
@@ -21,6 +21,9 @@
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"URLPattern": "readonly"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "typescript"
|
||||
},
|
||||
|
||||
179
packages/extension/STORE_LISTING.md
Normal file
179
packages/extension/STORE_LISTING.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Chrome Web Store Listing
|
||||
|
||||
## Extension Name
|
||||
TLSNotary
|
||||
|
||||
## Description
|
||||
|
||||
TLSNotary Extension enables you to create cryptographic proofs of any data you access on the web. Prove ownership of your online accounts, verify your credentials, or demonstrate that you received specific information from a website—all without exposing your private data.
|
||||
|
||||
### What is TLSNotary?
|
||||
|
||||
TLSNotary is an open-source protocol that allows you to prove the authenticity of any data fetched from websites. When you visit an HTTPS website, your browser establishes a secure TLS (Transport Layer Security) connection. TLSNotary leverages this existing security infrastructure to generate verifiable proofs that specific data was genuinely returned by a particular website, without requiring any cooperation from the website itself.
|
||||
|
||||
### Why Install This Extension?
|
||||
|
||||
**Prove What You See Online**
|
||||
Have you ever needed to prove that a website displayed certain information? Whether it's proving your account balance, ownership of a social media profile, or the contents of a private message, TLSNotary creates tamper-proof cryptographic evidence that stands up to scrutiny.
|
||||
|
||||
**Privacy-Preserving Proofs**
|
||||
Unlike screenshots or screen recordings that can be easily faked and expose all your data, TLSNotary proofs are:
|
||||
- Cryptographically verifiable and cannot be forged
|
||||
- Selectively disclosed—reveal only the specific data points you choose while keeping everything else private
|
||||
- Generated without exposing your login credentials or session tokens to third parties
|
||||
|
||||
**No Website Cooperation Required**
|
||||
TLSNotary works with any HTTPS website without requiring the website to implement any special support. The proof generation happens entirely in your browser, using the standard TLS connection you already have.
|
||||
|
||||
### Key Features
|
||||
|
||||
**Cryptographic Data Proofs**
|
||||
Generate unforgeable proofs that specific data was returned by a website. Each proof contains cryptographic evidence tied to the website's TLS certificate, making it impossible to fabricate or alter.
|
||||
|
||||
**Selective Disclosure**
|
||||
Choose exactly what information to reveal in your proofs. Prove your account balance without revealing your transaction history. Verify your identity without exposing your full profile. Show specific fields while keeping everything else hidden.
|
||||
|
||||
**Plugin System**
|
||||
Build and run custom plugins for specific proof workflows. The extension includes a Developer Console with a code editor for creating and testing plugins. Use React-like hooks for reactive UI updates and easy integration with the proof generation pipeline.
|
||||
|
||||
**Multi-Window Management**
|
||||
Open and manage multiple browser windows for tracking different proof sessions. Each window maintains its own request history, allowing you to work on multiple proofs simultaneously.
|
||||
|
||||
**Request Interception**
|
||||
Automatically capture HTTP/HTTPS requests from managed windows. View intercepted requests in real-time through an intuitive overlay interface. Select the specific requests you want to include in your proofs.
|
||||
|
||||
**Sandboxed Execution**
|
||||
Plugins run in an isolated QuickJS WebAssembly environment for security. Network and filesystem access are disabled by default, ensuring plugins cannot access data beyond what you explicitly provide.
|
||||
|
||||
### Use Cases
|
||||
|
||||
**Identity Verification**
|
||||
Prove you own a specific social media account, email address, or online profile without sharing your password or giving third-party access to your account.
|
||||
|
||||
**Financial Attestations**
|
||||
Demonstrate your account balance, transaction history, or financial standing to lenders, landlords, or other parties who require proof—without exposing your complete financial information.
|
||||
|
||||
**Content Authentication**
|
||||
Create verifiable evidence of online content that cannot be forged. Useful for legal documentation, journalism, research, or any situation where proving the authenticity of web content matters.
|
||||
|
||||
**Credential Verification**
|
||||
Prove your credentials, certifications, or qualifications as displayed by official issuing organizations, without relying on easily-faked screenshots.
|
||||
|
||||
**Privacy-Preserving KYC**
|
||||
Complete Know Your Customer (KYC) requirements while revealing only the minimum necessary information. Prove you meet eligibility criteria without exposing your full identity.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Install the Extension**: Add TLSNotary to Chrome from the Web Store.
|
||||
|
||||
2. **Access the Developer Console**: Right-click on any webpage and select "Developer Console" to open the plugin editor.
|
||||
|
||||
3. **Run a Plugin**: Use the built-in example plugins or write your own. Plugins define what data to capture and which parts to include in proofs.
|
||||
|
||||
4. **Generate Proofs**: The extension captures your HTTPS traffic, creates a cryptographic commitment with a verifier server, and generates a proof of the data you selected.
|
||||
|
||||
5. **Share Selectively**: Export proofs containing only the data you want to reveal. Verifiers can confirm the proof's authenticity without seeing your hidden information.
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Manifest V3**: Built on Chrome's latest extension platform for improved security and performance
|
||||
- **WebAssembly Powered**: Uses compiled Rust code via WebAssembly for efficient cryptographic operations
|
||||
- **Plugin SDK**: Comprehensive SDK for developing custom proof workflows with TypeScript support
|
||||
- **Open Source**: Full source code available for review and community contributions
|
||||
|
||||
### Requirements
|
||||
|
||||
- Chrome browser version 109 or later (for offscreen document support)
|
||||
- A verifier server for proof generation (public servers available or run your own)
|
||||
- Active internet connection for HTTPS request interception
|
||||
|
||||
### Privacy and Security
|
||||
|
||||
TLSNotary is designed with privacy as a core principle:
|
||||
|
||||
- **No Data Collection**: The extension does not collect, store, or transmit your browsing data to any third party
|
||||
- **Local Processing**: All proof generation happens locally in your browser
|
||||
- **Open Source**: The entire codebase is publicly auditable
|
||||
- **Selective Disclosure**: You control exactly what data appears in proofs
|
||||
- **Sandboxed Plugins**: Plugin code runs in an isolated environment with no access to your system
|
||||
|
||||
### Getting Started
|
||||
|
||||
After installation:
|
||||
|
||||
1. Right-click anywhere on a webpage
|
||||
2. Select "Developer Console" from the context menu
|
||||
3. Review the example plugin code in the editor
|
||||
4. Click "Run Code" to execute the plugin
|
||||
5. Follow the on-screen instructions to generate your first proof
|
||||
|
||||
For detailed documentation, tutorials, and plugin development guides, visit the TLSNotary documentation site.
|
||||
|
||||
### About TLSNotary
|
||||
|
||||
TLSNotary is an open-source project dedicated to enabling data portability and verifiable provenance for web data. The protocol has been in development since 2013 and has undergone multiple security audits. Join our community to learn more about trustless data verification and contribute to the future of verifiable web data.
|
||||
|
||||
### Support and Feedback
|
||||
|
||||
- Documentation: https://docs.tlsnotary.org/
|
||||
- GitHub: https://github.com/tlsnotary/tlsn-extension
|
||||
- Issues: https://github.com/tlsnotary/tlsn-extension/issues
|
||||
|
||||
Licensed under MIT and Apache 2.0 licenses.
|
||||
|
||||
---
|
||||
|
||||
## Screenshot Captions
|
||||
|
||||
### Screenshot 1: Plugin UI
|
||||
**Caption:** "Prove any web data without compromising privacy"
|
||||
|
||||
### Screenshot 2: Permission Popup
|
||||
**Caption:** "Control exactly what data you reveal in each proof"
|
||||
|
||||
### Screenshot 3: Developer Console
|
||||
**Caption:** "Build custom plugins with the built-in code editor"
|
||||
|
||||
---
|
||||
|
||||
## Permission Justifications
|
||||
|
||||
The following permissions are required for the extension's core functionality of generating cryptographic proofs of web data:
|
||||
|
||||
### offscreen
|
||||
|
||||
**Justification:** Required to create offscreen documents for executing WebAssembly-based cryptographic operations. The TLSNotary proof generation uses Rust compiled to WebAssembly, which requires DOM APIs unavailable in Manifest V3 service workers. The offscreen document hosts the plugin execution environment (QuickJS sandbox) and the cryptographic prover that generates TLS proofs. Without this permission, the extension cannot perform its core function of generating cryptographic proofs.
|
||||
|
||||
### webRequest
|
||||
|
||||
**Justification:** Required to intercept HTTP/HTTPS requests from browser windows managed by the extension. When users initiate a proof generation workflow, the extension opens a managed browser window and captures the HTTP request/response data that will be included in the cryptographic proof. This interception is essential for capturing the exact data the user wants to prove, including request headers and URLs. The extension only intercepts requests in windows it explicitly manages for proof generation—not general browsing activity.
|
||||
|
||||
### storage
|
||||
|
||||
**Justification:** Required to persist user preferences and plugin configurations across browser sessions. The extension stores user settings such as preferred verifier server URLs and plugin code. This ensures users do not need to reconfigure the extension each time they restart their browser.
|
||||
|
||||
### activeTab
|
||||
|
||||
**Justification:** Required to access information about the currently active tab when the user initiates a proof generation workflow. The extension needs to read the current page URL and title to display context in the Developer Console and to determine which requests belong to the active proof session.
|
||||
|
||||
### tabs
|
||||
|
||||
**Justification:** Required to create, query, and manage browser tabs for proof generation workflows. When a plugin opens a managed window for capturing web data, the extension must create new tabs, send messages to content scripts in those tabs, and track which tabs belong to which proof session. This is essential for the multi-window proof management feature.
|
||||
|
||||
### windows
|
||||
|
||||
**Justification:** Required to create and manage browser windows for proof generation sessions. The extension opens dedicated browser windows when users run proof plugins, allowing isolation of the proof capture session from regular browsing. The extension tracks these windows to route intercepted requests to the correct proof session and to clean up resources when windows are closed.
|
||||
|
||||
### contextMenus
|
||||
|
||||
**Justification:** Required to add the "Developer Console" menu item to the browser's right-click context menu. This provides the primary access point for users to open the plugin development and execution interface. Without this permission, users would have no convenient way to access the Developer Console for writing and running proof plugins.
|
||||
|
||||
### Host Permissions (<all_urls>)
|
||||
|
||||
**Justification:** Required because TLSNotary is designed to generate cryptographic proofs of data from any HTTPS website. Users need to prove data from various websites including social media platforms, financial services, government portals, and any other web service. The extension cannot predict which websites users will need to generate proofs for, so it requires broad host access to intercept requests and inject content scripts for the proof overlay UI. The extension only actively intercepts requests in windows explicitly managed for proof generation—it does not monitor or collect data from general browsing activity.
|
||||
|
||||
---
|
||||
|
||||
## Single Purpose Description
|
||||
|
||||
TLSNotary Extension has a single purpose: to generate cryptographic proofs of web data. All requested permissions directly support this purpose by enabling request interception for proof capture, window management for proof sessions, and background processing for cryptographic operations.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "extension",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0.1300",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,13 +20,13 @@
|
||||
"serve:test": "python3 -m http.server 8081 --directory ./tests/integration"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tlsn/common": "*",
|
||||
"@tlsn/plugin-sdk": "*",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@tlsn/common": "*",
|
||||
"@tlsn/plugin-sdk": "*",
|
||||
"@uiw/react-codemirror": "^4.25.2",
|
||||
"assert": "^2.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -84,7 +84,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"happy-dom": "^19.0.1",
|
||||
"happy-dom": "^20.0.11",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"null-loader": "^4.0.1",
|
||||
@@ -107,8 +107,8 @@
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
"webpack-ext-reloader": "^1.1.12",
|
||||
"zip-webpack-plugin": "^4.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
packages/extension/src/assets/img/store-icon.png
Normal file
BIN
packages/extension/src/assets/img/store-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,12 +1,6 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { logger } from '@tlsn/common';
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
version?: string;
|
||||
author?: string;
|
||||
}
|
||||
import { PluginConfig } from '@tlsn/plugin-sdk/src/types';
|
||||
|
||||
interface PendingConfirmation {
|
||||
requestId: string;
|
||||
@@ -30,7 +24,7 @@ export class ConfirmationManager {
|
||||
|
||||
// Popup window dimensions
|
||||
private readonly POPUP_WIDTH = 600;
|
||||
private readonly POPUP_HEIGHT = 400;
|
||||
private readonly POPUP_HEIGHT = 550;
|
||||
|
||||
constructor() {
|
||||
// Listen for window removal to handle popup close
|
||||
@@ -192,6 +186,18 @@ export class ConfirmationManager {
|
||||
if (config.author) {
|
||||
params.set('author', encodeURIComponent(config.author));
|
||||
}
|
||||
|
||||
// Pass permission arrays as JSON
|
||||
if (config.requests && config.requests.length > 0) {
|
||||
params.set(
|
||||
'requests',
|
||||
encodeURIComponent(JSON.stringify(config.requests)),
|
||||
);
|
||||
}
|
||||
|
||||
if (config.urls && config.urls.length > 0) {
|
||||
params.set('urls', encodeURIComponent(JSON.stringify(config.urls)));
|
||||
}
|
||||
}
|
||||
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Confirmation Popup Styles
|
||||
// Size: 600x400 pixels
|
||||
// Size: 600x550 pixels
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
body {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
height: 550px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
@@ -154,6 +154,114 @@ body {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
// Permissions Section
|
||||
&__permissions {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&__permissions-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__permission-group {
|
||||
margin-bottom: 12px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #8b8b9a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__permission-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__permission-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__permission-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__method {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #8fa4f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__host {
|
||||
color: #e8e8e8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__pathname {
|
||||
color: #8b8b9a;
|
||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__url {
|
||||
color: #8fa4f0;
|
||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__no-permissions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: rgba(139, 139, 154, 0.1);
|
||||
border: 1px solid rgba(139, 139, 154, 0.3);
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #8b8b9a;
|
||||
}
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
font-size: 14px;
|
||||
color: #ff6b6b;
|
||||
|
||||
@@ -7,11 +7,21 @@ import './index.scss';
|
||||
// Initialize logger at DEBUG level for popup (no IndexedDB access)
|
||||
logger.init(LogLevel.DEBUG);
|
||||
|
||||
interface RequestPermission {
|
||||
method: string;
|
||||
host: string;
|
||||
pathname: string;
|
||||
verifierUrl: string;
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
interface PluginInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
version?: string;
|
||||
author?: string;
|
||||
requests?: RequestPermission[];
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
const ConfirmPopup: React.FC = () => {
|
||||
@@ -26,6 +36,8 @@ const ConfirmPopup: React.FC = () => {
|
||||
const description = params.get('description');
|
||||
const version = params.get('version');
|
||||
const author = params.get('author');
|
||||
const requestsParam = params.get('requests');
|
||||
const urlsParam = params.get('urls');
|
||||
const reqId = params.get('requestId');
|
||||
|
||||
if (!reqId) {
|
||||
@@ -36,6 +48,26 @@ const ConfirmPopup: React.FC = () => {
|
||||
setRequestId(reqId);
|
||||
|
||||
if (name) {
|
||||
// Parse permission arrays from JSON
|
||||
let requests: RequestPermission[] | undefined;
|
||||
let urls: string[] | undefined;
|
||||
|
||||
try {
|
||||
if (requestsParam) {
|
||||
requests = JSON.parse(decodeURIComponent(requestsParam));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse requests param:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (urlsParam) {
|
||||
urls = JSON.parse(decodeURIComponent(urlsParam));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse urls param:', e);
|
||||
}
|
||||
|
||||
setPluginInfo({
|
||||
name: decodeURIComponent(name),
|
||||
description: description
|
||||
@@ -43,6 +75,8 @@ const ConfirmPopup: React.FC = () => {
|
||||
: 'No description provided',
|
||||
version: version ? decodeURIComponent(version) : undefined,
|
||||
author: author ? decodeURIComponent(author) : undefined,
|
||||
requests,
|
||||
urls,
|
||||
});
|
||||
} else {
|
||||
// No plugin info available - show unknown plugin warning
|
||||
@@ -174,6 +208,62 @@ const ConfirmPopup: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Permissions Section */}
|
||||
{(pluginInfo.requests || pluginInfo.urls) && (
|
||||
<div className="confirm-popup__permissions">
|
||||
<h2 className="confirm-popup__permissions-title">Permissions</h2>
|
||||
|
||||
{pluginInfo.requests && pluginInfo.requests.length > 0 && (
|
||||
<div className="confirm-popup__permission-group">
|
||||
<label>
|
||||
<span className="confirm-popup__permission-icon">🌐</span>
|
||||
Network Requests
|
||||
</label>
|
||||
<ul className="confirm-popup__permission-list">
|
||||
{pluginInfo.requests.map((req, index) => (
|
||||
<li key={index} className="confirm-popup__permission-item">
|
||||
<span className="confirm-popup__method">
|
||||
{req.method}
|
||||
</span>
|
||||
<span className="confirm-popup__host">{req.host}</span>
|
||||
<span className="confirm-popup__pathname">
|
||||
{req.pathname}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pluginInfo.urls && pluginInfo.urls.length > 0 && (
|
||||
<div className="confirm-popup__permission-group">
|
||||
<label>
|
||||
<span className="confirm-popup__permission-icon">🔗</span>
|
||||
Allowed URLs
|
||||
</label>
|
||||
<ul className="confirm-popup__permission-list">
|
||||
{pluginInfo.urls.map((url, index) => (
|
||||
<li key={index} className="confirm-popup__permission-item">
|
||||
<span className="confirm-popup__url">{url}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No permissions warning */}
|
||||
{!pluginInfo.requests && !pluginInfo.urls && !isUnknown && (
|
||||
<div className="confirm-popup__no-permissions">
|
||||
<span className="confirm-popup__warning-icon">!</span>
|
||||
<p>
|
||||
This plugin has no permissions defined. It will not be able to
|
||||
make network requests or open browser windows.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUnknown && (
|
||||
<div className="confirm-popup__warning">
|
||||
<span className="confirm-popup__warning-icon">!</span>
|
||||
|
||||
47
packages/extension/src/global.d.ts
vendored
47
packages/extension/src/global.d.ts
vendored
@@ -2,3 +2,50 @@ declare module '*.png' {
|
||||
const value: any;
|
||||
export = value;
|
||||
}
|
||||
|
||||
// URLPattern Web API (available in Chrome 95+)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
|
||||
interface URLPatternInit {
|
||||
protocol?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
pathname?: string;
|
||||
search?: string;
|
||||
hash?: string;
|
||||
baseURL?: string;
|
||||
}
|
||||
|
||||
interface URLPatternComponentResult {
|
||||
input: string;
|
||||
groups: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
interface URLPatternResult {
|
||||
inputs: [string | URLPatternInit];
|
||||
protocol: URLPatternComponentResult;
|
||||
username: URLPatternComponentResult;
|
||||
password: URLPatternComponentResult;
|
||||
hostname: URLPatternComponentResult;
|
||||
port: URLPatternComponentResult;
|
||||
pathname: URLPatternComponentResult;
|
||||
search: URLPatternComponentResult;
|
||||
hash: URLPatternComponentResult;
|
||||
}
|
||||
|
||||
declare class URLPattern {
|
||||
constructor(input: string | URLPatternInit, baseURL?: string);
|
||||
|
||||
test(input: string | URLPatternInit): boolean;
|
||||
exec(input: string | URLPatternInit): URLPatternResult | null;
|
||||
|
||||
readonly protocol: string;
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
readonly hostname: string;
|
||||
readonly port: string;
|
||||
readonly pathname: string;
|
||||
readonly search: string;
|
||||
readonly hash: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "TLSN Extension",
|
||||
"description": "A Chrome extension for TLSN",
|
||||
"version": "0.1.0.13",
|
||||
"name": "TLSNotary",
|
||||
"description": "A Chrome extension for TLSNotary",
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"service_worker": "background.bundle.js"
|
||||
@@ -29,7 +30,6 @@
|
||||
"permissions": [
|
||||
"offscreen",
|
||||
"webRequest",
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"windows",
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import Host, { Parser } from '@tlsn/plugin-sdk/src';
|
||||
import { ProveManager } from './ProveManager';
|
||||
import { Method } from 'tlsn-js';
|
||||
import { DomJson, Handler } from '@tlsn/plugin-sdk/src/types';
|
||||
import { DomJson, Handler, PluginConfig } from '@tlsn/plugin-sdk/src/types';
|
||||
import { processHandlers } from './rangeExtractor';
|
||||
import { logger } from '@tlsn/common';
|
||||
import {
|
||||
validateProvePermission,
|
||||
validateOpenWindowPermission,
|
||||
} from './permissionValidator';
|
||||
|
||||
export class SessionManager {
|
||||
private host: Host;
|
||||
private proveManager: ProveManager;
|
||||
private initPromise: Promise<void>;
|
||||
private currentConfig: PluginConfig | null = null;
|
||||
|
||||
constructor() {
|
||||
this.host = new Host({
|
||||
@@ -36,6 +41,13 @@ export class SessionManager {
|
||||
throw new Error('Invalid URL');
|
||||
}
|
||||
|
||||
// Validate permissions before proceeding
|
||||
validateProvePermission(
|
||||
requestOptions,
|
||||
proverOptions,
|
||||
this.currentConfig,
|
||||
);
|
||||
|
||||
// Build sessionData with defaults + user-provided data
|
||||
const sessionData: Record<string, string> = {
|
||||
...proverOptions.sessionData,
|
||||
@@ -131,6 +143,9 @@ export class SessionManager {
|
||||
url: string,
|
||||
options?: { width?: number; height?: number; showOverlay?: boolean },
|
||||
) => {
|
||||
// Validate permissions before proceeding
|
||||
validateOpenWindowPermission(url, this.currentConfig);
|
||||
|
||||
const chromeRuntime = (
|
||||
global as unknown as { chrome?: { runtime?: any } }
|
||||
).chrome?.runtime;
|
||||
@@ -164,6 +179,14 @@ export class SessionManager {
|
||||
if (!chromeRuntime?.onMessage) {
|
||||
throw new Error('Chrome runtime not available');
|
||||
}
|
||||
|
||||
// Extract and store plugin config before execution for permission validation
|
||||
this.currentConfig = await this.extractConfig(code);
|
||||
logger.debug(
|
||||
'[SessionManager] Extracted plugin config:',
|
||||
this.currentConfig,
|
||||
);
|
||||
|
||||
return this.host.executePlugin(code, {
|
||||
eventEmitter: {
|
||||
addListener: (listener: (message: any) => void) => {
|
||||
|
||||
141
packages/extension/src/offscreen/permissionValidator.ts
Normal file
141
packages/extension/src/offscreen/permissionValidator.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { PluginConfig, RequestPermission } from '@tlsn/plugin-sdk/src/types';
|
||||
|
||||
/**
|
||||
* Derives the default proxy URL from a verifier URL.
|
||||
* https://verifier.example.com -> wss://verifier.example.com/proxy?token={host}
|
||||
* http://localhost:7047 -> ws://localhost:7047/proxy?token={host}
|
||||
*/
|
||||
export function deriveProxyUrl(
|
||||
verifierUrl: string,
|
||||
targetHost: string,
|
||||
): string {
|
||||
const url = new URL(verifierUrl);
|
||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${url.host}/proxy?token=${targetHost}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a URL pathname against a URLPattern pathname pattern.
|
||||
* Uses the URLPattern API for pattern matching.
|
||||
*/
|
||||
export function matchesPathnamePattern(
|
||||
pathname: string,
|
||||
pattern: string,
|
||||
): boolean {
|
||||
try {
|
||||
// URLPattern is available in modern browsers
|
||||
const urlPattern = new URLPattern({ pathname: pattern });
|
||||
return urlPattern.test({ pathname });
|
||||
} catch {
|
||||
// Fallback: simple wildcard matching
|
||||
// Convert * to regex .* and ** to multi-segment match
|
||||
const regexPattern = pattern
|
||||
.replace(/\*\*/g, '<<<MULTI>>>')
|
||||
.replace(/\*/g, '[^/]*')
|
||||
.replace(/<<<MULTI>>>/g, '.*');
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(pathname);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a prove() call is allowed by the plugin's permissions.
|
||||
* Throws an error if the permission is not granted.
|
||||
*/
|
||||
export function validateProvePermission(
|
||||
requestOptions: { url: string; method: string },
|
||||
proverOptions: { verifierUrl: string; proxyUrl: string },
|
||||
config: PluginConfig | null,
|
||||
): void {
|
||||
// If no config or no requests permissions defined, deny by default
|
||||
if (!config?.requests || config.requests.length === 0) {
|
||||
throw new Error(
|
||||
`Permission denied: Plugin has no request permissions defined. ` +
|
||||
`Cannot make ${requestOptions.method} request to ${requestOptions.url}`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(requestOptions.url);
|
||||
const requestMethod = requestOptions.method.toUpperCase();
|
||||
|
||||
const matchingPermission = config.requests.find((perm: RequestPermission) => {
|
||||
// Check method (case-insensitive)
|
||||
const methodMatch = perm.method.toUpperCase() === requestMethod;
|
||||
if (!methodMatch) return false;
|
||||
|
||||
// Check host
|
||||
const hostMatch = perm.host === url.hostname;
|
||||
if (!hostMatch) return false;
|
||||
|
||||
// Check pathname pattern
|
||||
const pathnameMatch = matchesPathnamePattern(url.pathname, perm.pathname);
|
||||
if (!pathnameMatch) return false;
|
||||
|
||||
// Check verifier URL
|
||||
const verifierMatch = perm.verifierUrl === proverOptions.verifierUrl;
|
||||
if (!verifierMatch) return false;
|
||||
|
||||
// Check proxy URL (use derived default if not specified in permission)
|
||||
const expectedProxyUrl =
|
||||
perm.proxyUrl ?? deriveProxyUrl(perm.verifierUrl, url.hostname);
|
||||
const proxyMatch = expectedProxyUrl === proverOptions.proxyUrl;
|
||||
if (!proxyMatch) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!matchingPermission) {
|
||||
const permissionsSummary = config.requests
|
||||
.map(
|
||||
(p: RequestPermission) =>
|
||||
` - ${p.method} ${p.host}${p.pathname} (verifier: ${p.verifierUrl})`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
throw new Error(
|
||||
`Permission denied: Plugin does not have permission to make ${requestMethod} request to ${url.hostname}${url.pathname} ` +
|
||||
`with verifier ${proverOptions.verifierUrl} and proxy ${proverOptions.proxyUrl}.\n` +
|
||||
`Declared request permissions:\n${permissionsSummary}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an openWindow() call is allowed by the plugin's permissions.
|
||||
* Throws an error if the permission is not granted.
|
||||
*/
|
||||
export function validateOpenWindowPermission(
|
||||
url: string,
|
||||
config: PluginConfig | null,
|
||||
): void {
|
||||
// If no config or no urls permissions defined, deny by default
|
||||
if (!config?.urls || config.urls.length === 0) {
|
||||
throw new Error(
|
||||
`Permission denied: Plugin has no URL permissions defined. ` +
|
||||
`Cannot open URL ${url}`,
|
||||
);
|
||||
}
|
||||
|
||||
const hasPermission = config.urls.some((allowedPattern: string) => {
|
||||
try {
|
||||
// Try URLPattern first
|
||||
const pattern = new URLPattern(allowedPattern);
|
||||
return pattern.test(url);
|
||||
} catch {
|
||||
// Fallback: treat as simple glob pattern
|
||||
// Convert * to regex
|
||||
const regexPattern = allowedPattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *
|
||||
.replace(/\*/g, '.*');
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(url);
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new Error(
|
||||
`Permission denied: Plugin does not have permission to open URL ${url}.\n` +
|
||||
`Declared URL permissions:\n${config.urls.map((u: string) => ` - ${u}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
295
packages/extension/tests/offscreen/permissionValidator.test.ts
Normal file
295
packages/extension/tests/offscreen/permissionValidator.test.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
deriveProxyUrl,
|
||||
matchesPathnamePattern,
|
||||
validateProvePermission,
|
||||
validateOpenWindowPermission,
|
||||
} from '../../src/offscreen/permissionValidator';
|
||||
import { PluginConfig } from '@tlsn/plugin-sdk/src/types';
|
||||
|
||||
describe('deriveProxyUrl', () => {
|
||||
it('should derive wss proxy URL from https verifier', () => {
|
||||
const result = deriveProxyUrl('https://verifier.example.com', 'api.x.com');
|
||||
expect(result).toBe('wss://verifier.example.com/proxy?token=api.x.com');
|
||||
});
|
||||
|
||||
it('should derive ws proxy URL from http verifier', () => {
|
||||
const result = deriveProxyUrl('http://localhost:7047', 'api.x.com');
|
||||
expect(result).toBe('ws://localhost:7047/proxy?token=api.x.com');
|
||||
});
|
||||
|
||||
it('should preserve port in proxy URL', () => {
|
||||
const result = deriveProxyUrl(
|
||||
'https://verifier.example.com:8080',
|
||||
'api.x.com',
|
||||
);
|
||||
expect(result).toBe(
|
||||
'wss://verifier.example.com:8080/proxy?token=api.x.com',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPathnamePattern', () => {
|
||||
it('should match exact pathname', () => {
|
||||
expect(matchesPathnamePattern('/api/v1/users', '/api/v1/users')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match different pathname', () => {
|
||||
expect(matchesPathnamePattern('/api/v1/users', '/api/v1/posts')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should match wildcard at end', () => {
|
||||
expect(matchesPathnamePattern('/api/v1/users/123', '/api/v1/users/*')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should match wildcard in middle', () => {
|
||||
expect(
|
||||
matchesPathnamePattern(
|
||||
'/api/v1/users/123/profile',
|
||||
'/api/v1/users/*/profile',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match wildcard across segments', () => {
|
||||
// Single * should only match one segment
|
||||
expect(
|
||||
matchesPathnamePattern('/api/v1/users/123/456', '/api/v1/users/*'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should match double wildcard across segments', () => {
|
||||
expect(
|
||||
matchesPathnamePattern(
|
||||
'/api/v1/users/123/456/profile',
|
||||
'/api/v1/users/**',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateProvePermission', () => {
|
||||
const baseConfig: PluginConfig = {
|
||||
name: 'Test Plugin',
|
||||
description: 'Test',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
host: 'api.example.com',
|
||||
pathname: '/api/v1/*',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('should allow matching request with exact pathname', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/account/settings.json', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow matching request with wildcard pathname', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.example.com/api/v1/users', method: 'POST' },
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.example.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should deny request with wrong method', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/account/settings.json', method: 'POST' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny request with wrong host', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{
|
||||
url: 'https://api.twitter.com/1.1/account/settings.json',
|
||||
method: 'GET',
|
||||
},
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.twitter.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny request with wrong pathname', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/users/show.json', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny request with wrong verifier URL', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/account/settings.json', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny request with wrong proxy URL', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/account/settings.json', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://malicious.com/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny request when no permissions defined', () => {
|
||||
const noPermConfig: PluginConfig = {
|
||||
name: 'No Perm Plugin',
|
||||
description: 'Test',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/test', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
noPermConfig,
|
||||
),
|
||||
).toThrow('Plugin has no request permissions defined');
|
||||
});
|
||||
|
||||
it('should deny request when config is null', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/test', method: 'GET' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
null,
|
||||
),
|
||||
).toThrow('Plugin has no request permissions defined');
|
||||
});
|
||||
|
||||
it('should be case-insensitive for HTTP method', () => {
|
||||
expect(() =>
|
||||
validateProvePermission(
|
||||
{ url: 'https://api.x.com/1.1/account/settings.json', method: 'get' },
|
||||
{
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
proxyUrl: 'wss://verifier.tlsnotary.org/proxy?token=api.x.com',
|
||||
},
|
||||
baseConfig,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOpenWindowPermission', () => {
|
||||
const baseConfig: PluginConfig = {
|
||||
name: 'Test Plugin',
|
||||
description: 'Test',
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
'https://twitter.com/*',
|
||||
'https://example.com/specific/page',
|
||||
],
|
||||
};
|
||||
|
||||
it('should allow matching URL with wildcard', () => {
|
||||
expect(() =>
|
||||
validateOpenWindowPermission('https://x.com/user/profile', baseConfig),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow exact URL match', () => {
|
||||
expect(() =>
|
||||
validateOpenWindowPermission(
|
||||
'https://example.com/specific/page',
|
||||
baseConfig,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should deny URL not in permissions', () => {
|
||||
expect(() =>
|
||||
validateOpenWindowPermission(
|
||||
'https://malicious.com/phishing',
|
||||
baseConfig,
|
||||
),
|
||||
).toThrow('Permission denied');
|
||||
});
|
||||
|
||||
it('should deny URL when no permissions defined', () => {
|
||||
const noPermConfig: PluginConfig = {
|
||||
name: 'No Perm Plugin',
|
||||
description: 'Test',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateOpenWindowPermission('https://x.com/test', noPermConfig),
|
||||
).toThrow('Plugin has no URL permissions defined');
|
||||
});
|
||||
|
||||
it('should deny URL when config is null', () => {
|
||||
expect(() =>
|
||||
validateOpenWindowPermission('https://x.com/test', null),
|
||||
).toThrow('Plugin has no URL permissions defined');
|
||||
});
|
||||
|
||||
it('should match wildcard at end of URL', () => {
|
||||
expect(() =>
|
||||
validateOpenWindowPermission('https://x.com/', baseConfig),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
validateOpenWindowPermission('https://x.com/any/path/here', baseConfig),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ var compiler = webpack(config);
|
||||
|
||||
var server = new WebpackDevServer(
|
||||
{
|
||||
https: false,
|
||||
server: 'http',
|
||||
hot: true,
|
||||
liveReload: false,
|
||||
client: {
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./styles": {
|
||||
"import": "./dist/styles.js",
|
||||
"types": "./dist/styles.d.ts"
|
||||
},
|
||||
"./src": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./src/types": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest",
|
||||
@@ -42,6 +60,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.18",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
@@ -51,7 +70,7 @@
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"happy-dom": "^19.0.2",
|
||||
"happy-dom": "^20.0.11",
|
||||
"path-browserify": "^1.0.1",
|
||||
"playwright": "^1.55.1",
|
||||
"prettier": "^3.6.2",
|
||||
@@ -67,6 +86,7 @@
|
||||
"@jitl/quickjs-ng-wasmfile-release-sync": "^0.31.0",
|
||||
"@sebastianwessel/quickjs": "^3.0.0",
|
||||
"@tlsn/common": "*",
|
||||
"quickjs-emscripten": "^0.31.0"
|
||||
"quickjs-emscripten": "^0.31.0",
|
||||
"uuid": "^13.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,4 +151,7 @@ describe('extractConfig', () => {
|
||||
expect(result?.name).toBe('Backtick Plugin');
|
||||
expect(result?.description).toBe('Uses template literals');
|
||||
});
|
||||
|
||||
// Note: The regex-based extractConfig cannot handle array fields like requests and urls.
|
||||
// For full config extraction including permissions, use Host.getPluginConfig() which uses QuickJS sandbox.
|
||||
});
|
||||
|
||||
135
packages/plugin-sdk/src/globals.d.ts
vendored
Normal file
135
packages/plugin-sdk/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Global type declarations for TLSNotary plugin runtime environment
|
||||
*
|
||||
* These functions are injected at runtime by the plugin sandbox.
|
||||
* They are automatically available as globals in TypeScript plugins.
|
||||
*/
|
||||
|
||||
import type {
|
||||
InterceptedRequest,
|
||||
InterceptedRequestHeader,
|
||||
Handler,
|
||||
DomOptions,
|
||||
DomJson,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Create a div DOM element
|
||||
*/
|
||||
export type DivFunction = {
|
||||
(options?: DomOptions, children?: DomJson[]): DomJson;
|
||||
(children: DomJson[]): DomJson;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a button DOM element
|
||||
*/
|
||||
export type ButtonFunction = {
|
||||
(options?: DomOptions, children?: DomJson[]): DomJson;
|
||||
(children: DomJson[]): DomJson;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a new browser window
|
||||
*/
|
||||
export type OpenWindowFunction = (
|
||||
url: string,
|
||||
options?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
showOverlay?: boolean;
|
||||
}
|
||||
) => Promise<{
|
||||
windowId: number;
|
||||
uuid: string;
|
||||
tabId: number;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* React-like effect hook that runs when dependencies change
|
||||
*/
|
||||
export type UseEffectFunction = (callback: () => void, deps: any[]) => void;
|
||||
|
||||
/**
|
||||
* Subscribe to intercepted HTTP headers with filtering
|
||||
*/
|
||||
export type UseHeadersFunction = (
|
||||
filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[]
|
||||
) => InterceptedRequestHeader[];
|
||||
|
||||
/**
|
||||
* Subscribe to intercepted HTTP requests with filtering
|
||||
*/
|
||||
export type UseRequestsFunction = (
|
||||
filter: (requests: InterceptedRequest[]) => InterceptedRequest[]
|
||||
) => InterceptedRequest[];
|
||||
|
||||
/**
|
||||
* Get state value (does not trigger re-render)
|
||||
*/
|
||||
export type UseStateFunction = <T>(key: string, defaultValue: T) => T;
|
||||
|
||||
/**
|
||||
* Set state value (triggers UI re-render)
|
||||
*/
|
||||
export type SetStateFunction = <T>(key: string, value: T) => void;
|
||||
|
||||
/**
|
||||
* Generate TLS proof using the unified prove() API
|
||||
*/
|
||||
export type ProveFunction = (
|
||||
requestOptions: {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string | undefined>;
|
||||
body?: string;
|
||||
},
|
||||
proverOptions: {
|
||||
verifierUrl: string;
|
||||
proxyUrl: string;
|
||||
maxRecvData?: number;
|
||||
maxSentData?: number;
|
||||
handlers: Handler[];
|
||||
}
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Complete plugin execution and return result
|
||||
*/
|
||||
export type DoneFunction = (result?: any) => void;
|
||||
|
||||
/**
|
||||
* Complete Plugin API surface available in the QuickJS sandbox
|
||||
*/
|
||||
export interface PluginAPI {
|
||||
div: DivFunction;
|
||||
button: ButtonFunction;
|
||||
openWindow: OpenWindowFunction;
|
||||
useEffect: UseEffectFunction;
|
||||
useHeaders: UseHeadersFunction;
|
||||
useRequests: UseRequestsFunction;
|
||||
useState: UseStateFunction;
|
||||
setState: SetStateFunction;
|
||||
prove: ProveFunction;
|
||||
done: DoneFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global declarations for plugin environment
|
||||
*
|
||||
* These are automatically available in TypeScript plugins without imports.
|
||||
*/
|
||||
declare global {
|
||||
const div: DivFunction;
|
||||
const button: ButtonFunction;
|
||||
const openWindow: OpenWindowFunction;
|
||||
const useEffect: UseEffectFunction;
|
||||
const useHeaders: UseHeadersFunction;
|
||||
const useRequests: UseRequestsFunction;
|
||||
const useState: UseStateFunction;
|
||||
const setState: SetStateFunction;
|
||||
const prove: ProveFunction;
|
||||
const done: DoneFunction;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
WindowMessage,
|
||||
Handler,
|
||||
PluginConfig,
|
||||
RequestPermission,
|
||||
} from './types';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
@@ -738,8 +739,11 @@ async function waitForWindow(callback: () => Promise<any>, retry = 0): Promise<a
|
||||
|
||||
/**
|
||||
* Extract plugin configuration from plugin code without executing it.
|
||||
* Uses regex-based parsing to extract the config object from the source code
|
||||
* without running any JavaScript.
|
||||
* Uses regex-based parsing to extract the config object from the source code.
|
||||
*
|
||||
* Note: This regex-based approach cannot extract complex fields like arrays
|
||||
* (requests, urls). For full config extraction including permissions, use
|
||||
* Host.getPluginConfig() which uses the QuickJS sandbox.
|
||||
*
|
||||
* @param code - The plugin source code
|
||||
* @returns The plugin config object, or null if extraction fails
|
||||
@@ -792,7 +796,40 @@ export async function extractConfig(code: string): Promise<PluginConfig | null>
|
||||
}
|
||||
|
||||
// Export types
|
||||
export type { PluginConfig };
|
||||
export type {
|
||||
PluginConfig,
|
||||
RequestPermission,
|
||||
Handler,
|
||||
StartLineHandler,
|
||||
HeadersHandler,
|
||||
BodyHandler,
|
||||
AllHandler,
|
||||
HandlerType,
|
||||
HandlerPart,
|
||||
HandlerAction,
|
||||
InterceptedRequest,
|
||||
InterceptedRequestHeader,
|
||||
DomJson,
|
||||
DomOptions,
|
||||
OpenWindowResponse,
|
||||
WindowMessage,
|
||||
ExecutionContext,
|
||||
} from './types';
|
||||
|
||||
// Export Plugin API types
|
||||
export type {
|
||||
PluginAPI,
|
||||
DivFunction,
|
||||
ButtonFunction,
|
||||
OpenWindowFunction,
|
||||
UseEffectFunction,
|
||||
UseHeadersFunction,
|
||||
UseRequestsFunction,
|
||||
UseStateFunction,
|
||||
SetStateFunction,
|
||||
ProveFunction,
|
||||
DoneFunction,
|
||||
} from './globals';
|
||||
|
||||
// Re-export LogLevel for consumers
|
||||
export { LogLevel } from '@tlsn/common';
|
||||
|
||||
@@ -929,6 +929,85 @@ describe('Parser', () => {
|
||||
|
||||
expect(ranges).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should extract entire array value when accessing array field directly', () => {
|
||||
const request =
|
||||
'POST /api HTTP/1.1\r\n' +
|
||||
'Content-Type: application/json\r\n' +
|
||||
'\r\n' +
|
||||
'{"a":[{"b":1},{"c":2}]}';
|
||||
|
||||
const parser = new Parser(request);
|
||||
const ranges = parser.ranges.body('a', { type: 'json' });
|
||||
|
||||
expect(ranges).toHaveLength(1);
|
||||
const extracted = request.substring(ranges[0].start, ranges[0].end);
|
||||
// Should include the key and the entire array value
|
||||
expect(extracted).toContain('"a"');
|
||||
expect(extracted).toContain('[{"b":1},{"c":2}]');
|
||||
});
|
||||
|
||||
it('should extract only array value with hideKey option', () => {
|
||||
const request =
|
||||
'POST /api HTTP/1.1\r\n' +
|
||||
'Content-Type: application/json\r\n' +
|
||||
'\r\n' +
|
||||
'{"a":[{"b":1},{"c":2}]}';
|
||||
|
||||
const parser = new Parser(request);
|
||||
const ranges = parser.ranges.body('a', { type: 'json', hideKey: true });
|
||||
|
||||
expect(ranges).toHaveLength(1);
|
||||
const extracted = request.substring(ranges[0].start, ranges[0].end);
|
||||
expect(extracted).toBe('[{"b":1},{"c":2}]');
|
||||
expect(extracted).not.toContain('"a"');
|
||||
});
|
||||
|
||||
it('should extract only array key with hideValue option', () => {
|
||||
const request =
|
||||
'POST /api HTTP/1.1\r\n' +
|
||||
'Content-Type: application/json\r\n' +
|
||||
'\r\n' +
|
||||
'{"a":[{"b":1},{"c":2}]}';
|
||||
|
||||
const parser = new Parser(request);
|
||||
const ranges = parser.ranges.body('a', { type: 'json', hideValue: true });
|
||||
|
||||
expect(ranges).toHaveLength(1);
|
||||
const extracted = request.substring(ranges[0].start, ranges[0].end);
|
||||
expect(extracted).toBe('"a"');
|
||||
expect(extracted).not.toContain('[');
|
||||
});
|
||||
|
||||
it('should extract array of primitives', () => {
|
||||
const request =
|
||||
'POST /api HTTP/1.1\r\n' +
|
||||
'Content-Type: application/json\r\n' +
|
||||
'\r\n' +
|
||||
'{"numbers":[1,2,3,4,5]}';
|
||||
|
||||
const parser = new Parser(request);
|
||||
const ranges = parser.ranges.body('numbers', { type: 'json', hideKey: true });
|
||||
|
||||
expect(ranges).toHaveLength(1);
|
||||
const extracted = request.substring(ranges[0].start, ranges[0].end);
|
||||
expect(extracted).toBe('[1,2,3,4,5]');
|
||||
});
|
||||
|
||||
it('should extract nested array field', () => {
|
||||
const request =
|
||||
'POST /api HTTP/1.1\r\n' +
|
||||
'Content-Type: application/json\r\n' +
|
||||
'\r\n' +
|
||||
'{"data":{"items":[1,2,3]}}';
|
||||
|
||||
const parser = new Parser(request);
|
||||
const ranges = parser.ranges.body('data.items', { type: 'json', hideKey: true });
|
||||
|
||||
expect(ranges).toHaveLength(1);
|
||||
const extracted = request.substring(ranges[0].start, ranges[0].end);
|
||||
expect(extracted).toBe('[1,2,3]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed Paths (Objects and Arrays)', () => {
|
||||
|
||||
@@ -425,7 +425,33 @@ export class Parser {
|
||||
// Handle different value types
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (Array.isArray(value)) {
|
||||
// Handle array
|
||||
// Handle array - first store the array itself, then process elements
|
||||
const valueStr = JSON.stringify(value);
|
||||
const valueBytes = Buffer.from(valueStr, 'utf8');
|
||||
const valueByteIndex = textBytes.indexOf(valueBytes, actualValueByteStart);
|
||||
|
||||
if (valueByteIndex !== -1) {
|
||||
const valueByteEnd = valueByteIndex + valueBytes.length;
|
||||
|
||||
// Store the array itself as a field
|
||||
result[pathKey] = {
|
||||
value: value,
|
||||
ranges: {
|
||||
start: baseOffset + keyByteIndex,
|
||||
end: baseOffset + valueByteEnd,
|
||||
},
|
||||
keyRange: {
|
||||
start: baseOffset + keyByteIndex,
|
||||
end: baseOffset + keyByteIndex + keyBytes.length,
|
||||
},
|
||||
valueRange: {
|
||||
start: baseOffset + valueByteIndex,
|
||||
end: baseOffset + valueByteEnd,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Then recursively process array elements
|
||||
this.processJsonArray(
|
||||
value,
|
||||
textBytes,
|
||||
|
||||
361
packages/plugin-sdk/src/styles.ts
Normal file
361
packages/plugin-sdk/src/styles.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Tailwind-like style utilities for plugin UI components
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// DESIGN TOKENS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Color palette with Tailwind-like naming
|
||||
* Non-opinionated color scales from 100-900
|
||||
*/
|
||||
const colorTokens = {
|
||||
// Neutral
|
||||
'white': '#ffffff',
|
||||
'black': '#000000',
|
||||
'transparent': 'transparent',
|
||||
|
||||
// Gray scale
|
||||
'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',
|
||||
|
||||
// Blue
|
||||
'blue-100': '#dbeafe',
|
||||
'blue-200': '#bfdbfe',
|
||||
'blue-300': '#93c5fd',
|
||||
'blue-400': '#60a5fa',
|
||||
'blue-500': '#3b82f6',
|
||||
'blue-600': '#2563eb',
|
||||
'blue-700': '#1d4ed8',
|
||||
'blue-800': '#1e40af',
|
||||
'blue-900': '#1e3a8a',
|
||||
|
||||
// Purple
|
||||
'purple-100': '#f3e8ff',
|
||||
'purple-200': '#e9d5ff',
|
||||
'purple-300': '#d8b4fe',
|
||||
'purple-400': '#c084fc',
|
||||
'purple-500': '#a855f7',
|
||||
'purple-600': '#9333ea',
|
||||
'purple-700': '#7e22ce',
|
||||
'purple-800': '#6b21a8',
|
||||
'purple-900': '#581c87',
|
||||
|
||||
// Red
|
||||
'red-100': '#fee2e2',
|
||||
'red-200': '#fecaca',
|
||||
'red-300': '#fca5a5',
|
||||
'red-400': '#f87171',
|
||||
'red-500': '#ef4444',
|
||||
'red-600': '#dc2626',
|
||||
'red-700': '#b91c1c',
|
||||
'red-800': '#991b1b',
|
||||
'red-900': '#7f1d1d',
|
||||
|
||||
// Yellow
|
||||
'yellow-100': '#fef3c7',
|
||||
'yellow-200': '#fde68a',
|
||||
'yellow-300': '#fcd34d',
|
||||
'yellow-400': '#fbbf24',
|
||||
'yellow-500': '#f59e0b',
|
||||
'yellow-600': '#d97706',
|
||||
'yellow-700': '#b45309',
|
||||
'yellow-800': '#92400e',
|
||||
'yellow-900': '#78350f',
|
||||
|
||||
// Orange
|
||||
'orange-100': '#ffedd5',
|
||||
'orange-200': '#fed7aa',
|
||||
'orange-300': '#fdba74',
|
||||
'orange-400': '#fb923c',
|
||||
'orange-500': '#f97316',
|
||||
'orange-600': '#ea580c',
|
||||
'orange-700': '#c2410c',
|
||||
'orange-800': '#9a3412',
|
||||
'orange-900': '#7c2d12',
|
||||
|
||||
// Green
|
||||
'green-100': '#d1fae5',
|
||||
'green-200': '#a7f3d0',
|
||||
'green-300': '#6ee7b7',
|
||||
'green-400': '#34d399',
|
||||
'green-500': '#10b981',
|
||||
'green-600': '#059669',
|
||||
'green-700': '#047857',
|
||||
'green-800': '#065f46',
|
||||
'green-900': '#064e3b',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Spacing scale
|
||||
*/
|
||||
const spacingTokens = {
|
||||
'0': '0',
|
||||
'1': '4px',
|
||||
'2': '8px',
|
||||
'3': '12px',
|
||||
'4': '16px',
|
||||
'5': '20px',
|
||||
'6': '24px',
|
||||
'8': '32px',
|
||||
'10': '40px',
|
||||
'12': '48px',
|
||||
|
||||
// Named aliases
|
||||
'xs': '8px',
|
||||
'sm': '12px',
|
||||
'md': '16px',
|
||||
'lg': '20px',
|
||||
'xl': '24px',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Font sizes
|
||||
*/
|
||||
const fontSizeTokens = {
|
||||
'xs': '12px',
|
||||
'sm': '14px',
|
||||
'md': '15px',
|
||||
'base': '16px',
|
||||
'lg': '18px',
|
||||
'xl': '20px',
|
||||
'2xl': '24px',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Font weights
|
||||
*/
|
||||
const fontWeightTokens = {
|
||||
'normal': '400',
|
||||
'medium': '500',
|
||||
'semibold': '600',
|
||||
'bold': '700',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Border radius
|
||||
*/
|
||||
const borderRadiusTokens = {
|
||||
'none': '0',
|
||||
'sm': '6px',
|
||||
'md': '8px',
|
||||
'lg': '12px',
|
||||
'full': '9999px',
|
||||
'circle': '50%',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Box shadows
|
||||
*/
|
||||
const shadowTokens = {
|
||||
'sm': '0 2px 4px rgba(0,0,0,0.1)',
|
||||
'md': '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
'lg': '0 4px 8px rgba(0,0,0,0.3)',
|
||||
'xl': '0 10px 25px rgba(0,0,0,0.2)',
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// TYPE DEFINITIONS
|
||||
// =============================================================================
|
||||
|
||||
type StyleObject = Record<string, string>;
|
||||
type StyleHelper = StyleObject | false | null | undefined;
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Resolve a color token to its CSS value
|
||||
*/
|
||||
function resolveColor(token: string): string {
|
||||
return colorTokens[token as keyof typeof colorTokens] || token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a spacing token to its CSS value
|
||||
*/
|
||||
function resolveSpacing(token: string): string {
|
||||
return spacingTokens[token as keyof typeof spacingTokens] || token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a font size token to its CSS value
|
||||
*/
|
||||
function resolveFontSize(token: string): string {
|
||||
return fontSizeTokens[token as keyof typeof fontSizeTokens] || token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a font weight token to its CSS value
|
||||
*/
|
||||
function resolveFontWeight(token: string): string {
|
||||
return fontWeightTokens[token as keyof typeof fontWeightTokens] || token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a border radius token to its CSS value
|
||||
*/
|
||||
function resolveBorderRadius(token: string): string {
|
||||
return borderRadiusTokens[token as keyof typeof borderRadiusTokens] || token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a shadow token to its CSS value
|
||||
*/
|
||||
function resolveShadow(token: string): string {
|
||||
return shadowTokens[token as keyof typeof shadowTokens] || token;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STYLE HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// Color helpers
|
||||
export const color = (value: string): StyleObject => ({ color: resolveColor(value) });
|
||||
export const bgColor = (value: string): StyleObject => ({ backgroundColor: resolveColor(value) });
|
||||
export const borderColor = (value: string): StyleObject => ({ borderColor: resolveColor(value) });
|
||||
export const bg = bgColor; // Alias
|
||||
|
||||
// Spacing helpers - Padding
|
||||
export const padding = (value: string): StyleObject => ({ padding: resolveSpacing(value) });
|
||||
export const paddingX = (value: string): StyleObject => {
|
||||
const val = resolveSpacing(value);
|
||||
return { paddingLeft: val, paddingRight: val };
|
||||
};
|
||||
export const paddingY = (value: string): StyleObject => {
|
||||
const val = resolveSpacing(value);
|
||||
return { paddingTop: val, paddingBottom: val };
|
||||
};
|
||||
export const paddingTop = (value: string): StyleObject => ({ paddingTop: resolveSpacing(value) });
|
||||
export const paddingBottom = (value: string): StyleObject => ({ paddingBottom: resolveSpacing(value) });
|
||||
export const paddingLeft = (value: string): StyleObject => ({ paddingLeft: resolveSpacing(value) });
|
||||
export const paddingRight = (value: string): StyleObject => ({ paddingRight: resolveSpacing(value) });
|
||||
|
||||
// Aliases
|
||||
export const p = padding;
|
||||
export const px = paddingX;
|
||||
export const py = paddingY;
|
||||
export const pt = paddingTop;
|
||||
export const pb = paddingBottom;
|
||||
export const pl = paddingLeft;
|
||||
export const pr = paddingRight;
|
||||
|
||||
// Spacing helpers - Margin
|
||||
export const margin = (value: string): StyleObject => ({ margin: resolveSpacing(value) });
|
||||
export const marginX = (value: string): StyleObject => {
|
||||
const val = resolveSpacing(value);
|
||||
return { marginLeft: val, marginRight: val };
|
||||
};
|
||||
export const marginY = (value: string): StyleObject => {
|
||||
const val = resolveSpacing(value);
|
||||
return { marginTop: val, marginBottom: val };
|
||||
};
|
||||
export const marginTop = (value: string): StyleObject => ({ marginTop: resolveSpacing(value) });
|
||||
export const marginBottom = (value: string): StyleObject => ({ marginBottom: resolveSpacing(value) });
|
||||
export const marginLeft = (value: string): StyleObject => ({ marginLeft: resolveSpacing(value) });
|
||||
export const marginRight = (value: string): StyleObject => ({ marginRight: resolveSpacing(value) });
|
||||
|
||||
// Aliases
|
||||
export const m = margin;
|
||||
export const mx = marginX;
|
||||
export const my = marginY;
|
||||
export const mt = marginTop;
|
||||
export const mb = marginBottom;
|
||||
export const ml = marginLeft;
|
||||
export const mr = marginRight;
|
||||
|
||||
// Typography helpers
|
||||
export const fontSize = (value: string): StyleObject => ({ fontSize: resolveFontSize(value) });
|
||||
export const fontWeight = (value: string): StyleObject => ({ fontWeight: resolveFontWeight(value) });
|
||||
export const textAlign = (value: string): StyleObject => ({ textAlign: value });
|
||||
export const fontFamily = (value: string): StyleObject => ({ fontFamily: value });
|
||||
|
||||
// Layout helpers
|
||||
export const display = (value: string): StyleObject => ({ display: value });
|
||||
export const position = (value: string): StyleObject => ({ position: value });
|
||||
export const width = (value: string): StyleObject => ({ width: value });
|
||||
export const height = (value: string): StyleObject => ({ height: value });
|
||||
export const minWidth = (value: string): StyleObject => ({ minWidth: value });
|
||||
export const minHeight = (value: string): StyleObject => ({ minHeight: value });
|
||||
export const maxWidth = (value: string): StyleObject => ({ maxWidth: value });
|
||||
export const maxHeight = (value: string): StyleObject => ({ maxHeight: value });
|
||||
|
||||
// Flexbox helpers
|
||||
export const flex = (value: string = '1'): StyleObject => ({ flex: value });
|
||||
export const flexDirection = (value: string): StyleObject => ({ flexDirection: value });
|
||||
export const alignItems = (value: string): StyleObject => ({ alignItems: value });
|
||||
export const justifyContent = (value: string): StyleObject => ({ justifyContent: value });
|
||||
export const flexWrap = (value: string): StyleObject => ({ flexWrap: value });
|
||||
|
||||
// Positioning helpers
|
||||
export const top = (value: string): StyleObject => ({ top: resolveSpacing(value) });
|
||||
export const bottom = (value: string): StyleObject => ({ bottom: resolveSpacing(value) });
|
||||
export const left = (value: string): StyleObject => ({ left: resolveSpacing(value) });
|
||||
export const right = (value: string): StyleObject => ({ right: resolveSpacing(value) });
|
||||
|
||||
// Border helpers
|
||||
export const border = (value: string): StyleObject => ({ border: value });
|
||||
export const borderRadius = (value: string): StyleObject => ({ borderRadius: resolveBorderRadius(value) });
|
||||
export const borderWidth = (value: string): StyleObject => ({ borderWidth: value });
|
||||
|
||||
// Visual helpers
|
||||
export const boxShadow = (value: string): StyleObject => ({ boxShadow: resolveShadow(value) });
|
||||
export const opacity = (value: string): StyleObject => ({ opacity: value });
|
||||
export const overflow = (value: string): StyleObject => ({ overflow: value });
|
||||
export const zIndex = (value: string): StyleObject => ({ zIndex: value });
|
||||
|
||||
// Interaction helpers
|
||||
export const cursor = (value: string): StyleObject => ({ cursor: value });
|
||||
export const pointerEvents = (value: string): StyleObject => ({ pointerEvents: value });
|
||||
|
||||
// Transition/Animation helpers
|
||||
export const transition = (value: string = 'all 0.2s ease'): StyleObject => ({ transition: value });
|
||||
|
||||
// Background helpers
|
||||
export const background = (value: string): StyleObject => ({ background: value });
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INLINE STYLE FUNCTION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Combine multiple style helpers into a single style object
|
||||
* Automatically filters out falsey values for conditional styling
|
||||
*
|
||||
* @example
|
||||
* inlineStyle(
|
||||
* textAlign('center'),
|
||||
* color('gray-500'),
|
||||
* padding('sm'),
|
||||
* bgColor('yellow-100'),
|
||||
* isPending && display('none'),
|
||||
* { borderRadius: '12px' }
|
||||
* )
|
||||
*/
|
||||
export function inlineStyle(...styles: StyleHelper[]): StyleObject {
|
||||
return styles.reduce<StyleObject>((acc, style) => {
|
||||
if (style) {
|
||||
Object.assign(acc, style);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Common font family
|
||||
*/
|
||||
export const defaultFontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
||||
@@ -185,6 +185,33 @@ export type AllHandler = {
|
||||
|
||||
export type Handler = StartLineHandler | HeadersHandler | BodyHandler | AllHandler;
|
||||
|
||||
/**
|
||||
* Permission for making HTTP requests via prove()
|
||||
*/
|
||||
export interface RequestPermission {
|
||||
/** HTTP method (GET, POST, etc.) */
|
||||
method: string;
|
||||
|
||||
/** Host name (e.g., "api.x.com") */
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* URL pathname pattern (URLPattern syntax, e.g., "/1.1/users/*")
|
||||
* Supports wildcards: * matches any single segment, ** matches multiple segments
|
||||
*/
|
||||
pathname: string;
|
||||
|
||||
/** Verifier URL to use for this request */
|
||||
verifierUrl: string;
|
||||
|
||||
/**
|
||||
* Proxy URL for WebSocket connection.
|
||||
* Defaults to ws/wss of verifierUrl's /proxy endpoint if not specified.
|
||||
* e.g., verifierUrl "https://verifier.example.com" -> "wss://verifier.example.com/proxy?token={host}"
|
||||
*/
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin configuration object that all plugins must export
|
||||
*/
|
||||
@@ -197,4 +224,17 @@ export interface PluginConfig {
|
||||
version?: string;
|
||||
/** Optional author name */
|
||||
author?: string;
|
||||
|
||||
/**
|
||||
* Allowed HTTP requests the plugin can make via prove().
|
||||
* Empty array or undefined means no prove() calls allowed.
|
||||
*/
|
||||
requests?: RequestPermission[];
|
||||
|
||||
/**
|
||||
* Allowed URLs the plugin can open via openWindow().
|
||||
* Supports URLPattern syntax (e.g., "https://x.com/*").
|
||||
* Empty array or undefined means no openWindow() calls allowed.
|
||||
*/
|
||||
urls?: string[];
|
||||
}
|
||||
|
||||
@@ -15,26 +15,16 @@ export default defineConfig({
|
||||
build: {
|
||||
target: 'es2020',
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||
name: 'TLSNPluginSDK',
|
||||
formats: ['es', 'cjs', 'umd'],
|
||||
fileName: (format) => {
|
||||
if (format === 'es') return 'index.js';
|
||||
if (format === 'cjs') return 'index.cjs';
|
||||
if (format === 'umd') return 'index.umd.js';
|
||||
return `index.${format}.js`;
|
||||
entry: {
|
||||
index: path.resolve(__dirname, 'src/index.ts'),
|
||||
styles: path.resolve(__dirname, 'src/styles.ts'),
|
||||
},
|
||||
formats: ['es'],
|
||||
},
|
||||
rollupOptions: {
|
||||
// Externalize QuickJS and Node.js dependencies
|
||||
external: ['@sebastianwessel/quickjs', '@jitl/quickjs-ng-wasmfile-release-sync', /^node:.*/],
|
||||
external: ['@sebastianwessel/quickjs', '@jitl/quickjs-ng-wasmfile-release-sync', /^node:.*/, '@tlsn/common'],
|
||||
output: {
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps
|
||||
globals: {
|
||||
'@sebastianwessel/quickjs': 'QuickJS',
|
||||
'@jitl/quickjs-ng-wasmfile-release-sync': 'QuickJSVariant',
|
||||
},
|
||||
exports: 'named',
|
||||
},
|
||||
},
|
||||
|
||||
19
packages/ts-plugin-sample/.gitignore
vendored
Normal file
19
packages/ts-plugin-sample/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Build output
|
||||
build/
|
||||
*.js
|
||||
*.js.map
|
||||
*.d.ts
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
312
packages/ts-plugin-sample/README.md
Normal file
312
packages/ts-plugin-sample/README.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# TypeScript Plugin Sample
|
||||
|
||||
A TypeScript implementation of the X Profile Prover plugin demonstrating how to write type-safe TLSN plugins.
|
||||
|
||||
## Overview
|
||||
|
||||
This package shows how to:
|
||||
- Write TLSN plugins in TypeScript with full type safety
|
||||
- Import types from `@tlsn/plugin-sdk`
|
||||
- Compile TypeScript plugins to JavaScript for execution
|
||||
- Use all plugin API features (prove, openWindow, UI rendering, hooks)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd packages/ts-plugin-sample
|
||||
npm install
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This bundles `src/index.ts` and `src/config.ts` into a single `build/index.js` file with clean `export default` statement.
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Watches for changes and rebuilds automatically.
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
Runs TypeScript type checking without emitting files.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ts-plugin-sample/
|
||||
├── package.json # Dependencies and build scripts
|
||||
├── tsconfig.json # TypeScript compiler configuration
|
||||
├── build-wrapper.cjs # Custom build script for clean exports
|
||||
├── src/
|
||||
│ ├── index.ts # TypeScript plugin implementation
|
||||
│ └── config.ts # Plugin configuration
|
||||
├── build/
|
||||
│ ├── index.js # Bundled plugin with export default
|
||||
│ └── index.js.map # Source map for debugging
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## TypeScript Features
|
||||
|
||||
### Type Imports
|
||||
|
||||
Import types from the plugin SDK for compile-time checking:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
PluginConfig,
|
||||
RequestPermission,
|
||||
Handler,
|
||||
HandlerType,
|
||||
HandlerPart,
|
||||
HandlerAction,
|
||||
InterceptedRequestHeader,
|
||||
DomJson,
|
||||
} from '@tlsn/plugin-sdk';
|
||||
```
|
||||
|
||||
### Plugin Config Type Safety
|
||||
|
||||
```typescript
|
||||
const config: PluginConfig = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
version: '0.1.0',
|
||||
author: 'TLSN Team',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'https://verifier.tlsnotary.org',
|
||||
} satisfies RequestPermission,
|
||||
],
|
||||
urls: ['https://x.com/*'],
|
||||
};
|
||||
```
|
||||
|
||||
### Plugin API Globals
|
||||
|
||||
The plugin execution environment (QuickJS sandbox) provides these globals:
|
||||
|
||||
```typescript
|
||||
// Declare types for globals injected by the sandbox
|
||||
declare function div(options?: DomOptions, children?: DomJson[]): DomJson;
|
||||
declare function button(options?: DomOptions, children?: DomJson[]): DomJson;
|
||||
declare function openWindow(url: string, options?: {...}): Promise<{...}>;
|
||||
declare function useEffect(callback: () => void, deps: any[]): void;
|
||||
declare function useHeaders(filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[]): InterceptedRequestHeader[];
|
||||
declare function useState<T>(key: string, defaultValue: T): T;
|
||||
declare function setState<T>(key: string, value: T): void;
|
||||
declare function prove(requestOptions: {...}, proverOptions: {...}): Promise<any>;
|
||||
declare function done(result?: any): void;
|
||||
```
|
||||
|
||||
### Type-Safe Handlers
|
||||
|
||||
```typescript
|
||||
const handlers: Handler[] = [
|
||||
{
|
||||
type: 'SENT' as HandlerType,
|
||||
part: 'START_LINE' as HandlerPart,
|
||||
action: 'REVEAL' as HandlerAction,
|
||||
},
|
||||
{
|
||||
type: 'RECV' as HandlerType,
|
||||
part: 'BODY' as HandlerPart,
|
||||
action: 'REVEAL' as HandlerAction,
|
||||
params: {
|
||||
type: 'json',
|
||||
path: 'screen_name',
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Key Differences from JavaScript
|
||||
|
||||
### 1. Type Annotations
|
||||
|
||||
```typescript
|
||||
// JavaScript
|
||||
function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
// ...
|
||||
}
|
||||
|
||||
// TypeScript
|
||||
async function onClick(): Promise<void> {
|
||||
const isRequestPending = useState<boolean>('isRequestPending', false);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Interface Compliance
|
||||
|
||||
TypeScript ensures your config matches the `PluginConfig` interface:
|
||||
|
||||
```typescript
|
||||
const config: PluginConfig = {
|
||||
name: 'X Profile Prover', // ✓ Required
|
||||
description: 'Proves X profile', // ✓ Required
|
||||
version: '0.1.0', // ✓ Optional
|
||||
requests: [...], // ✓ Optional
|
||||
urls: [...], // ✓ Optional
|
||||
// TypeScript will error if required fields are missing!
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Compile-Time Errors
|
||||
|
||||
```typescript
|
||||
// This will error at compile time:
|
||||
const handler: Handler = {
|
||||
type: 'INVALID', // ❌ Type '"INVALID"' is not assignable to type 'HandlerType'
|
||||
part: 'BODY',
|
||||
action: 'REVEAL',
|
||||
};
|
||||
|
||||
// This will pass:
|
||||
const handler: Handler = {
|
||||
type: 'RECV', // ✓ Valid HandlerType
|
||||
part: 'BODY',
|
||||
action: 'REVEAL',
|
||||
};
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### Build Tool: esbuild + Custom Wrapper
|
||||
|
||||
The plugin uses **esbuild** with a custom build wrapper:
|
||||
- **Single file output:** All code bundled into `build/index.js` (7.2KB, 257 lines)
|
||||
- **ES Module format:** Standard `export default` statement
|
||||
- **No external imports:** All dependencies bundled inline
|
||||
- **Inlined enums:** Handler enums included directly (no SDK imports)
|
||||
- **Source maps:** Generated for debugging (`build/index.js.map`)
|
||||
- **Fast builds:** ~10ms typical build time
|
||||
|
||||
The build wrapper (`build-wrapper.cjs`) transforms the esbuild output to use a clean `export default` statement matching the JavaScript plugin format.
|
||||
|
||||
### TypeScript Config (`tsconfig.json`)
|
||||
|
||||
TypeScript is used for type checking only (`npm run typecheck`):
|
||||
- **Target:** ES2020 (modern browser features)
|
||||
- **Strict:** Full type checking enabled
|
||||
- **Global types:** Includes SDK globals for plugin API functions
|
||||
|
||||
## Loading in Extension
|
||||
|
||||
After building, the compiled `build/index.js` can be loaded in the TLSN extension:
|
||||
|
||||
1. Build the plugin: `npm run build`
|
||||
2. The output is `build/index.js` with clean ES module export:
|
||||
```javascript
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
```
|
||||
3. Load and execute in the extension:
|
||||
```javascript
|
||||
const pluginCode = fs.readFileSync('build/index.js', 'utf8');
|
||||
const plugin = await sandbox.eval(pluginCode);
|
||||
// plugin = { main, onClick, expandUI, minimizeUI, config }
|
||||
```
|
||||
4. The plugin executes with full type safety verified at compile time
|
||||
|
||||
**Output Characteristics:**
|
||||
- ✅ Single file with `export default` statement
|
||||
- ✅ No external imports (all dependencies bundled)
|
||||
- ✅ Inlined enums (no SDK runtime dependency)
|
||||
- ✅ ES Module format
|
||||
- ✅ Matches JavaScript plugin structure
|
||||
|
||||
## Comparison with JavaScript Plugin
|
||||
|
||||
See `packages/demo/generated/twitter.js` for the equivalent JavaScript implementation.
|
||||
|
||||
**Advantages of TypeScript:**
|
||||
- Compile-time type checking
|
||||
- IDE autocomplete and IntelliSense
|
||||
- Catches errors before runtime
|
||||
- Better documentation via types
|
||||
- Refactoring safety
|
||||
|
||||
**Trade-offs:**
|
||||
- Requires build step
|
||||
- Slightly more verbose (type annotations)
|
||||
- Need to maintain type declarations
|
||||
|
||||
## Development Tips
|
||||
|
||||
### 1. Use Type Inference
|
||||
|
||||
TypeScript can infer many types:
|
||||
|
||||
```typescript
|
||||
// Explicit (verbose)
|
||||
const header: InterceptedRequestHeader | undefined = useHeaders(...)[0];
|
||||
|
||||
// Inferred (cleaner)
|
||||
const [header] = useHeaders(...); // Type inferred from useHeaders return type
|
||||
```
|
||||
|
||||
### 2. Use `satisfies` for Config
|
||||
|
||||
```typescript
|
||||
// Good: Type-checked but allows literal types
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
// ...
|
||||
} satisfies RequestPermission,
|
||||
]
|
||||
|
||||
// Also good: Full type annotation
|
||||
const request: RequestPermission = {
|
||||
method: 'GET',
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Enable Strict Mode
|
||||
|
||||
Keep `"strict": true` in `tsconfig.json` for maximum type safety.
|
||||
|
||||
### 4. Check Build Errors
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
|
||||
# Check for type errors without building
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||
- [Plugin SDK Types](../plugin-sdk/src/types.ts)
|
||||
- [JavaScript Plugin Example](../demo/generated/twitter.js)
|
||||
- [TLSN Extension Docs](../../CLAUDE.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
20
packages/ts-plugin-sample/build-wrapper.cjs
Executable file
20
packages/ts-plugin-sample/build-wrapper.cjs
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build wrapper to create clean export default statement
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Run esbuild
|
||||
console.log('Building with esbuild...');
|
||||
execSync('esbuild src/index.ts --bundle --format=esm --outfile=build/index.js --sourcemap --external:@sebastianwessel/quickjs --external:@jitl/quickjs-ng-wasmfile-release-sync --external:uuid --external:fast-deep-equal', {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// Read the generated code
|
||||
let code = fs.readFileSync('build/index.js', 'utf8');
|
||||
|
||||
// Write back
|
||||
fs.writeFileSync('build/index.js', code);
|
||||
|
||||
console.log('✓ Build complete: build/index.js');
|
||||
33
packages/ts-plugin-sample/package.json
Normal file
33
packages/ts-plugin-sample/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tlsn/ts-plugin-sample",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"description": "TypeScript plugin sample for TLSN extension",
|
||||
"type": "module",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "node build-wrapper.cjs",
|
||||
"clean": "rm -rf build",
|
||||
"dev": "esbuild src/index.ts --bundle --format=esm --outfile=build/index.js --sourcemap --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"tlsn",
|
||||
"plugin",
|
||||
"typescript",
|
||||
"example"
|
||||
],
|
||||
"author": "TLSN Team",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tlsnotary/tlsn-extension.git",
|
||||
"directory": "packages/ts-plugin-sample"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tlsn/plugin-sdk": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
58
packages/ts-plugin-sample/src/components/FloatingButton.ts
Normal file
58
packages/ts-plugin-sample/src/components/FloatingButton.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* FloatingButton Component
|
||||
*
|
||||
* Minimized floating action button
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
position,
|
||||
bottom,
|
||||
right,
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
bgColor,
|
||||
boxShadow,
|
||||
zIndex,
|
||||
display,
|
||||
alignItems,
|
||||
justifyContent,
|
||||
cursor,
|
||||
fontSize,
|
||||
color,
|
||||
transition,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
|
||||
export interface FloatingButtonProps {
|
||||
onClick: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function FloatingButton({ onClick, icon = '🔐' }: FloatingButtonProps): DomJson {
|
||||
return div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
position('fixed'),
|
||||
bottom('lg'),
|
||||
right('lg'),
|
||||
width('60px'),
|
||||
height('60px'),
|
||||
borderRadius('circle'),
|
||||
bgColor('#4CAF50'),
|
||||
boxShadow('lg'),
|
||||
zIndex('999999'),
|
||||
display('flex'),
|
||||
alignItems('center'),
|
||||
justifyContent('center'),
|
||||
cursor('pointer'),
|
||||
fontSize('2xl'),
|
||||
color('white'),
|
||||
transition()
|
||||
),
|
||||
onclick: onClick,
|
||||
},
|
||||
[icon]
|
||||
);
|
||||
}
|
||||
32
packages/ts-plugin-sample/src/components/LoginPrompt.ts
Normal file
32
packages/ts-plugin-sample/src/components/LoginPrompt.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* LoginPrompt Component
|
||||
*
|
||||
* Displays a message prompting the user to login
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
textAlign,
|
||||
color,
|
||||
padding,
|
||||
bgColor,
|
||||
borderRadius,
|
||||
border,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
|
||||
export function LoginPrompt(): DomJson {
|
||||
return div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
textAlign('center'),
|
||||
color('gray-600'),
|
||||
padding('sm'),
|
||||
bgColor('yellow-100'),
|
||||
borderRadius('sm'),
|
||||
border('1px solid #ffeaa7')
|
||||
),
|
||||
},
|
||||
['Please login to x.com to continue']
|
||||
);
|
||||
}
|
||||
75
packages/ts-plugin-sample/src/components/OverlayHeader.ts
Normal file
75
packages/ts-plugin-sample/src/components/OverlayHeader.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* OverlayHeader Component
|
||||
*
|
||||
* Header bar with title and minimize button
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
background,
|
||||
paddingY,
|
||||
paddingX,
|
||||
display,
|
||||
justifyContent,
|
||||
alignItems,
|
||||
color,
|
||||
fontWeight,
|
||||
fontSize,
|
||||
border,
|
||||
cursor,
|
||||
padding,
|
||||
width,
|
||||
height,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
|
||||
export interface OverlayHeaderProps {
|
||||
title: string;
|
||||
onMinimize: string;
|
||||
}
|
||||
|
||||
export function OverlayHeader({ title, onMinimize }: OverlayHeaderProps): DomJson {
|
||||
return div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
background('linear-gradient(135deg, #667eea 0%, #764ba2 100%)'),
|
||||
paddingY('sm'),
|
||||
paddingX('md'),
|
||||
display('flex'),
|
||||
justifyContent('space-between'),
|
||||
alignItems('center'),
|
||||
color('white')
|
||||
),
|
||||
},
|
||||
[
|
||||
div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
fontWeight('semibold'),
|
||||
fontSize('lg')
|
||||
),
|
||||
},
|
||||
[title]
|
||||
),
|
||||
button(
|
||||
{
|
||||
style: inlineStyle(
|
||||
background('transparent'),
|
||||
border('none'),
|
||||
color('white'),
|
||||
fontSize('xl'),
|
||||
cursor('pointer'),
|
||||
padding('0'),
|
||||
width('24px'),
|
||||
height('24px'),
|
||||
display('flex'),
|
||||
alignItems('center'),
|
||||
justifyContent('center')
|
||||
),
|
||||
onclick: onMinimize,
|
||||
},
|
||||
['−']
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
85
packages/ts-plugin-sample/src/components/PluginOverlay.ts
Normal file
85
packages/ts-plugin-sample/src/components/PluginOverlay.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* PluginOverlay Component
|
||||
*
|
||||
* Main plugin UI overlay container
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
position,
|
||||
bottom,
|
||||
right,
|
||||
width,
|
||||
borderRadius,
|
||||
bgColor,
|
||||
boxShadow,
|
||||
zIndex,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
overflow,
|
||||
padding,
|
||||
defaultFontFamily,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
import { OverlayHeader } from './OverlayHeader';
|
||||
import { StatusIndicator } from './StatusIndicator';
|
||||
import { ProveButton } from './ProveButton';
|
||||
import { LoginPrompt } from './LoginPrompt';
|
||||
|
||||
export interface PluginOverlayProps {
|
||||
title: string;
|
||||
isConnected: boolean;
|
||||
isPending: boolean;
|
||||
onMinimize: string;
|
||||
onProve: string;
|
||||
}
|
||||
|
||||
export function PluginOverlay({
|
||||
title,
|
||||
isConnected,
|
||||
isPending,
|
||||
onMinimize,
|
||||
onProve,
|
||||
}: PluginOverlayProps): DomJson {
|
||||
return div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
position('fixed'),
|
||||
bottom('0'),
|
||||
right('xs'),
|
||||
width('280px'),
|
||||
borderRadius('md'),
|
||||
{ borderRadius: '8px 8px 0 0' }, // Custom override for specific corner rounding
|
||||
bgColor('white'),
|
||||
boxShadow('md'),
|
||||
zIndex('999999'),
|
||||
fontSize('sm'),
|
||||
fontFamily(defaultFontFamily),
|
||||
overflow('hidden')
|
||||
),
|
||||
},
|
||||
[
|
||||
// Header
|
||||
OverlayHeader({ title, onMinimize }),
|
||||
|
||||
// Content area
|
||||
div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
padding('lg'),
|
||||
bgColor('gray-100')
|
||||
),
|
||||
},
|
||||
[
|
||||
// Status indicator
|
||||
StatusIndicator({ isConnected }),
|
||||
|
||||
// Conditional content: button or login prompt
|
||||
isConnected
|
||||
? ProveButton({ onClick: onProve, isPending })
|
||||
: LoginPrompt(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
49
packages/ts-plugin-sample/src/components/ProveButton.ts
Normal file
49
packages/ts-plugin-sample/src/components/ProveButton.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* ProveButton Component
|
||||
*
|
||||
* Button for initiating proof generation
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
width,
|
||||
padding,
|
||||
background,
|
||||
color,
|
||||
border,
|
||||
borderRadius,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
cursor,
|
||||
transition,
|
||||
opacity,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
|
||||
export interface ProveButtonProps {
|
||||
onClick: string;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
export function ProveButton({ onClick, isPending }: ProveButtonProps): DomJson {
|
||||
return button(
|
||||
{
|
||||
style: inlineStyle(
|
||||
width('100%'),
|
||||
padding('sm'),
|
||||
background('linear-gradient(135deg, #667eea 0%, #764ba2 100%)'),
|
||||
color('white'),
|
||||
border('none'),
|
||||
borderRadius('sm'),
|
||||
fontSize('md'),
|
||||
fontWeight('semibold'),
|
||||
cursor('pointer'),
|
||||
transition(),
|
||||
isPending && opacity('0.6'),
|
||||
isPending && cursor('not-allowed')
|
||||
),
|
||||
onclick: onClick,
|
||||
},
|
||||
[isPending ? 'Generating Proof...' : 'Prove']
|
||||
);
|
||||
}
|
||||
61
packages/ts-plugin-sample/src/components/StatusIndicator.ts
Normal file
61
packages/ts-plugin-sample/src/components/StatusIndicator.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* StatusIndicator Component
|
||||
*
|
||||
* Shows connection status with visual indicator
|
||||
*/
|
||||
|
||||
import type { DomJson } from '@tlsn/plugin-sdk';
|
||||
import {
|
||||
inlineStyle,
|
||||
display,
|
||||
alignItems,
|
||||
marginBottom,
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
bgColor,
|
||||
marginRight,
|
||||
fontSize,
|
||||
color,
|
||||
} from '@tlsn/plugin-sdk/styles';
|
||||
|
||||
export interface StatusIndicatorProps {
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
export function StatusIndicator({ isConnected }: StatusIndicatorProps): DomJson {
|
||||
return div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
display('flex'),
|
||||
alignItems('center'),
|
||||
marginBottom('md')
|
||||
),
|
||||
},
|
||||
[
|
||||
// Status dot
|
||||
div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
width('8px'),
|
||||
height('8px'),
|
||||
borderRadius('circle'),
|
||||
bgColor(isConnected ? '#48bb78' : '#cbd5e0'),
|
||||
marginRight('2')
|
||||
),
|
||||
},
|
||||
[]
|
||||
),
|
||||
// Status text
|
||||
div(
|
||||
{
|
||||
style: inlineStyle(
|
||||
fontSize('sm'),
|
||||
color('gray-700')
|
||||
),
|
||||
},
|
||||
[isConnected ? 'Connected' : 'Waiting for connection...']
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
22
packages/ts-plugin-sample/src/components/index.ts
Normal file
22
packages/ts-plugin-sample/src/components/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Component exports
|
||||
*
|
||||
* Centralized export point for all UI components
|
||||
*/
|
||||
|
||||
export { FloatingButton } from './FloatingButton';
|
||||
export type { FloatingButtonProps } from './FloatingButton';
|
||||
|
||||
export { PluginOverlay } from './PluginOverlay';
|
||||
export type { PluginOverlayProps } from './PluginOverlay';
|
||||
|
||||
export { OverlayHeader } from './OverlayHeader';
|
||||
export type { OverlayHeaderProps } from './OverlayHeader';
|
||||
|
||||
export { StatusIndicator } from './StatusIndicator';
|
||||
export type { StatusIndicatorProps } from './StatusIndicator';
|
||||
|
||||
export { ProveButton } from './ProveButton';
|
||||
export type { ProveButtonProps } from './ProveButton';
|
||||
|
||||
export { LoginPrompt } from './LoginPrompt';
|
||||
24
packages/ts-plugin-sample/src/config.ts
Normal file
24
packages/ts-plugin-sample/src/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Plugin Configuration
|
||||
*
|
||||
* Defines metadata and permissions for the X Profile Prover plugin.
|
||||
*/
|
||||
|
||||
// Type imports only (stripped at compile time)
|
||||
import type { PluginConfig, RequestPermission } from '@tlsn/plugin-sdk';
|
||||
|
||||
export const config: PluginConfig = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
version: '0.1.0',
|
||||
author: 'TLSN Team',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
} satisfies RequestPermission,
|
||||
],
|
||||
urls: ['https://x.com/*'],
|
||||
};
|
||||
211
packages/ts-plugin-sample/src/index.ts
Normal file
211
packages/ts-plugin-sample/src/index.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* X Profile Prover - TypeScript Plugin Sample
|
||||
*
|
||||
* This is a TypeScript implementation of the X.com profile prover plugin.
|
||||
* It demonstrates how to write type-safe TLSN plugins using TypeScript.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// IMPORTS
|
||||
// =============================================================================
|
||||
/**
|
||||
* Import types from the plugin SDK (type-only, stripped at compile time).
|
||||
*
|
||||
* The plugin API functions (div, button, openWindow, etc.) are declared globally
|
||||
* via the SDK type declarations.
|
||||
*/
|
||||
import type { Handler, DomJson } from '@tlsn/plugin-sdk';
|
||||
import { config } from './config';
|
||||
import { FloatingButton, PluginOverlay } from './components';
|
||||
|
||||
// =============================================================================
|
||||
// HANDLER ENUMS (Inlined for standalone execution)
|
||||
// =============================================================================
|
||||
/**
|
||||
* These enum values are inlined instead of imported to create a standalone
|
||||
* JavaScript file with no external dependencies.
|
||||
*/
|
||||
enum HandlerType {
|
||||
SENT = 'SENT',
|
||||
RECV = 'RECV',
|
||||
}
|
||||
|
||||
enum HandlerPart {
|
||||
START_LINE = 'START_LINE',
|
||||
PROTOCOL = 'PROTOCOL',
|
||||
METHOD = 'METHOD',
|
||||
REQUEST_TARGET = 'REQUEST_TARGET',
|
||||
STATUS_CODE = 'STATUS_CODE',
|
||||
HEADERS = 'HEADERS',
|
||||
BODY = 'BODY',
|
||||
ALL = 'ALL',
|
||||
}
|
||||
|
||||
enum HandlerAction {
|
||||
REVEAL = 'REVEAL',
|
||||
PEDERSEN = 'PEDERSEN',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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(): Promise<void> {
|
||||
const isRequestPending = useState<boolean>('isRequestPending', false);
|
||||
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
// Step 1: Get the intercepted header from the X.com API request
|
||||
const [header] = useHeaders((headers) => {
|
||||
return headers.filter((header) =>
|
||||
header.url.includes('https://api.x.com/1.1/account/settings.json')
|
||||
);
|
||||
});
|
||||
|
||||
if (!header) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Extract authentication headers from the intercepted request
|
||||
const headers: Record<string, string | undefined> = {
|
||||
cookie: header.requestHeaders.find((h) => h.name === 'Cookie')?.value,
|
||||
'x-csrf-token': header.requestHeaders.find((h) => h.name === 'x-csrf-token')?.value,
|
||||
'x-client-transaction-id': header.requestHeaders.find(
|
||||
(h) => h.name === 'x-client-transaction-id'
|
||||
)?.value,
|
||||
Host: 'api.x.com',
|
||||
authorization: header.requestHeaders.find((h) => h.name === 'authorization')?.value,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
// Step 3: Generate TLS proof using the unified prove() API
|
||||
const resp = await prove(
|
||||
// Request options
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json',
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
// Prover options
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
|
||||
maxRecvData: 4000,
|
||||
maxSentData: 2000,
|
||||
handlers: [
|
||||
// Reveal the request start line
|
||||
{
|
||||
type: HandlerType.SENT,
|
||||
part: HandlerPart.START_LINE,
|
||||
action: HandlerAction.REVEAL,
|
||||
} satisfies Handler,
|
||||
// Reveal the response start line
|
||||
{
|
||||
type: HandlerType.RECV,
|
||||
part: HandlerPart.START_LINE,
|
||||
action: HandlerAction.REVEAL,
|
||||
} satisfies Handler,
|
||||
// Reveal the 'date' header from the response
|
||||
{
|
||||
type: HandlerType.RECV,
|
||||
part: HandlerPart.HEADERS,
|
||||
action: HandlerAction.REVEAL,
|
||||
params: {
|
||||
key: 'date',
|
||||
},
|
||||
} satisfies Handler,
|
||||
// Reveal the 'screen_name' field from the JSON response body
|
||||
{
|
||||
type: HandlerType.RECV,
|
||||
part: HandlerPart.BODY,
|
||||
action: HandlerAction.REVEAL,
|
||||
params: {
|
||||
type: 'json' as const,
|
||||
path: 'screen_name',
|
||||
},
|
||||
} satisfies Handler,
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the minimized UI to show full plugin interface
|
||||
*/
|
||||
function expandUI(): void {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimize the UI to a floating action button
|
||||
*/
|
||||
function minimizeUI(): void {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN UI FUNCTION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The main() function is called reactively whenever plugin state changes.
|
||||
* It returns a DOM structure that is rendered as the plugin UI.
|
||||
*/
|
||||
function main(): DomJson {
|
||||
// Subscribe to intercepted headers for the X.com API endpoint
|
||||
const [header] = useHeaders((headers) =>
|
||||
headers.filter((header) => header.url.includes('https://api.x.com/1.1/account/settings.json'))
|
||||
);
|
||||
|
||||
const isMinimized = useState<boolean>('isMinimized', false);
|
||||
const isRequestPending = useState<boolean>('isRequestPending', false);
|
||||
|
||||
// Run once on plugin load: Open X.com in a new window
|
||||
useEffect(() => {
|
||||
openWindow('https://x.com');
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return FloatingButton({ onClick: 'expandUI' });
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
return PluginOverlay({
|
||||
title: 'X Profile Prover',
|
||||
isConnected: !!header,
|
||||
isPending: isRequestPending,
|
||||
onMinimize: 'minimizeUI',
|
||||
onProve: 'onClick',
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PLUGIN EXPORTS
|
||||
// =============================================================================
|
||||
/**
|
||||
* All plugins must export an object with these properties:
|
||||
* - main: The reactive UI rendering function
|
||||
* - onClick: Click handler callback for buttons
|
||||
* - config: Plugin metadata
|
||||
*
|
||||
* Additional exported functions (expandUI, minimizeUI) are also available
|
||||
* as click handlers referenced by the 'onclick' property in DOM elements.
|
||||
*/
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
40
packages/ts-plugin-sample/tsconfig.json
Normal file
40
packages/ts-plugin-sample/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Language and Environment */
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Emit */
|
||||
"outDir": "./build",
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
|
||||
/* Interop Constraints */
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Completeness */
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../plugin-sdk/src/globals.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
||||
Reference in New Issue
Block a user