Compare commits

...

12 Commits

Author SHA1 Message Date
tsukino
2bfc197316 fix: add backward compatibility exports for extension imports
- Add /src and /src/types paths to package.json exports
- Extension imports from @tlsn/plugin-sdk/src/types now work correctly
- Fixes 'npm run dev' in packages/extension
2026-01-14 17:15:39 +08:00
tsukino
78cec7651b feat: enable tree-shaking with separate styles entry point
- Add package.json exports field with @tlsn/plugin-sdk/styles entry point
- Configure Vite to build separate bundles for index and styles
- Remove styling exports from main SDK index (now only in /styles)
- Update all components to import from @tlsn/plugin-sdk/styles
- Update ts-plugin-sample tsconfig to use bundler module resolution
- Eliminate QuickJS imports from plugin build output

Benefits:
- Plugin bundle size reduced from 17.4kb to 12.7kb (-27%)
- SDK main bundle reduced from 51.74kb to 41.10kb (-21%)
- Styles bundle is only 7.71kb
- Proper tree-shaking prevents importing unused Host/QuickJS code
2026-01-14 17:07:27 +08:00
tsukino
40bf2cec82 feat: add comprehensive non-opinionated color palette
- Add full color scales (100-900) for gray, blue, purple, red, yellow, orange, green
- Add white and black as base colors
- Remove opinionated 'primary' color scheme
- Update OverlayHeader to use literal gradient instead of primary token
- Remove legacy 'colors' export from plugin-sdk
- Color system is now completely non-opinionated and Tailwind-compatible
2026-01-14 17:01:33 +08:00
tsukino
d6d80cf277 feat: move styling system to plugin-sdk package
- Move styles.ts from ts-plugin-sample to plugin-sdk for reusability
- Export all styling utilities from plugin-sdk package
- Update all components to import styling functions from @tlsn/plugin-sdk
- Add external dependencies to esbuild config to avoid bundling SDK runtime
- Styling functions are now available to all plugins via the SDK
2026-01-14 16:55:09 +08:00
tsukino
449d2843ea feat: implement Tailwind-like styling system with composable helpers
- Add inlineStyle() function that merges multiple style helpers and filters falsey values
- Create comprehensive style helper functions (color, padding, margin, display, etc.)
- Add design token system with semantic color, spacing, and typography tokens
- Support conditional styling with falsey value filtering (e.g., isPending && opacity('0.6'))
- Update all components to use new composable styling API
- Replace object-based styles with function-based helpers for better readability
2026-01-14 16:51:37 +08:00
tsukino
8d93ff679e refactor: organize ts-plugin-sample UI into reusable components
- Create centralized design system in src/styles.ts with colors, spacing, typography
- Extract UI into component files: FloatingButton, PluginOverlay, OverlayHeader, StatusIndicator, ProveButton, LoginPrompt
- Simplify main plugin file by composing components instead of inline DOM construction
- Add TypeScript interfaces for component props
- Improve code maintainability and readability
2026-01-14 16:39:04 +08:00
tsukino
2c1fc6daad chore: simplify build-wrapper by removing unnecessary transformation
esbuild already outputs the desired export format
2026-01-14 16:26:28 +08:00
tsukino
1cdf314e8d feat: add TypeScript plugin support with sample implementation
- Export plugin API types from SDK (DivFunction, ButtonFunction, etc.)
- Enhanced globals.d.ts with all plugin API function types
- Created ts-plugin-sample package with TypeScript implementation
- Build outputs single file with clean export default statement
- Inlined handler enums for standalone execution
- No external imports in build output
2026-01-14 16:24:25 +08:00
tsukino
d52c7eb43f fix: webpack-dev-server config and remove unused storage permission (#217)
- Update webpack-dev-server config to use server: 'http' instead of deprecated https property
- Remove unused 'storage' permission from manifest (extension only uses IndexedDB)
- Bump version to 0.1.0.1300
2026-01-14 15:20:59 +08:00
Hendrik Eeckhaut
7fc1af8501 React demo (#216)
* React demo

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

- Convert demo from vanilla HTML/JS to React + TypeScript + Vite
- Add modern UI with collapsible sections, status bar, and plugin cards
- Convert plugins (twitter, swissbank, spotify) to TypeScript
- Add plugin build script with environment variable injection at build time
- Create globals.d.ts in plugin-sdk for plugin runtime type definitions

* lint

* fix test

* Improved status bar

* minor improvements

* Improved demo flow

* rebase
2026-01-13 15:19:45 +08:00
tsukino
eb18485361 Fix parser to return range for entire array when accessing array field (#212)
When calling parser.ranges.body('a', {type: 'json'}) for a body like
{a: [{b: 1}, {c: 2}]}, the parser now returns the range for the entire
array value instead of returning empty.

Changes:
- Store array field with full range (key + value) before processing elements
- hideKey option returns just the array value: [{b: 1}, {c: 2}]
- hideValue option returns just the key: "a"

Added tests for:
- Extracting entire array value directly
- hideKey/hideValue options for array fields
- Array of primitives
- Nested array fields (e.g., data.items)
2026-01-06 19:16:14 +08:00
tsukino
5a3a844527 feat: add plugin permission control system (#211)
* Add plugin permission control system

- Add RequestPermission interface to plugin-sdk types
- Extend PluginConfig with requests[] and urls[] permission arrays
- Create permissionValidator.ts with validation functions:
  - validateProvePermission() for prove() calls
  - validateOpenWindowPermission() for openWindow() calls
  - deriveProxyUrl() for automatic proxy URL derivation
  - matchesPathnamePattern() using URLPattern API
- Update SessionManager to validate permissions before execution
- Update ConfirmationManager to pass permissions to popup
- Update ConfirmPopup UI to display network and URL permissions
- Add URLPattern type declaration to global.d.ts
- Update demo plugins (twitter.js, swissbank.js) with permissions
- Add comprehensive tests for permission validation
- Add Chrome Web Store listing documentation

* Fix lint

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2026-01-06 19:16:01 +08:00
74 changed files with 7839 additions and 2175 deletions

3083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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

View File

@@ -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
```

View File

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

View File

@@ -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>

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

285
packages/demo/src/App.tsx Normal file
View 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>
);
}

View 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>
);
}

View File

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

View File

@@ -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>
);
}

View 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>
);
}

View 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>
);
}

View 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 >
);
}

View 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>
</>
);
}

View 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>
);
}

View File

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

View File

@@ -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>
);

View 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;
},
},
};

View 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,
};

View 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,
};

View 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,
};

View 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 { };

View 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();
}

View File

@@ -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';

View 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"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

View File

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

View File

@@ -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/*',
],
};
// =============================================================================

View 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,
},
});

View File

@@ -21,6 +21,9 @@
"browser": true,
"node": true
},
"globals": {
"URLPattern": "readonly"
},
"settings": {
"import/resolver": "typescript"
},

View 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.

View File

@@ -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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -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()}`;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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) => {

View 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')}`,
);
}
}

View 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();
});
});

View File

@@ -27,7 +27,7 @@ var compiler = webpack(config);
var server = new WebpackDevServer(
{
https: false,
server: 'http',
hot: true,
liveReload: false,
client: {

View File

@@ -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"
}
}

View File

@@ -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
View 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 {};

View File

@@ -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';

View File

@@ -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)', () => {

View File

@@ -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,

View 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';

View File

@@ -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[];
}

View File

@@ -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
View 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

View 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

View 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');

View 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"
}
}

View 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]
);
}

View 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']
);
}

View 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,
},
['']
),
]
);
}

View 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(),
]
),
]
);
}

View 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']
);
}

View 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...']
),
]
);
}

View 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';

View 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/*'],
};

View 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,
};

View 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"]
}