mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-29 08:58:01 -05:00
Compare commits
19 Commits
feat/fine-
...
plugins_cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f309d9226d | ||
|
|
764de211f8 | ||
|
|
d3aeaf227d | ||
|
|
163a1bd4b9 | ||
|
|
8e17101d11 | ||
|
|
fddba16cf8 | ||
|
|
52cc68937b | ||
|
|
a04b3c671a | ||
|
|
2b72884192 | ||
|
|
9b22e2af37 | ||
|
|
bbe6e23d5f | ||
|
|
af9148ee4a | ||
|
|
6331f03940 | ||
|
|
3b4554cd2a | ||
|
|
367479fd77 | ||
|
|
d42db516c7 | ||
|
|
1b776929d8 | ||
|
|
d52c7eb43f | ||
|
|
7fc1af8501 |
24
.github/workflows/demo.yml
vendored
24
.github/workflows/demo.yml
vendored
@@ -20,9 +20,26 @@ env:
|
||||
should_publish: ${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.')) || github.ref == 'refs/heads/staging' }}
|
||||
|
||||
jobs:
|
||||
test_verifier:
|
||||
name: test verifier server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Run verifier tests
|
||||
working-directory: packages/verifier
|
||||
run: cargo test
|
||||
|
||||
build_and_publish_demo_verifier_server:
|
||||
name: build and publish demo verifier server image
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_verifier
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -52,6 +69,8 @@ jobs:
|
||||
push: ${{ env.should_publish == 'true' }}
|
||||
tags: ${{ steps.meta-prover-server.outputs.tags }}
|
||||
labels: ${{ steps.meta-prover-server.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_HASH=${{ github.sha }}
|
||||
|
||||
build_and_publish_demo_frontend:
|
||||
name: build and publish demo frontend image
|
||||
@@ -86,5 +105,6 @@ jobs:
|
||||
tags: ${{ steps.meta-verifier-webapp.outputs.tags }}
|
||||
labels: ${{ steps.meta-verifier-webapp.outputs.labels }}
|
||||
build-args: |
|
||||
VERIFIER_HOST=demo-staging.tlsnotary.org
|
||||
SSL=true
|
||||
VITE_VERIFIER_HOST=demo.tlsnotary.org
|
||||
VITE_SSL=true
|
||||
GIT_HASH=${{ github.sha }}
|
||||
|
||||
27
CLAUDE.md
27
CLAUDE.md
@@ -591,37 +591,32 @@ logger.setLevel(LogLevel.WARN);
|
||||
Docker-based demo environment for testing plugins:
|
||||
|
||||
**Files:**
|
||||
- `twitter.js`, `swissbank.js` - Example plugin files
|
||||
- `src/plugins/*.plugin.ts` - Plugin source files (TypeScript)
|
||||
- `public/plugins/*.js` - Built plugin files (generated by `build-plugins.js`)
|
||||
- `docker-compose.yml` - Docker services configuration
|
||||
- `nginx.conf` - Reverse proxy configuration
|
||||
- `start.sh` - Setup script with URL templating
|
||||
|
||||
**Docker Services:**
|
||||
1. `verifier` - TLSNotary verifier server (port 7047)
|
||||
2. `demo-static` - nginx serving static plugin files
|
||||
3. `nginx` - Reverse proxy (port 80)
|
||||
|
||||
**Environment Variables:**
|
||||
- `VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
|
||||
- `SSL` - Use https/wss protocols (default: `false`)
|
||||
**Environment Variables (via `.env` files or Docker build args):**
|
||||
- `VITE_VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
|
||||
- `VITE_SSL` - Use https/wss protocols (default: `false`)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Local development
|
||||
./start.sh
|
||||
# Local development with npm
|
||||
npm run demo
|
||||
|
||||
# Production with SSL
|
||||
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./start.sh
|
||||
# Docker (detached mode)
|
||||
npm run docker:up
|
||||
|
||||
# Docker detached mode
|
||||
./start.sh -d
|
||||
# Docker with custom verifier
|
||||
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
|
||||
```
|
||||
|
||||
The `start.sh` script:
|
||||
1. Processes plugin files, replacing `verifierUrl` and `proxyUrl` placeholders
|
||||
2. Copies processed files to `generated/` directory
|
||||
3. Starts Docker Compose services
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
### Plugin API Changes
|
||||
|
||||
30
README.md
30
README.md
@@ -58,8 +58,7 @@ tlsn-extension/
|
||||
│ │
|
||||
│ ├── demo/ # Demo server with Docker setup
|
||||
│ │ ├── *.js # Example plugin files
|
||||
│ │ ├── docker-compose.yml # Docker services configuration
|
||||
│ │ └── start.sh # Setup script with configurable URLs
|
||||
│ │ └── docker-compose.yml # Docker services configuration
|
||||
│ │
|
||||
│ ├── tutorial/ # Tutorial examples
|
||||
│ │ └── *.js # Tutorial plugin files
|
||||
@@ -116,10 +115,9 @@ Rust-based HTTP/WebSocket server for TLSNotary verification:
|
||||
#### 5. **demo** - Demo Server
|
||||
Docker-based demo environment with:
|
||||
- Pre-configured example plugins (Twitter, SwissBank)
|
||||
- React + Vite frontend with environment-based configuration
|
||||
- Docker Compose setup with verifier and nginx
|
||||
- Configurable verifier URLs via environment variables
|
||||
- Plugin file generator (`generate.sh`) with SSL support
|
||||
- Docker startup script (`start.sh`)
|
||||
- Configurable verifier URLs via `.env` files or Docker build args
|
||||
|
||||
#### 6. **tlsn-wasm-pkg** - TLSN WebAssembly Package
|
||||
Pre-built WebAssembly binaries for TLSNotary functionality in the browser.
|
||||
@@ -495,25 +493,19 @@ npm run demo
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure the demo for different environments:
|
||||
The demo uses `.env` files for configuration:
|
||||
- `.env` - Local development defaults (`localhost:7047`)
|
||||
- `.env.production` - Production settings (`verifier.tlsnotary.org`, SSL enabled)
|
||||
|
||||
For Docker deployments, override via environment variables:
|
||||
```bash
|
||||
# Local development (default)
|
||||
cd packages/demo
|
||||
./generate.sh && ./start.sh
|
||||
npm run docker:up
|
||||
|
||||
# Production with SSL
|
||||
cd packages/demo
|
||||
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
|
||||
./start.sh
|
||||
|
||||
# Docker detached mode
|
||||
./generate.sh && ./start.sh -d
|
||||
# Production with custom verifier
|
||||
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
|
||||
```
|
||||
|
||||
The demo uses two scripts:
|
||||
- **`generate.sh`** - Generates plugin files with configured verifier URLs (use environment variables here)
|
||||
- **`start.sh`** - Starts Docker Compose services (assumes `generated/` directory exists)
|
||||
|
||||
### Tutorial
|
||||
|
||||
```bash
|
||||
|
||||
2615
package-lock.json
generated
2615
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-monorepo",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.14",
|
||||
"private": true,
|
||||
"description": "TLSN Extension monorepo with plugin SDK",
|
||||
"license": "MIT",
|
||||
@@ -22,11 +22,11 @@
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"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",
|
||||
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.14 --no-logging",
|
||||
"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"
|
||||
"docker:up": "cd packages/demo && docker compose up --build -d",
|
||||
"docker:down": "cd packages/demo && docker compose down"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4",
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^1.0.0"
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"happy-dom": "20.0.11",
|
||||
"vite": "7.3.0",
|
||||
"webpack-dev-server": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { Logger, LogLevel, DEFAULT_LOG_LEVEL } from './index';
|
||||
|
||||
describe('Logger', () => {
|
||||
@@ -11,6 +11,10 @@ describe('Logger', () => {
|
||||
logger.init(DEFAULT_LOG_LEVEL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('LogLevel', () => {
|
||||
it('should have correct hierarchy values', () => {
|
||||
expect(LogLevel.DEBUG).toBe(0);
|
||||
|
||||
3
packages/demo/.env
Normal file
3
packages/demo/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
# Verifier Configuration
|
||||
VITE_VERIFIER_HOST=localhost:7047
|
||||
VITE_SSL=false
|
||||
3
packages/demo/.env.production
Normal file
3
packages/demo/.env.production
Normal file
@@ -0,0 +1,3 @@
|
||||
# Production environment variables
|
||||
VITE_VERIFIER_HOST=verifier.tlsnotary.org
|
||||
VITE_SSL=true
|
||||
2
packages/demo/.gitignore
vendored
2
packages/demo/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
*.wasm
|
||||
dist/
|
||||
public/plugins/
|
||||
generated/
|
||||
52
packages/demo/ADDING_PLUGINS.md
Normal file
52
packages/demo/ADDING_PLUGINS.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Adding New Plugins
|
||||
|
||||
Adding new plugins to the demo is straightforward. Just update the `plugins.ts` file:
|
||||
|
||||
## Example: Adding a GitHub Plugin
|
||||
|
||||
```typescript
|
||||
// packages/demo/src/plugins.ts
|
||||
|
||||
export const plugins: Record<string, Plugin> = {
|
||||
// ... existing plugins ...
|
||||
|
||||
github: {
|
||||
name: 'GitHub Profile',
|
||||
description: 'Prove your GitHub contributions and profile information',
|
||||
logo: '🐙', // or use emoji: '💻', '⚡', etc.
|
||||
file: '/github.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Plugin Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------------- | -------- | ------------------------------------------------------- |
|
||||
| `name` | string | Display name shown in the card header |
|
||||
| `description` | string | Brief description of what the plugin proves |
|
||||
| `logo` | string | Emoji or character to display as the plugin icon |
|
||||
| `file` | string | Path to the plugin JavaScript file |
|
||||
| `parseResult` | function | Function to extract the result from the plugin response |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Logo**: Use emojis for visual appeal (🔒, 🎮, 📧, 💰, etc.)
|
||||
- **Description**: Keep it concise (1-2 lines) explaining what data is proven
|
||||
- **File**: Place the plugin JS file in `/packages/demo/` directory
|
||||
- **Name**: Use short, recognizable names
|
||||
|
||||
## Card Display
|
||||
|
||||
The plugin will automatically render as a card with:
|
||||
- Large logo at the top
|
||||
- Plugin name as heading
|
||||
- Description text below
|
||||
- "Run Plugin" button at the bottom
|
||||
- Hover effects and animations
|
||||
- Running state with spinner
|
||||
|
||||
No additional UI code needed!
|
||||
@@ -1,17 +1,27 @@
|
||||
# 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_HOST=localhost:7047
|
||||
ARG VITE_SSL=false
|
||||
ARG GIT_HASH=local
|
||||
|
||||
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_HOST=${VITE_VERIFIER_HOST}
|
||||
ENV VITE_SSL=${VITE_SSL}
|
||||
ENV GIT_HASH=${GIT_HASH}
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/generated /usr/share/nginx/html
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
@@ -57,31 +57,44 @@ You can use the websocketproxy hosted by the TLSNotary team, or run your own pro
|
||||
|
||||
## 4. Launch the demo
|
||||
|
||||
Run the demo with `npm run demo` from the repository root, or run it with docker using `npm run docker:up`.
|
||||
### Development with React
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you want to run the scripts manually:
|
||||
This demo is built with React + TypeScript + Vite. To run it locally:
|
||||
|
||||
```bash
|
||||
cd packages/demo
|
||||
./generate.sh && ./start.sh
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The demo uses two scripts:
|
||||
- **`generate.sh`** - Generates plugin files with configured verifier URLs
|
||||
- **`start.sh`** - Starts Docker Compose services
|
||||
The demo will open at `http://localhost:3000` in your browser with the TLSNotary extension.
|
||||
|
||||
### Environment Variables
|
||||
### Docker Setup
|
||||
|
||||
Configure for different environments:
|
||||
Run the demo with `npm run demo` from the repository root, or run it with docker using `npm run docker:up`.
|
||||
|
||||
#### Manual Docker Setup
|
||||
|
||||
If you want to run Docker manually:
|
||||
|
||||
```bash
|
||||
cd packages/demo
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
The demo uses `.env` files for configuration:
|
||||
- `.env` - Local development defaults (`localhost:7047`)
|
||||
- `.env.production` - Production settings (`verifier.tlsnotary.org`, SSL enabled)
|
||||
|
||||
For Docker deployments, override via environment variables:
|
||||
```bash
|
||||
# Local development (default)
|
||||
./generate.sh && ./start.sh
|
||||
docker compose up --build
|
||||
|
||||
# Production with SSL
|
||||
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
|
||||
./start.sh
|
||||
# Production with custom verifier
|
||||
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
|
||||
```
|
||||
|
||||
You can now open the demo by opening http://localhost:8080 in your browser with the TLSNotary extension
|
||||
46
packages/demo/build-plugins.js
Normal file
46
packages/demo/build-plugins.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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', 'duolingo'];
|
||||
|
||||
// Build URLs from environment variables (matching config.ts pattern)
|
||||
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';
|
||||
const SSL = process.env.VITE_SSL === 'true';
|
||||
|
||||
const VERIFIER_URL = `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`;
|
||||
const PROXY_URL = `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=`;
|
||||
|
||||
// Build each plugin separately as plain ES module
|
||||
for (const plugin of plugins) {
|
||||
await build({
|
||||
configFile: false,
|
||||
publicDir: false, // Don't copy public assets into plugin output
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, `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(VERIFIER_URL),
|
||||
VITE_PROXY_URL: JSON.stringify(PROXY_URL),
|
||||
},
|
||||
});
|
||||
console.log(`✓ Built ${plugin}.js`);
|
||||
}
|
||||
|
||||
console.log('✓ All plugins built successfully');
|
||||
@@ -9,14 +9,16 @@ services:
|
||||
- "7047:7047"
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- GIT_HASH=${GIT_HASH:-dev}
|
||||
restart: unless-stopped
|
||||
|
||||
demo-static:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
VERIFIER_HOST: ${VERIFIER_HOST:-localhost:7047}
|
||||
SSL: ${SSL:-false}
|
||||
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
|
||||
VITE_SSL: ${VITE_SSL:-false}
|
||||
GIT_HASH: ${GIT_HASH:-dev}
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Demo Plugin File Generator
|
||||
#
|
||||
# This script generates plugin files with configurable verifier URLs.
|
||||
# Used both locally and in CI/CD pipelines.
|
||||
#
|
||||
# Environment Variables:
|
||||
# VERIFIER_HOST - Verifier server host (default: localhost:7047)
|
||||
# SSL - Use https/wss if true (default: false)
|
||||
#
|
||||
# Usage:
|
||||
# ./generate.sh # Local development
|
||||
# VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh # Production
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Configuration with defaults
|
||||
VERIFIER_HOST="${VERIFIER_HOST:-localhost:7047}"
|
||||
SSL="${SSL:-false}"
|
||||
|
||||
# Determine protocol based on SSL setting
|
||||
if [ "$SSL" = "true" ]; then
|
||||
HTTP_PROTOCOL="https"
|
||||
WS_PROTOCOL="wss"
|
||||
else
|
||||
HTTP_PROTOCOL="http"
|
||||
WS_PROTOCOL="ws"
|
||||
fi
|
||||
|
||||
VERIFIER_URL="${HTTP_PROTOCOL}://${VERIFIER_HOST}"
|
||||
PROXY_URL_BASE="${WS_PROTOCOL}://${VERIFIER_HOST}/proxy?token="
|
||||
|
||||
echo "========================================"
|
||||
echo "TLSNotary Demo Plugin Generator"
|
||||
echo "========================================"
|
||||
echo "Verifier Host: $VERIFIER_HOST"
|
||||
echo "SSL Enabled: $SSL"
|
||||
echo "Verifier URL: $VERIFIER_URL"
|
||||
echo "Proxy URL: ${PROXY_URL_BASE}<host>"
|
||||
echo "========================================"
|
||||
|
||||
# Create generated directory for processed files
|
||||
mkdir -p generated
|
||||
|
||||
# Function to process a plugin file
|
||||
process_plugin() {
|
||||
local input_file="$1"
|
||||
local output_file="generated/$(basename "$input_file")"
|
||||
|
||||
echo "Processing: $input_file -> $output_file"
|
||||
|
||||
# Replace verifierUrl and proxyUrl patterns
|
||||
sed -E \
|
||||
-e "s|verifierUrl: '[^']*'|verifierUrl: '${VERIFIER_URL}'|g" \
|
||||
-e "s|verifierUrl: \"[^\"]*\"|verifierUrl: \"${VERIFIER_URL}\"|g" \
|
||||
-e "s|proxyUrl: 'ws://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
|
||||
-e "s|proxyUrl: 'wss://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
|
||||
-e "s|proxyUrl: \"ws://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
|
||||
-e "s|proxyUrl: \"wss://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
|
||||
"$input_file" > "$output_file"
|
||||
}
|
||||
|
||||
# Function to process index.html
|
||||
process_index_html() {
|
||||
local input_file="$1"
|
||||
local output_file="generated/$(basename "$input_file")"
|
||||
|
||||
echo "Processing: $input_file -> $output_file"
|
||||
|
||||
# Replace hardcoded health check URL with configured verifier URL
|
||||
sed -E \
|
||||
-e "s|http://localhost:7047/health|${VERIFIER_URL}/health|g" \
|
||||
"$input_file" > "$output_file"
|
||||
}
|
||||
|
||||
# Process index.html
|
||||
echo ""
|
||||
echo "Processing index.html..."
|
||||
process_index_html "index.html"
|
||||
|
||||
# Copy other static files
|
||||
echo ""
|
||||
echo "Copying other static files..."
|
||||
cp favicon.ico generated/ 2>/dev/null || true
|
||||
|
||||
# Process plugin files
|
||||
echo ""
|
||||
echo "Processing plugin files..."
|
||||
for plugin_file in *.js; do
|
||||
if [ -f "$plugin_file" ]; then
|
||||
process_plugin "$plugin_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Generated files:"
|
||||
ls -la generated/
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Generation complete!"
|
||||
echo "========================================"
|
||||
@@ -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>
|
||||
@@ -39,6 +39,12 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /info {
|
||||
proxy_pass http://verifier:7047;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Default: proxy to static demo server
|
||||
location / {
|
||||
proxy_pass http://demo-static:80;
|
||||
|
||||
26
packages/demo/package.json
Normal file
26
packages/demo/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@tlsnotary/demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run build:plugins && vite",
|
||||
"build": "npm run build:plugins && vite build",
|
||||
"build:plugins": "node build-plugins.js",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"happy-dom": "20.0.11",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"webpack-dev-server": "5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +1,69 @@
|
||||
/// <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 = 'www.duolingo.com';
|
||||
const ui = 'https://www.duolingo.com/';
|
||||
|
||||
const config = {
|
||||
name: 'Swiss Bank Prover',
|
||||
description: 'This plugin will prove your Swiss Bank account balance.',
|
||||
name: 'Duolingo Plugin',
|
||||
description: 'This plugin will prove your email and current streak on Duolingo.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'swissbank.tlsnotary.org',
|
||||
pathname: '/balances',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
host: 'www.duolingo.com',
|
||||
pathname: '/2023-05-23/users/*',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://swissbank.tlsnotary.org/*',
|
||||
'https://www.duolingo.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
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 => {
|
||||
console.log('Intercepted headers:', headers);
|
||||
return headers.filter(header => header.url.includes(`https://${host}`));
|
||||
});
|
||||
|
||||
// Use cached values from state
|
||||
const authorization = useState('authorization', null);
|
||||
const user_id = useState('user_id', null);
|
||||
|
||||
if (!authorization || !user_id) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
Host: host,
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{
|
||||
url: url,
|
||||
url: `https://${api}/2023-05-23/users/${user_id}?fields=longestStreak,username`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=swissbank.tlsnotary.org',
|
||||
// proxyUrl: 'ws://localhost:55688',
|
||||
maxRecvData: 460, // Maximum bytes to receive from server (response size limit)
|
||||
maxSentData: 180,// Maximum bytes to send to server (request size limit)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 2400,
|
||||
maxSentData: 1200,
|
||||
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' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:\s*"[^"]+"' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"275_000_000"' }, },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'longestStreak', }, },
|
||||
]
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
@@ -82,23 +74,39 @@ function expandUI() {
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(
|
||||
headers => headers
|
||||
.filter(header => header.url.includes(`https://${host}${ui_path}`))
|
||||
);
|
||||
|
||||
|
||||
const hasNecessaryHeader = header?.requestHeaders.some(h => h.name === 'Cookie');
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const authorization = useState('authorization', null);
|
||||
const user_id = useState('user_id', null);
|
||||
|
||||
// Only search for auth values if not already cached
|
||||
if (!authorization || !user_id) {
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}/2023-05-23/users`));
|
||||
});
|
||||
|
||||
const authValue = header?.requestHeaders.find(h => h.name === 'Authorization')?.value;
|
||||
const traceId = header?.requestHeaders.find(h => h.name === 'X-Amzn-Trace-Id')?.value;
|
||||
const userIdValue = traceId?.split('=')[1];
|
||||
|
||||
if (authValue && !authorization) {
|
||||
setState('authorization', authValue);
|
||||
console.log('Authorization found:', authValue);
|
||||
}
|
||||
if (userIdValue && !user_id) {
|
||||
setState('user_id', userIdValue);
|
||||
console.log('User ID found:', userIdValue);
|
||||
}
|
||||
}
|
||||
|
||||
const header_has_necessary_values = authorization && user_id;
|
||||
|
||||
// Run once on plugin load
|
||||
useEffect(() => {
|
||||
openWindow(`https://${host}${ui_path}`);
|
||||
openWindow(ui);
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
@@ -108,7 +116,7 @@ function main() {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#4CAF50',
|
||||
backgroundColor: '#58CC02',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
@@ -120,11 +128,9 @@ function main() {
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}, ['🦉']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
@@ -140,10 +146,9 @@ function main() {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -156,7 +161,7 @@ function main() {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['Swiss Bank Prover']),
|
||||
}, ['Duolingo Streak']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
@@ -175,51 +180,45 @@ function main() {
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether cookie is detected
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: header_has_necessary_values ? '#d4edda' : '#f8d7da',
|
||||
color: header_has_necessary_values ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header_has_necessary_values ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected'
|
||||
header_has_necessary_values ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
hasNecessaryHeader ? (
|
||||
// Show prove button when not pending
|
||||
header_has_necessary_values ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
@@ -229,7 +228,7 @@ function main() {
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to continue'])
|
||||
}, ['Please login to Duolingo to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -1,12 +1,30 @@
|
||||
const config = {
|
||||
name: 'Spotify Top Artist',
|
||||
description: 'This plugin will prove your top artist on Spotify.',
|
||||
};
|
||||
/// <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);
|
||||
@@ -15,29 +33,30 @@ async function onClick() {
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}`));
|
||||
});
|
||||
// Use cached authorization token from state
|
||||
const authToken = useState('authToken', null);
|
||||
|
||||
// console.log('Intercepted Spotify API request header:', header);
|
||||
if (!authToken) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
|
||||
authorization: authToken,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
url: `https://${api}${top_artist_path}`, // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
url: `https://${api}${top_artist_path}`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.spotify.com',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 2400,
|
||||
maxSentData: 600,
|
||||
handlers: [
|
||||
@@ -48,7 +67,6 @@ async function onClick() {
|
||||
},
|
||||
{
|
||||
type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'items[0].name', },
|
||||
// type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'items[0].external_urls.spotify', },
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -65,11 +83,23 @@ function minimizeUI() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
|
||||
// const [header] = useHeaders(headers => { return headers.filter(headers => headers.url.includes('https://api.spotify.com')) });
|
||||
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const authToken = useState('authToken', null);
|
||||
|
||||
|
||||
// Only search for auth token if not already cached
|
||||
if (!authToken) {
|
||||
const token = useHeaders(h => h.filter(x => x.url.startsWith(`https://${api}`)))
|
||||
.flatMap(h => h.requestHeaders)
|
||||
.find((h: { name: string; value?: string }) => h.name === 'Authorization')
|
||||
?.value;
|
||||
|
||||
if (token) {
|
||||
setState('authToken', token);
|
||||
console.log('Auth Token found:', token);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
openWindow(ui);
|
||||
@@ -159,18 +189,16 @@ function main() {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: authToken ? '#d4edda' : '#f8d7da',
|
||||
color: authToken ? '#155724' : '#721c24',
|
||||
border: `1px solid ${authToken ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
authToken ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
authToken ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
@@ -181,16 +209,14 @@ function main() {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
opacity: isRequestPending ? '0.5' : '1',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
272
packages/demo/plugins/swissbank.plugin.ts
Normal file
272
packages/demo/plugins/swissbank.plugin.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/// <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);
|
||||
|
||||
// Use cached cookie from state
|
||||
const cachedCookie = useState('cookie', null);
|
||||
|
||||
if (!cachedCookie) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'cookie': cachedCookie,
|
||||
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 isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const cachedCookie = useState('cookie', null);
|
||||
|
||||
// Only search for cookie if not already cached
|
||||
if (!cachedCookie) {
|
||||
const [header] = useHeaders((headers: any[]) =>
|
||||
headers.filter(h => h.url.includes(`https://${host}`))
|
||||
);
|
||||
|
||||
if (header) {
|
||||
const cookie = header.requestHeaders.find((h: any) => h.name === 'Cookie')?.value;
|
||||
if (cookie) {
|
||||
setState('cookie', cookie);
|
||||
console.log('Cookie found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: cachedCookie ? '#d4edda' : '#f8d7da',
|
||||
color: cachedCookie ? '#155724' : '#721c24',
|
||||
border: `1px solid ${cachedCookie ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
},
|
||||
[cachedCookie ? '✓ Cookie detected' : '⚠ No Cookie detected']
|
||||
),
|
||||
cachedCookie
|
||||
? 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,
|
||||
};
|
||||
319
packages/demo/plugins/twitter.plugin.ts
Normal file
319
packages/demo/plugins/twitter.plugin.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/// <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);
|
||||
|
||||
// Use cached values from state
|
||||
const cachedCookie = useState('cookie', null);
|
||||
const cachedCsrfToken = useState('x-csrf-token', null);
|
||||
const cachedTransactionId = useState('x-client-transaction-id', null);
|
||||
const cachedAuthorization = useState('authorization', null);
|
||||
|
||||
if (!cachedCookie || !cachedCsrfToken || !cachedAuthorization) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'cookie': cachedCookie,
|
||||
'x-csrf-token': cachedCsrfToken,
|
||||
...(cachedTransactionId ? { 'x-client-transaction-id': cachedTransactionId } : {}),
|
||||
Host: 'api.x.com',
|
||||
authorization: cachedAuthorization,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json',
|
||||
method: 'GET',
|
||||
headers: headers as unknown as Record<string, string>,
|
||||
},
|
||||
{
|
||||
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 isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const cachedCookie = useState('cookie', null);
|
||||
const cachedCsrfToken = useState('x-csrf-token', null);
|
||||
const cachedTransactionId = useState('x-client-transaction-id', null);
|
||||
const cachedAuthorization = useState('authorization', null);
|
||||
|
||||
// Only search for header values if not already cached
|
||||
if (!cachedCookie || !cachedCsrfToken || !cachedAuthorization) {
|
||||
const [header] = useHeaders((headers: any[]) =>
|
||||
headers.filter(h => h.url.includes('https://api.x.com/1.1/account/settings.json'))
|
||||
);
|
||||
|
||||
if (header) {
|
||||
const cookie = header.requestHeaders.find((h: any) => h.name === 'Cookie')?.value;
|
||||
const csrfToken = header.requestHeaders.find((h: any) => h.name === 'x-csrf-token')?.value;
|
||||
const transactionId = header.requestHeaders.find((h: any) => h.name === 'x-client-transaction-id')?.value;
|
||||
const authorization = header.requestHeaders.find((h: any) => h.name === 'authorization')?.value;
|
||||
|
||||
if (cookie && !cachedCookie) {
|
||||
setState('cookie', cookie);
|
||||
console.log('Cookie found');
|
||||
}
|
||||
if (csrfToken && !cachedCsrfToken) {
|
||||
setState('x-csrf-token', csrfToken);
|
||||
console.log('CSRF token found');
|
||||
}
|
||||
if (transactionId && !cachedTransactionId) {
|
||||
setState('x-client-transaction-id', transactionId);
|
||||
console.log('Transaction ID found');
|
||||
}
|
||||
if (authorization && !cachedAuthorization) {
|
||||
setState('authorization', authorization);
|
||||
console.log('Authorization found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const header = cachedCookie && cachedCsrfToken && cachedAuthorization;
|
||||
|
||||
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,
|
||||
};
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
2
packages/demo/public/robots.txt
Normal file
2
packages/demo/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
1258
packages/demo/src/App.css
Normal file
1258
packages/demo/src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
299
packages/demo/src/App.tsx
Normal file
299
packages/demo/src/App.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
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>
|
||||
|
||||
<footer className="app-footer">
|
||||
<a
|
||||
href="https://github.com/tlsnotary/tlsn-extension/tree/main/packages/demo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link"
|
||||
>
|
||||
View source on GitHub
|
||||
</a>
|
||||
<span className="footer-version">{__GIT_COMMIT_HASH__}</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
declare const __GIT_COMMIT_HASH__: string;
|
||||
62
packages/demo/src/components/BuildYourOwn.tsx
Normal file
62
packages/demo/src/components/BuildYourOwn.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
export function BuildYourOwn() {
|
||||
return (
|
||||
<div className="build-your-own">
|
||||
<div className="cta-content">
|
||||
<h2 className="cta-title">Ready to Build Your Own Plugin?</h2>
|
||||
<p className="cta-description">
|
||||
Create custom plugins to prove data from any website.
|
||||
Our SDK and documentation will help you get started in minutes.
|
||||
</p>
|
||||
|
||||
<div className="cta-buttons">
|
||||
<a
|
||||
href="https://tlsnotary.org/docs/extension/plugins"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cta-btn cta-btn-primary"
|
||||
>
|
||||
📚 Read the Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/tlsnotary/tlsn-extension/tree/main/packages/demo/src/plugins"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cta-btn cta-btn-secondary"
|
||||
>
|
||||
💻 View Plugin Sources
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="cta-resources">
|
||||
<h4 className="cta-resources-title">Resources</h4>
|
||||
<ul className="cta-resources-list">
|
||||
<li>
|
||||
<a href="https://github.com/tlsnotary/tlsn-extension" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Repository
|
||||
<span className="resource-desc">— Extension source code and examples</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://tlsnotary.org/docs/extension/plugins" target="_blank" rel="noopener noreferrer">
|
||||
TLSNotary Plugin Documentation
|
||||
<span className="resource-desc">— Complete protocol and API reference</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://tlsnotary.org" target="_blank" rel="noopener noreferrer">
|
||||
TLSNotary
|
||||
<span className="resource-desc">— TLSNotary landing page</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.com/invite/9XwESXtcN7" target="_blank" rel="noopener noreferrer">
|
||||
Discord Community
|
||||
<span className="resource-desc">— Get help and share your plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
packages/demo/src/components/CollapsibleSection.tsx
Normal file
28
packages/demo/src/components/CollapsibleSection.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
defaultExpanded?: boolean;
|
||||
expanded?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CollapsibleSection({ title, defaultExpanded = false, expanded, children }: CollapsibleSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded !== undefined) {
|
||||
setIsExpanded(expanded);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<div className="collapsible-section">
|
||||
<button className="collapsible-header" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<span className="collapsible-icon">{isExpanded ? '▼' : '▶'}</span>
|
||||
<span className="collapsible-title">{title}</span>
|
||||
</button>
|
||||
{isExpanded && <div className="collapsible-content">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
packages/demo/src/components/Console.tsx
Normal file
45
packages/demo/src/components/Console.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
interface ConsoleEntryProps {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
type: 'info' | 'success' | 'error' | 'warning';
|
||||
}
|
||||
|
||||
export function ConsoleEntry({ timestamp, message, type }: ConsoleEntryProps) {
|
||||
return (
|
||||
<div className={`console-entry ${type}`}>
|
||||
<span className="console-timestamp">[{timestamp}]</span>
|
||||
<span className="console-message">{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConsoleOutputProps {
|
||||
entries: ConsoleEntryProps[];
|
||||
onClear: () => void;
|
||||
onOpenExtensionLogs: () => void;
|
||||
}
|
||||
|
||||
export function ConsoleOutput({ entries, onClear, onOpenExtensionLogs }: ConsoleOutputProps) {
|
||||
return (
|
||||
<div className="console-section">
|
||||
<div className="console-header">
|
||||
<div className="console-title">Console Output</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button className="btn-console" onClick={onOpenExtensionLogs} style={{ background: '#6c757d' }}>
|
||||
View Extension Logs
|
||||
</button>
|
||||
<button className="btn-console" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="console-output" id="consoleOutput">
|
||||
{entries.map((entry, index) => (
|
||||
<ConsoleEntry key={index} {...entry} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
packages/demo/src/components/HowItWorks.tsx
Normal file
51
packages/demo/src/components/HowItWorks.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<div className="how-it-works">
|
||||
<h2 className="how-it-works-title">How It Works</h2>
|
||||
<p className="how-it-works-subtitle">
|
||||
Experience cryptographic proof generation in three simple steps
|
||||
</p>
|
||||
|
||||
<div className="steps-container">
|
||||
<div className="step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-icon">🔌</div>
|
||||
<h3 className="step-title">Run a Plugin</h3>
|
||||
<p className="step-description">
|
||||
Select a plugin and click "Run". A new browser window opens to the target website.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="step-arrow">→</div>
|
||||
|
||||
<div className="step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-icon">🔐</div>
|
||||
<h3 className="step-title">Create Proof</h3>
|
||||
<p className="step-description">
|
||||
Log in if needed, then click "Prove". TLSNotary creates a cryptographic proof of your data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="step-arrow">→</div>
|
||||
|
||||
<div className="step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-icon">✅</div>
|
||||
<h3 className="step-title">Verify Result</h3>
|
||||
<p className="step-description">
|
||||
The proof is verified by the server. Only the data you chose to reveal is shared.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="how-it-works-note">
|
||||
<span className="note-icon">💡</span>
|
||||
<span>
|
||||
<strong>Your data stays private:</strong> Plugins run inside the TLSNotary extension's secure sandbox.
|
||||
Data flows through your browser — never through third-party servers.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
packages/demo/src/components/PluginButtons.tsx
Normal file
111
packages/demo/src/components/PluginButtons.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useState } from 'react';
|
||||
import { Plugin } from '../types';
|
||||
|
||||
interface PluginResultData {
|
||||
resultHtml: string;
|
||||
debugJson: string;
|
||||
}
|
||||
|
||||
interface PluginButtonsProps {
|
||||
plugins: Record<string, Plugin>;
|
||||
runningPlugins: Set<string>;
|
||||
pluginResults: Record<string, PluginResultData>;
|
||||
allChecksPass: boolean;
|
||||
onRunPlugin: (pluginKey: string) => void;
|
||||
}
|
||||
|
||||
export function PluginButtons({
|
||||
plugins,
|
||||
runningPlugins,
|
||||
pluginResults,
|
||||
allChecksPass,
|
||||
onRunPlugin,
|
||||
}: PluginButtonsProps) {
|
||||
const [expandedRawData, setExpandedRawData] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleRawData = (key: string) => {
|
||||
setExpandedRawData((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(key)) {
|
||||
newSet.delete(key);
|
||||
} else {
|
||||
newSet.add(key);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plugin-grid">
|
||||
{Object.entries(plugins).map(([key, plugin]) => {
|
||||
const isRunning = runningPlugins.has(key);
|
||||
const result = pluginResults[key];
|
||||
const hasResult = !!result;
|
||||
|
||||
return (
|
||||
<div key={key} className={`plugin-card ${hasResult ? 'plugin-card--completed' : ''}`}>
|
||||
<div className="plugin-header">
|
||||
<div className="plugin-logo">{plugin.logo}</div>
|
||||
<div className="plugin-info">
|
||||
<h3 className="plugin-name">
|
||||
{plugin.name}
|
||||
{hasResult && <span className="plugin-badge">✓ Verified</span>}
|
||||
</h3>
|
||||
<p className="plugin-description">{plugin.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="plugin-actions">
|
||||
<button
|
||||
className="plugin-run-btn"
|
||||
disabled={!allChecksPass || isRunning}
|
||||
onClick={() => onRunPlugin(key)}
|
||||
title={!allChecksPass ? 'Please complete all system checks first' : ''}
|
||||
>
|
||||
{isRunning ? (
|
||||
<>
|
||||
<span className="spinner"></span> Running...
|
||||
</>
|
||||
) : hasResult ? (
|
||||
'↻ Run Again'
|
||||
) : (
|
||||
'▶ Run Plugin'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={plugin.file}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="plugin-source-btn"
|
||||
>
|
||||
<span>📄 View Source</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{hasResult && (
|
||||
<div className="plugin-result">
|
||||
<div className="plugin-result-header">
|
||||
<span className="plugin-result-title">Result</span>
|
||||
</div>
|
||||
<div
|
||||
className="plugin-result-content"
|
||||
dangerouslySetInnerHTML={{ __html: result.resultHtml }}
|
||||
/>
|
||||
<button
|
||||
className="plugin-raw-toggle"
|
||||
onClick={() => toggleRawData(key)}
|
||||
>
|
||||
{expandedRawData.has(key) ? '▼ Hide Raw Data' : '▶ Show Raw Data'}
|
||||
</button>
|
||||
{expandedRawData.has(key) && (
|
||||
<pre className="plugin-raw-data">{result.debugJson}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
packages/demo/src/components/StatusBar.tsx
Normal file
98
packages/demo/src/components/StatusBar.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface StatusBarProps {
|
||||
browserOk: boolean;
|
||||
extensionOk: boolean;
|
||||
verifierOk: boolean;
|
||||
onRecheck: () => void;
|
||||
detailsContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function StatusBar({
|
||||
browserOk,
|
||||
extensionOk,
|
||||
verifierOk,
|
||||
onRecheck,
|
||||
detailsContent,
|
||||
}: StatusBarProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const allOk = browserOk && extensionOk && verifierOk;
|
||||
const someIssues = !allOk;
|
||||
|
||||
return (
|
||||
<div className={`status-bar ${allOk ? 'status-ready' : 'status-issues'}`}>
|
||||
<div className="status-bar-content">
|
||||
<div className="status-indicator">
|
||||
{allOk ? (
|
||||
<>
|
||||
<span className="status-icon">✓</span>
|
||||
<span className="status-text">System Ready</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="status-icon">⚠</span>
|
||||
<span className="status-text">Setup Required</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="status-items">
|
||||
<div className={`status-badge ${browserOk ? 'ok' : 'error'}`}>
|
||||
Browser: {browserOk ? '✓' : '✗'}
|
||||
</div>
|
||||
<div className={`status-badge ${extensionOk ? 'ok' : 'error'}`}>
|
||||
Extension: {extensionOk ? '✓' : '✗'}
|
||||
</div>
|
||||
<div className={`status-badge ${verifierOk ? 'ok' : 'error'}`}>
|
||||
Verifier: {verifierOk ? '✓' : '✗'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="status-actions">
|
||||
{!verifierOk && (
|
||||
<button className="btn-recheck" onClick={onRecheck}>
|
||||
Recheck
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`btn-details ${showDetails ? 'expanded' : ''}`}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<span className="btn-details-icon">{showDetails ? '▼' : '▶'}</span>
|
||||
<span>Details</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
someIssues && (
|
||||
<div className="status-help">
|
||||
{!browserOk && <div>Please use a Chrome-based browser (Chrome, Edge, Brave)</div>}
|
||||
{!extensionOk && (
|
||||
<div>
|
||||
TLSNotary extension not detected.{' '}
|
||||
<a href="https://chromewebstore.google.com/detail/tlsnotary/gnoglgpcamodhflknhmafmjdahcejcgg?authuser=2&hl=en" target="_blank" rel="noopener noreferrer">
|
||||
Install extension
|
||||
</a>
|
||||
{' '}then <strong>refresh this page</strong>.
|
||||
</div>
|
||||
)}
|
||||
{!verifierOk && (
|
||||
<div>
|
||||
Verifier server not running. Start it with: <code>cd packages/verifier; cargo run --release</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showDetails && detailsContent && (
|
||||
<div className="status-details-content">
|
||||
{detailsContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
85
packages/demo/src/components/SystemChecks.tsx
Normal file
85
packages/demo/src/components/SystemChecks.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { CheckStatus } from '../types';
|
||||
|
||||
|
||||
interface CheckItemProps {
|
||||
id: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
status: CheckStatus;
|
||||
message: string;
|
||||
showInstructions?: boolean;
|
||||
onRecheck?: () => void;
|
||||
}
|
||||
|
||||
export function CheckItem({ icon, label, status, message, showInstructions, onRecheck }: CheckItemProps) {
|
||||
return (
|
||||
<div className={`check-item ${status}`}>
|
||||
{icon} {label}: <span className={`status ${status}`}>{message}</span>
|
||||
{showInstructions && (
|
||||
<div style={{ marginTop: '10px', fontSize: '14px' }}>
|
||||
<p>Start the verifier server:</p>
|
||||
<code>cd packages/verifier; cargo run --release</code>
|
||||
{onRecheck && (
|
||||
<button onClick={onRecheck} style={{ marginLeft: '10px', padding: '5px 10px' }}>
|
||||
Check Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SystemChecksProps {
|
||||
checks: {
|
||||
browser: { status: CheckStatus; message: string };
|
||||
extension: { status: CheckStatus; message: string };
|
||||
verifier: { status: CheckStatus; message: string; showInstructions: boolean };
|
||||
};
|
||||
onRecheck: () => void;
|
||||
showBrowserWarning: boolean;
|
||||
}
|
||||
|
||||
export function SystemChecks({ checks, onRecheck, showBrowserWarning }: SystemChecksProps) {
|
||||
return (
|
||||
<>
|
||||
{showBrowserWarning && (
|
||||
<div className="warning-box">
|
||||
<h3>⚠️ Browser Compatibility</h3>
|
||||
<p>
|
||||
<strong>Unsupported Browser Detected</strong>
|
||||
</p>
|
||||
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
|
||||
<p>Please switch to a supported browser to continue.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<strong>System Checks:</strong>
|
||||
<CheckItem
|
||||
id="check-browser"
|
||||
icon="🌐"
|
||||
label="Browser"
|
||||
status={checks.browser.status}
|
||||
message={checks.browser.message}
|
||||
/>
|
||||
<CheckItem
|
||||
id="check-extension"
|
||||
icon="🔌"
|
||||
label="Extension"
|
||||
status={checks.extension.status}
|
||||
message={checks.extension.message}
|
||||
/>
|
||||
<CheckItem
|
||||
id="check-verifier"
|
||||
icon="✅"
|
||||
label="Verifier"
|
||||
status={checks.verifier.status}
|
||||
message={checks.verifier.message}
|
||||
showInstructions={checks.verifier.showInstructions}
|
||||
onRecheck={onRecheck}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
packages/demo/src/components/WhyPlugins.tsx
Normal file
39
packages/demo/src/components/WhyPlugins.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
export function WhyPlugins() {
|
||||
return (
|
||||
<div className="why-plugins">
|
||||
<h2 className="why-plugins-title">Why Plugins?</h2>
|
||||
<p className="why-plugins-subtitle">
|
||||
TLSNotary plugins provide a secure, flexible way to prove and verify web data
|
||||
</p>
|
||||
|
||||
<div className="benefits-grid">
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">🔒</div>
|
||||
<h3 className="benefit-title">Secure by Design</h3>
|
||||
<p className="benefit-description">
|
||||
Plugins run inside the TLSNotary extension's sandboxed environment.
|
||||
Your credentials and sensitive data never leave your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">👤</div>
|
||||
<h3 className="benefit-title">User-Controlled</h3>
|
||||
<p className="benefit-description">
|
||||
Data flows through the user's browser — not third-party servers.
|
||||
You choose exactly what data to reveal in each proof.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="benefit-card">
|
||||
<div className="benefit-icon">⚡</div>
|
||||
<h3 className="benefit-title">Easy to Build</h3>
|
||||
<p className="benefit-description">
|
||||
Write plugins in JavaScript with a simple API.
|
||||
Intercept requests, create proofs, and build custom UIs with minimal code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
packages/demo/src/config.ts
Normal file
10
packages/demo/src/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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 SSL = (import.meta as any).env.VITE_SSL === 'true';
|
||||
|
||||
export const config = {
|
||||
verifierUrl: `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`,
|
||||
getProxyUrl: (host: string) => `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=${host}`,
|
||||
};
|
||||
9
packages/demo/src/main.tsx
Normal file
9
packages/demo/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
40
packages/demo/src/plugins.ts
Normal file
40
packages/demo/src/plugins.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
},
|
||||
},
|
||||
duolingo: {
|
||||
name: 'Duolingo',
|
||||
description: 'Prove your Duolingo language learning progress and achievements',
|
||||
logo: '🦉',
|
||||
file: '/plugins/duolingo.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
};
|
||||
45
packages/demo/src/types.ts
Normal file
45
packages/demo/src/types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
file: string;
|
||||
parseResult: (json: PluginResult) => string;
|
||||
}
|
||||
|
||||
export interface PluginResult {
|
||||
results: Array<{
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ConsoleEntry {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
type: 'info' | 'success' | 'error' | 'warning';
|
||||
}
|
||||
|
||||
export type CheckStatus = 'checking' | 'success' | 'error';
|
||||
|
||||
export interface SystemCheck {
|
||||
id: string;
|
||||
label: string;
|
||||
status: CheckStatus;
|
||||
message: string;
|
||||
showInstructions?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tlsn?: {
|
||||
execCode: (code: string) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
brave?: {
|
||||
isBrave: () => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
32
packages/demo/src/utils.ts
Normal file
32
packages/demo/src/utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { config } from './config';
|
||||
|
||||
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(`${config.verifierUrl}/health`);
|
||||
if (response.ok && (await response.text()) === 'ok') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimestamp(): string {
|
||||
return new Date().toLocaleTimeString();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Demo Server Startup Script
|
||||
#
|
||||
# This script starts the verifier server and demo file server via Docker.
|
||||
# Note: Run generate.sh first to create plugin files in the generated/ directory.
|
||||
#
|
||||
# Usage:
|
||||
# ./generate.sh && ./start.sh # Generate and start
|
||||
# ./start.sh # Start only (assumes generated/ exists)
|
||||
# ./start.sh -d # Start in detached mode
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Check if generated directory exists
|
||||
if [ ! -d "generated" ]; then
|
||||
echo "ERROR: generated/ directory not found!"
|
||||
echo "Please run ./generate.sh first to create plugin files."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "========================================"
|
||||
echo "TLSNotary Demo Server"
|
||||
echo "========================================"
|
||||
echo "Starting Docker services..."
|
||||
echo "========================================"
|
||||
|
||||
# Start docker compose
|
||||
docker compose up --build "$@"
|
||||
33
packages/demo/tsconfig.json
Normal file
33
packages/demo/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
packages/demo/tsconfig.node.json
Normal file
12
packages/demo/tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
15
packages/demo/tsconfig.plugins.json
Normal file
15
packages/demo/tsconfig.plugins.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"isolatedModules": false
|
||||
},
|
||||
"include": [
|
||||
"src/plugins/**/*.ts",
|
||||
"src/plugins/plugin-globals.d.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
// =============================================================================
|
||||
// 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: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
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.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Get the intercepted X.com API request headers
|
||||
* 2. Extract authentication headers (Cookie, CSRF token, OAuth token, etc.)
|
||||
* 3. Call prove() with the request configuration and reveal handlers
|
||||
* 4. prove() internally:
|
||||
* - Creates a prover connection to the verifier
|
||||
* - Sends the HTTP request through the TLS prover
|
||||
* - Captures the TLS transcript (sent/received bytes)
|
||||
* - Parses the transcript with byte-level range tracking
|
||||
* - Applies selective reveal handlers to show only specified data
|
||||
* - Generates and returns the cryptographic proof
|
||||
* 5. Return the proof result to the caller via done()
|
||||
*/
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
// Step 1: Get the intercepted header from the X.com API request
|
||||
// useHeaders() provides access to all intercepted HTTP request headers
|
||||
// We filter for the specific X.com API endpoint we want to prove
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
|
||||
});
|
||||
|
||||
// Step 2: Extract authentication headers from the intercepted request
|
||||
// These headers are required to authenticate with the X.com API
|
||||
const headers = {
|
||||
// Cookie: Session authentication token
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
|
||||
// X-CSRF-Token: Cross-Site Request Forgery protection token
|
||||
'x-csrf-token': header.requestHeaders.find(header => header.name === 'x-csrf-token')?.value,
|
||||
|
||||
// X-Client-Transaction-ID: Request tracking identifier
|
||||
'x-client-transaction-id': header.requestHeaders.find(header => header.name === 'x-client-transaction-id')?.value,
|
||||
|
||||
// Host: Target server hostname
|
||||
Host: 'api.x.com',
|
||||
|
||||
// Authorization: OAuth bearer token for API authentication
|
||||
authorization: header.requestHeaders.find(header => header.name === 'authorization')?.value,
|
||||
|
||||
// Accept-Encoding: Must be 'identity' for TLSNotary (no compression)
|
||||
// TLSNotary requires uncompressed data to verify byte-for-byte
|
||||
'Accept-Encoding': 'identity',
|
||||
|
||||
// Connection: Use 'close' to complete the connection after one request
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
// Step 3: Generate TLS proof using the unified prove() API
|
||||
// This single function handles the entire proof generation pipeline
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
// REQUEST OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Defines the HTTP request to be proven
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json', // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PROVER OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Configures the TLS proof generation process
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
// Must be running locally or accessible at this address
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
|
||||
// Proxy URL: WebSocket proxy that relays TLS data to the target server
|
||||
// The token parameter specifies which server to connect to
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
|
||||
|
||||
// Maximum bytes to receive from server (response size limit)
|
||||
maxRecvData: 4000,
|
||||
|
||||
// Maximum bytes to send to server (request size limit)
|
||||
maxSentData: 2000,
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
handlers: [
|
||||
// Reveal the request start line (GET /path HTTP/1.1)
|
||||
// This proves the HTTP method and path were sent
|
||||
{
|
||||
type: 'SENT', // Direction: data sent to server
|
||||
part: 'START_LINE', // Part: HTTP request line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the response start line (HTTP/1.1 200 OK)
|
||||
// This proves the server responded with status code 200
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'START_LINE', // Part: HTTP response line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the 'date' header from the response
|
||||
// This proves when the server generated the response
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'HEADERS', // Part: HTTP headers
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
key: 'date', // Specific header to reveal
|
||||
},
|
||||
},
|
||||
|
||||
// Reveal the 'screen_name' field from the JSON response body
|
||||
// This proves the X.com username without revealing other profile data
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'BODY', // Part: HTTP response body
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
type: 'json', // Body format: JSON
|
||||
path: 'screen_name', // JSON field to reveal (top-level only)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
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.
|
||||
*
|
||||
* React-like Hooks Used:
|
||||
* - useHeaders(): Subscribes to intercepted HTTP request headers
|
||||
* - useEffect(): Runs side effects when dependencies change
|
||||
*
|
||||
* UI Flow:
|
||||
* 1. Check if X.com API request headers have been intercepted
|
||||
* 2. If not intercepted yet: Show "Please login" message
|
||||
* 3. If intercepted: Show "Profile detected" with a "Prove" button
|
||||
* 4. On first render: Open X.com in a new window to trigger login
|
||||
*/
|
||||
function main() {
|
||||
// Subscribe to intercepted headers for the X.com API endpoint
|
||||
// This will reactively update whenever new headers matching the filter arrive
|
||||
const [header] = useHeaders(headers => 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);
|
||||
|
||||
// Run once on plugin load: Open X.com in a new window
|
||||
// The empty dependency array [] means this runs only once
|
||||
// The opened window's requests will be intercepted by the plugin
|
||||
useEffect(() => {
|
||||
openWindow('https://x.com');
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
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',
|
||||
}, ['🔐']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
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',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
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',
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether profile is detected
|
||||
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'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
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',
|
||||
cursor: 'pointer',
|
||||
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'])
|
||||
) : (
|
||||
// Show login message
|
||||
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
|
||||
// =============================================================================
|
||||
/**
|
||||
* All plugins must export an object with these properties:
|
||||
* - main: The reactive UI rendering function
|
||||
* - onClick: Click handler callback for buttons
|
||||
* - config: Plugin metadata
|
||||
*/
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
23
packages/demo/vite.config.ts
Normal file
23
packages/demo/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// Get git commit hash from GIT_HASH env var (set by CI/Docker) or fallback to 'local'
|
||||
const gitHash = process.env.GIT_HASH || 'local';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__GIT_COMMIT_HASH__: JSON.stringify(gitHash),
|
||||
},
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "extension",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0.1400",
|
||||
"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",
|
||||
@@ -47,7 +47,6 @@
|
||||
"redux-thunk": "^2.4.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "^0.1.0-alpha.12.0",
|
||||
"tlsn-wasm": "./lib/tlsn-wasm-pkg/",
|
||||
"util": "^0.12.5"
|
||||
},
|
||||
@@ -84,7 +83,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,7 +106,7 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"version": "0.1.0.13",
|
||||
"version": "0.1.0.1400",
|
||||
"name": "TLSNotary",
|
||||
"description": "A Chrome extension for TLSNotary",
|
||||
"options_page": "options.html",
|
||||
@@ -15,22 +15,41 @@
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["contentScript.bundle.js"],
|
||||
"css": ["content.styles.css"]
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*",
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"contentScript.bundle.js"
|
||||
],
|
||||
"css": [
|
||||
"content.styles.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "*.wasm"],
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
|
||||
"resources": [
|
||||
"content.styles.css",
|
||||
"icon-128.png",
|
||||
"icon-34.png",
|
||||
"content.bundle.js",
|
||||
"*.wasm"
|
||||
],
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"permissions": [
|
||||
"offscreen",
|
||||
"webRequest",
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"windows",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Host, { Parser } from '@tlsn/plugin-sdk/src';
|
||||
import { ProveManager } from './ProveManager';
|
||||
import { Method } from 'tlsn-js';
|
||||
import type { Method } from '../../../tlsn-wasm-pkg/tlsn_wasm';
|
||||
import { DomJson, Handler, PluginConfig } from '@tlsn/plugin-sdk/src/types';
|
||||
import { processHandlers } from './rangeExtractor';
|
||||
import { logger } from '@tlsn/common';
|
||||
|
||||
@@ -27,7 +27,7 @@ var compiler = webpack(config);
|
||||
|
||||
var server = new WebpackDevServer(
|
||||
{
|
||||
https: false,
|
||||
server: 'http',
|
||||
hot: true,
|
||||
liveReload: false,
|
||||
client: {
|
||||
|
||||
@@ -42,6 +42,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 +52,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 +68,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"
|
||||
}
|
||||
}
|
||||
|
||||
97
packages/plugin-sdk/src/globals.d.ts
vendored
Normal file
97
packages/plugin-sdk/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Global type declarations for TLSNotary plugin runtime environment
|
||||
*
|
||||
* These functions are injected at runtime by the plugin sandbox.
|
||||
* Import this file in your plugin to get TypeScript support:
|
||||
*
|
||||
* /// <reference types="@tlsn/plugin-sdk/globals" />
|
||||
*/
|
||||
|
||||
import type {
|
||||
InterceptedRequest,
|
||||
InterceptedRequestHeader,
|
||||
Handler,
|
||||
DomOptions,
|
||||
DomJson,
|
||||
} from './types';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Create a div element
|
||||
*/
|
||||
function div(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
|
||||
function div(children?: (DomJson | string)[]): DomJson;
|
||||
|
||||
/**
|
||||
* Create a button element
|
||||
*/
|
||||
function button(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
|
||||
function button(children?: (DomJson | string)[]): DomJson;
|
||||
|
||||
/**
|
||||
* Get or initialize state value (React-like useState)
|
||||
*/
|
||||
function useState<T>(key: string, initialValue: T): T;
|
||||
|
||||
/**
|
||||
* Update state value
|
||||
*/
|
||||
function setState<T>(key: string, value: T): void;
|
||||
|
||||
/**
|
||||
* Run side effect when dependencies change (React-like useEffect)
|
||||
*/
|
||||
function useEffect(effect: () => void, deps: any[]): void;
|
||||
|
||||
/**
|
||||
* Subscribe to intercepted HTTP headers
|
||||
*/
|
||||
function useHeaders(
|
||||
filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[],
|
||||
): [InterceptedRequestHeader | undefined];
|
||||
|
||||
/**
|
||||
* Subscribe to intercepted HTTP requests
|
||||
*/
|
||||
function useRequests(
|
||||
filter: (requests: InterceptedRequest[]) => InterceptedRequest[],
|
||||
): [InterceptedRequest | undefined];
|
||||
|
||||
/**
|
||||
* Open a new browser window for user interaction
|
||||
*/
|
||||
function openWindow(
|
||||
url: string,
|
||||
options?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
showOverlay?: boolean;
|
||||
},
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Generate a TLS proof for an HTTP request
|
||||
*/
|
||||
function prove(
|
||||
requestOptions: {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body?: string;
|
||||
},
|
||||
proverOptions: {
|
||||
verifierUrl: string;
|
||||
proxyUrl: string;
|
||||
maxRecvData?: number;
|
||||
maxSentData?: number;
|
||||
handlers: Handler[];
|
||||
},
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* Complete plugin execution and return result
|
||||
*/
|
||||
function done(result?: any): void;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "tlsn-wasm",
|
||||
"type": "module",
|
||||
"description": "A core WebAssembly package for TLSNotary.",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.14",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
148
packages/tlsn-wasm-pkg/tlsn_wasm.d.ts
vendored
148
packages/tlsn-wasm-pkg/tlsn_wasm.d.ts
vendored
@@ -1,19 +1,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Initializes the module.
|
||||
*/
|
||||
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
|
||||
/**
|
||||
* Starts the thread spawner on a dedicated worker thread.
|
||||
*/
|
||||
export function startSpawner(): Promise<any>;
|
||||
export function web_spawn_start_worker(worker: number): void;
|
||||
export function web_spawn_recover_spawner(spawner: number): Spawner;
|
||||
export interface CrateLogFilter {
|
||||
level: LoggingLevel;
|
||||
name: string;
|
||||
}
|
||||
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
|
||||
|
||||
export type SpanEvent = "New" | "Close" | "Active";
|
||||
|
||||
export interface LoggingConfig {
|
||||
level: LoggingLevel | undefined;
|
||||
@@ -21,15 +10,43 @@ export interface LoggingConfig {
|
||||
span_events: SpanEvent[] | undefined;
|
||||
}
|
||||
|
||||
export type SpanEvent = "New" | "Close" | "Active";
|
||||
export interface CrateLogFilter {
|
||||
level: LoggingLevel;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
|
||||
export type Body = JsonValue;
|
||||
|
||||
export type NetworkSetting = "Bandwidth" | "Latency";
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export interface Commit {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
export interface HttpRequest {
|
||||
uri: string;
|
||||
method: Method;
|
||||
headers: Map<string, number[]>;
|
||||
body: Body | undefined;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
headers: [string, number[]][];
|
||||
}
|
||||
|
||||
export type TlsVersion = "V1_2" | "V1_3";
|
||||
|
||||
export interface TranscriptLength {
|
||||
sent: number;
|
||||
recv: number;
|
||||
}
|
||||
|
||||
export interface ConnectionInfo {
|
||||
time: number;
|
||||
version: TlsVersion;
|
||||
transcript_length: TranscriptLength;
|
||||
}
|
||||
|
||||
export interface Transcript {
|
||||
sent: number[];
|
||||
recv: number[];
|
||||
}
|
||||
|
||||
export interface PartialTranscript {
|
||||
@@ -39,52 +56,25 @@ export interface PartialTranscript {
|
||||
recv_authed: { start: number; end: number }[];
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
headers: [string, number[]][];
|
||||
export interface Commit {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
}
|
||||
|
||||
export type Body = JsonValue;
|
||||
|
||||
export interface VerifierOutput {
|
||||
server_name: string | undefined;
|
||||
connection_info: ConnectionInfo;
|
||||
transcript: PartialTranscript | undefined;
|
||||
}
|
||||
|
||||
export interface ConnectionInfo {
|
||||
time: number;
|
||||
version: TlsVersion;
|
||||
transcript_length: TranscriptLength;
|
||||
}
|
||||
|
||||
export interface TranscriptLength {
|
||||
sent: number;
|
||||
recv: number;
|
||||
}
|
||||
|
||||
export type TlsVersion = "V1_2" | "V1_3";
|
||||
|
||||
export interface HttpRequest {
|
||||
uri: string;
|
||||
method: Method;
|
||||
headers: Map<string, number[]>;
|
||||
body: Body | undefined;
|
||||
}
|
||||
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export interface Reveal {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
server_identity: boolean;
|
||||
}
|
||||
|
||||
export interface Transcript {
|
||||
sent: number[];
|
||||
recv: number[];
|
||||
export interface VerifierOutput {
|
||||
server_name: string | undefined;
|
||||
connection_info: ConnectionInfo;
|
||||
transcript: PartialTranscript | undefined;
|
||||
}
|
||||
|
||||
export type NetworkSetting = "Bandwidth" | "Latency";
|
||||
|
||||
export interface ProverConfig {
|
||||
server_name: string;
|
||||
max_sent_data: number;
|
||||
@@ -104,18 +94,14 @@ export interface VerifierConfig {
|
||||
max_recv_records_online: number | undefined;
|
||||
}
|
||||
|
||||
|
||||
export class Prover {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Returns the transcript.
|
||||
*/
|
||||
transcript(): Transcript;
|
||||
/**
|
||||
* Send the HTTP request to the server.
|
||||
*/
|
||||
send_request(ws_proxy_url: string, request: HttpRequest): Promise<HttpResponse>;
|
||||
constructor(config: ProverConfig);
|
||||
/**
|
||||
* Set up the prover.
|
||||
*
|
||||
@@ -127,10 +113,13 @@ export class Prover {
|
||||
* Reveals data to the verifier and finalizes the protocol.
|
||||
*/
|
||||
reveal(reveal: Reveal): Promise<void>;
|
||||
/**
|
||||
* Returns the transcript.
|
||||
*/
|
||||
transcript(): Transcript;
|
||||
constructor(config: ProverConfig);
|
||||
}
|
||||
/**
|
||||
* Global spawner which spawns closures into web workers.
|
||||
*/
|
||||
|
||||
export class Spawner {
|
||||
private constructor();
|
||||
free(): void;
|
||||
@@ -141,10 +130,10 @@ export class Spawner {
|
||||
run(url: string): Promise<void>;
|
||||
intoRaw(): number;
|
||||
}
|
||||
|
||||
export class Verifier {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
constructor(config: VerifierConfig);
|
||||
/**
|
||||
* Verifies the connection and finalizes the protocol.
|
||||
*/
|
||||
@@ -153,13 +142,29 @@ export class Verifier {
|
||||
* Connect to the prover.
|
||||
*/
|
||||
connect(prover_url: string): Promise<void>;
|
||||
constructor(config: VerifierConfig);
|
||||
}
|
||||
|
||||
export class WorkerData {
|
||||
private constructor();
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the module.
|
||||
*/
|
||||
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Starts the thread spawner on a dedicated worker thread.
|
||||
*/
|
||||
export function startSpawner(): Promise<any>;
|
||||
|
||||
export function web_spawn_recover_spawner(spawner: number): Spawner;
|
||||
|
||||
export function web_spawn_start_worker(worker: number): void;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
@@ -182,12 +187,12 @@ export interface InitOutput {
|
||||
readonly web_spawn_recover_spawner: (a: number) => number;
|
||||
readonly web_spawn_start_worker: (a: number) => void;
|
||||
readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
@@ -201,6 +206,7 @@ export interface InitOutput {
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
12
packages/tlsn-wasm-pkg/tlsn_wasm_bg.wasm.d.ts
vendored
12
packages/tlsn-wasm-pkg/tlsn_wasm_bg.wasm.d.ts
vendored
@@ -19,12 +19,12 @@ export const startSpawner: () => any;
|
||||
export const web_spawn_recover_spawner: (a: number) => number;
|
||||
export const web_spawn_start_worker: (a: number) => void;
|
||||
export const ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
|
||||
@@ -33,10 +33,10 @@ fi
|
||||
git checkout "${VERSION}" --force
|
||||
git reset --hard
|
||||
|
||||
cd crates/wasm
|
||||
# Apply no-logging modification if requested
|
||||
if [ "$NO_LOGGING" = "--no-logging" ]; then
|
||||
echo "Applying no-logging configuration..."
|
||||
cd crates/wasm
|
||||
|
||||
# Add it to the wasm32 target section (after the section header)
|
||||
sed -i.bak '/^\[target\.\x27cfg(target_arch = "wasm32")\x27\.dependencies\]$/a\
|
||||
@@ -45,11 +45,8 @@ tracing = { workspace = true, features = ["release_max_level_off"] }' Cargo.toml
|
||||
|
||||
# Clean up backup file
|
||||
rm Cargo.toml.bak
|
||||
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
cd crates/wasm
|
||||
cargo update
|
||||
./build.sh
|
||||
cd ../../
|
||||
|
||||
522
packages/verifier/Cargo.lock
generated
522
packages/verifier/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# TLSNotary dependency
|
||||
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.13" }
|
||||
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.14", features = ["mozilla-certs"] }
|
||||
|
||||
# HTTP server framework
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
@@ -46,7 +46,7 @@ eyre = "0.6"
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
regex = "1.12.2"
|
||||
rangeset = "0.2.0"
|
||||
rangeset = "0.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||
|
||||
@@ -34,6 +34,10 @@ FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Accept build argument for git hash and set as environment variable
|
||||
ARG GIT_HASH=local
|
||||
ENV GIT_HASH=${GIT_HASH}
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
|
||||
@@ -13,7 +13,7 @@ A Rust-based HTTP server with WebSocket support for TLSNotary verification opera
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **tlsn**: v0.1.0-alpha.13 from GitHub - TLSNotary verification library
|
||||
- **tlsn**: v0.1.0-alpha.14 from GitHub - TLSNotary verification library
|
||||
- **axum**: Modern web framework with WebSocket support
|
||||
- **tokio**: Async runtime with full features
|
||||
- **tokio-util**: Async utilities for stream compatibility
|
||||
|
||||
@@ -12,7 +12,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_websocket::{WebSocket, WebSocketUpgrade};
|
||||
use rangeset::RangeSet;
|
||||
use rangeset::prelude::RangeSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
@@ -59,6 +59,7 @@ async fn main() {
|
||||
// Build router with routes
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/info", get(info_handler))
|
||||
.route("/session", get(session_ws_handler))
|
||||
.route("/verifier", get(verifier_ws_handler))
|
||||
.route("/proxy", get(proxy_ws_handler))
|
||||
@@ -75,6 +76,7 @@ async fn main() {
|
||||
|
||||
info!("Server listening on http://{}", addr);
|
||||
info!("Health endpoint: http://{}/health", addr);
|
||||
info!("Info endpoint: http://{}/info", addr);
|
||||
info!("Session WebSocket endpoint: ws://{}/session", addr);
|
||||
info!(
|
||||
"Verifier WebSocket endpoint: ws://{}/verifier?sessionId=<id>",
|
||||
@@ -352,6 +354,28 @@ async fn health_handler() -> impl IntoResponse {
|
||||
"ok"
|
||||
}
|
||||
|
||||
/// Info response structure
|
||||
#[derive(Debug, Serialize)]
|
||||
struct InfoResponse {
|
||||
/// Package version from Cargo.toml
|
||||
version: &'static str,
|
||||
/// Git commit hash (from GIT_HASH env var, set by CI)
|
||||
git_hash: String,
|
||||
/// TLSNotary library version
|
||||
tlsn_version: &'static str,
|
||||
}
|
||||
|
||||
/// Info endpoint handler - returns server information as JSON
|
||||
pub(crate) async fn info_handler() -> impl IntoResponse {
|
||||
let git_hash = std::env::var("GIT_HASH").unwrap_or_else(|_| "dev".to_string());
|
||||
|
||||
axum::Json(InfoResponse {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
git_hash,
|
||||
tlsn_version: "0.1.0-alpha.14",
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket session handler for extension
|
||||
pub(crate) async fn session_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
@@ -892,13 +916,13 @@ async fn run_verifier_task(
|
||||
"[{}] Sent data length: {} bytes (authed: {} bytes)",
|
||||
session_id,
|
||||
sent_bytes.len(),
|
||||
transcript.sent_authed().iter().sum::<usize>(),
|
||||
transcript.sent_authed().len(),
|
||||
);
|
||||
info!(
|
||||
"[{}] Received data length: {} bytes (authed: {} bytes)",
|
||||
session_id,
|
||||
recv_bytes.len(),
|
||||
transcript.received_authed().iter().sum::<usize>()
|
||||
transcript.received_authed().len()
|
||||
);
|
||||
|
||||
// Wait for RevealConfig to be available (with polling and timeout)
|
||||
|
||||
@@ -28,9 +28,11 @@ use tracing::info;
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
use tlsn::{
|
||||
config::ProtocolConfig,
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig},
|
||||
config::{
|
||||
prove::ProveConfig, prover::ProverConfig, tls::TlsClientConfig, tls_commit::TlsCommitConfig,
|
||||
},
|
||||
prover::Prover,
|
||||
Session,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -156,11 +158,11 @@ async fn webhook_handler(
|
||||
// ============================================================================
|
||||
|
||||
async fn start_verifier_server(webhook_port: u16, verifier_port: u16) -> JoinHandle<()> {
|
||||
// Create config with webhook for swapi.dev
|
||||
// Create config with webhook for raw.githubusercontent.com
|
||||
let config_yaml = format!(
|
||||
r#"
|
||||
webhooks:
|
||||
"swapi.dev":
|
||||
"raw.githubusercontent.com":
|
||||
url: "http://127.0.0.1:{}"
|
||||
headers: {{}}
|
||||
"#,
|
||||
@@ -176,14 +178,9 @@ webhooks:
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", axum::routing::get(|| async { "ok" }))
|
||||
.route(
|
||||
"/session",
|
||||
axum::routing::get(crate::session_ws_handler),
|
||||
)
|
||||
.route(
|
||||
"/verifier",
|
||||
axum::routing::get(crate::verifier_ws_handler),
|
||||
)
|
||||
.route("/info", axum::routing::get(crate::info_handler))
|
||||
.route("/session", axum::routing::get(crate::session_ws_handler))
|
||||
.route("/verifier", axum::routing::get(crate::verifier_ws_handler))
|
||||
.route("/proxy", axum::routing::get(crate::proxy_ws_handler))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(app_state);
|
||||
@@ -385,15 +382,23 @@ async fn connect_wss(
|
||||
|
||||
/// Helper function that performs MPC-TLS and HTTP request with a given proxy stream
|
||||
async fn run_prover_with_stream<S>(
|
||||
prover: tlsn::prover::Prover<tlsn::prover::state::Setup>,
|
||||
prover: Prover,
|
||||
tls_commit_config: TlsCommitConfig,
|
||||
tls_client_config: TlsClientConfig,
|
||||
proxy_stream: S,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error + Send + Sync>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
// 5. Pass proxy connection into the prover for TLS
|
||||
// 5. Start the TLS commitment protocol
|
||||
let prover = prover
|
||||
.commit(tls_commit_config)
|
||||
.await
|
||||
.map_err(|e| format!("Commitment failed: {}", e))?;
|
||||
|
||||
// 6. Pass proxy connection into the prover for TLS
|
||||
let (mpc_tls_connection, prover_fut) = prover
|
||||
.connect(proxy_stream)
|
||||
.connect(tls_client_config, proxy_stream)
|
||||
.await
|
||||
.map_err(|e| format!("TLS connect failed: {}", e))?;
|
||||
|
||||
@@ -405,18 +410,19 @@ where
|
||||
// Spawn the prover task
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// 6. HTTP handshake
|
||||
let (mut request_sender, connection) = hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
// 7. HTTP handshake
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
|
||||
tokio::spawn(connection);
|
||||
|
||||
// 7. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /api/films/1/");
|
||||
// 8. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json");
|
||||
let request = Request::builder()
|
||||
.uri("/api/films/1/")
|
||||
.header("Host", "swapi.dev")
|
||||
.uri("/tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json")
|
||||
.header("Host", "raw.githubusercontent.com")
|
||||
.header("Accept", "application/json")
|
||||
.header("Connection", "close")
|
||||
.method("GET")
|
||||
@@ -431,7 +437,7 @@ where
|
||||
info!("[Prover] Response status: {}", response.status());
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// 8. Wait for prover task to complete
|
||||
// 9. Wait for prover task to complete
|
||||
let mut prover = prover_task
|
||||
.await
|
||||
.map_err(|e| format!("Prover task panicked: {}", e))?
|
||||
@@ -446,24 +452,26 @@ where
|
||||
recv.len()
|
||||
);
|
||||
|
||||
// 9. Build reveal configuration (reveal everything)
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
builder.server_identity();
|
||||
builder
|
||||
// 10. Build proof configuration (reveal everything including server identity)
|
||||
let mut prove_config = ProveConfig::builder(prover.transcript());
|
||||
prove_config.server_identity();
|
||||
prove_config
|
||||
.reveal_sent(&(0..sent.len()))
|
||||
.map_err(|e| format!("reveal_sent failed: {}", e))?;
|
||||
builder
|
||||
prove_config
|
||||
.reveal_recv(&(0..recv.len()))
|
||||
.map_err(|e| format!("reveal_recv failed: {}", e))?;
|
||||
let prove_config = prove_config
|
||||
.build()
|
||||
.map_err(|e| format!("build proof failed: {}", e))?;
|
||||
|
||||
let config = builder.build().unwrap();
|
||||
|
||||
// 10. Send proof to verifier
|
||||
// 11. Send proof to verifier
|
||||
info!("[Prover] Sending proof to verifier");
|
||||
prover
|
||||
.prove(&config)
|
||||
.prove(&prove_config)
|
||||
.await
|
||||
.map_err(|e| format!("prove failed: {}", e))?;
|
||||
|
||||
prover
|
||||
.close()
|
||||
.await
|
||||
@@ -491,50 +499,134 @@ async fn run_prover(
|
||||
// WsStream implements tokio::io::AsyncRead/AsyncWrite when inner implements futures_io traits
|
||||
let verifier_stream = WsStream::new(verifier_ws);
|
||||
|
||||
// 2. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.server_name(ServerName::Dns("swapi.dev".try_into().unwrap()))
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
// 2. Create session with verifier stream
|
||||
let session = Session::new(verifier_stream);
|
||||
let (driver, mut handle) = session.split();
|
||||
|
||||
// Spawn the session driver in the background
|
||||
let driver_task = tokio::spawn(driver);
|
||||
|
||||
// 3. Create TLS commit config for MPC protocol
|
||||
use tlsn::config::tls_commit::{mpc::MpcTlsConfig, TlsCommitProtocolConfig};
|
||||
let mpc_config = MpcTlsConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap();
|
||||
.map_err(|e| format!("Failed to build MPC TLS config: {}", e))?;
|
||||
|
||||
let tls_commit_config = TlsCommitConfig::builder()
|
||||
.protocol(TlsCommitProtocolConfig::Mpc(mpc_config))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS commit config: {}", e))?;
|
||||
|
||||
// 4. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build prover config: {}", e))?;
|
||||
|
||||
info!("[Prover] Setting up MPC-TLS with verifier");
|
||||
|
||||
// 3. Create prover and perform setup with verifier
|
||||
// tlsn expects futures_io traits, so we don't need compat() - WsStream already provides them
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_stream)
|
||||
.await
|
||||
.map_err(|e| format!("Prover setup failed: {}", e))?;
|
||||
// 5. Create prover via handle
|
||||
let prover = handle
|
||||
.new_prover(prover_config)
|
||||
.map_err(|e| format!("Failed to create prover: {}", e))?;
|
||||
|
||||
// 6. Create TLS client config with server name and root certs
|
||||
use tlsn::{connection::ServerName, webpki::RootCertStore};
|
||||
let tls_client_config = TlsClientConfig::builder()
|
||||
.server_name(ServerName::Dns(
|
||||
"raw.githubusercontent.com".try_into().unwrap(),
|
||||
))
|
||||
.root_store(RootCertStore::mozilla())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS client config: {}", e))?;
|
||||
|
||||
info!("[Prover] Connecting to proxy at {}", proxy_url);
|
||||
|
||||
// 4. Connect to proxy WebSocket (ws:// or wss://)
|
||||
if proxy_url.starts_with("wss://") {
|
||||
// 7. Connect to proxy WebSocket (ws:// or wss://) and run prover
|
||||
let result = if proxy_url.starts_with("wss://") {
|
||||
let proxy_ws = connect_wss(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (wss)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
} else {
|
||||
let proxy_ws = connect_ws(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (ws)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
}
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
};
|
||||
|
||||
// 8. Close the session handle
|
||||
handle.close();
|
||||
|
||||
// 9. Wait for the driver to complete
|
||||
driver_task
|
||||
.await
|
||||
.map_err(|e| format!("Driver task failed: {}", e))?
|
||||
.map_err(|e| format!("Session driver error: {}", e))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Test
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Test the /health endpoint
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_swapi() {
|
||||
async fn health() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 1, VERIFIER_PORT + 1).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/health", VERIFIER_PORT + 1))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.text().await.unwrap(), "ok");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
/// Test the /info endpoint returns expected JSON structure
|
||||
#[tokio::test]
|
||||
async fn info() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 2, VERIFIER_PORT + 2).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/info", VERIFIER_PORT + 2))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let info: Value = resp.json().await.expect("Failed to parse JSON");
|
||||
|
||||
// Verify required fields exist
|
||||
info.get("version").expect("Missing version field");
|
||||
info.get("git_hash").expect("Missing git_hash field");
|
||||
info.get("tlsn_version")
|
||||
.expect("Missing tlsn_version field");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_github() {
|
||||
// Initialize tracing for debugging
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
@@ -573,7 +665,10 @@ async fn test_webhook_integration_with_swapi() {
|
||||
"ws://127.0.0.1:{}/verifier?sessionId={}",
|
||||
VERIFIER_PORT, session_id
|
||||
);
|
||||
let proxy_url = format!("ws://127.0.0.1:{}/proxy?token=swapi.dev", VERIFIER_PORT);
|
||||
let proxy_url = format!(
|
||||
"ws://127.0.0.1:{}/proxy?token=raw.githubusercontent.com",
|
||||
VERIFIER_PORT
|
||||
);
|
||||
|
||||
let prover_handle = tokio::spawn(async move {
|
||||
run_prover(verifier_ws_url, proxy_url, MAX_SENT_DATA, MAX_RECV_DATA).await
|
||||
@@ -628,11 +723,11 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// 8. Verify results contain expected data
|
||||
assert!(!results.is_empty(), "Should have handler results");
|
||||
|
||||
// Check that response contains Star Wars data
|
||||
// Check that response contains expected JSON data
|
||||
let recv_str = String::from_utf8_lossy(&recv_transcript);
|
||||
assert!(
|
||||
recv_str.contains("A New Hope") || recv_str.contains("Star Wars"),
|
||||
"Response should contain Star Wars film data: {}",
|
||||
recv_str.contains("software engineer") || recv_str.contains("Anytown"),
|
||||
"Response should contain expected JSON data: {}",
|
||||
&recv_str[..recv_str.len().min(500)]
|
||||
);
|
||||
|
||||
@@ -651,8 +746,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
|
||||
// Verify webhook payload structure
|
||||
assert_eq!(
|
||||
payload["server_name"], "swapi.dev",
|
||||
"server_name should be swapi.dev"
|
||||
payload["server_name"], "raw.githubusercontent.com",
|
||||
"server_name should be raw.githubusercontent.com"
|
||||
);
|
||||
assert!(payload["results"].is_array(), "results should be an array");
|
||||
assert!(
|
||||
@@ -691,8 +786,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// Verify transcript contains expected content
|
||||
let webhook_recv = payload["transcript"]["recv"].as_str().unwrap();
|
||||
assert!(
|
||||
webhook_recv.contains("A New Hope") || webhook_recv.contains("title"),
|
||||
"Webhook transcript should contain Star Wars film data"
|
||||
webhook_recv.contains("software engineer") || webhook_recv.contains("Anytown"),
|
||||
"Webhook transcript should contain expected JSON data"
|
||||
);
|
||||
|
||||
info!("All assertions passed!");
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use eyre::eyre;
|
||||
use tlsn::{
|
||||
config::ProtocolConfigValidator,
|
||||
config::{tls_commit::TlsCommitProtocolConfig, verifier::VerifierConfig},
|
||||
connection::{DnsName, ServerName},
|
||||
transcript::PartialTranscript,
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
verifier::VerifierOutput,
|
||||
webpki::RootCertStore,
|
||||
Session,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
@@ -23,31 +25,103 @@ pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
max_sent_data, max_recv_data
|
||||
);
|
||||
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap();
|
||||
// Create a session with the prover
|
||||
let session = Session::new(socket.compat());
|
||||
let (driver, mut handle) = session.split();
|
||||
|
||||
// Spawn the session driver to run in the background
|
||||
let driver_task = tokio::spawn(driver);
|
||||
|
||||
// Create verifier config with Mozilla root certificates for TLS verification
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.protocol_config_validator(config_validator)
|
||||
.root_store(RootCertStore::mozilla())
|
||||
.build()
|
||||
.unwrap();
|
||||
.map_err(|e| eyre!("Failed to build verifier config: {}", e))?;
|
||||
|
||||
info!("verifier_config: {:?}", verifier_config);
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
let verifier = handle
|
||||
.new_verifier(verifier_config)
|
||||
.map_err(|e| eyre!("Failed to create verifier: {}", e))?;
|
||||
|
||||
info!("Starting verification");
|
||||
info!("Starting TLS commitment protocol");
|
||||
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
// Run the commitment protocol
|
||||
let verifier = verifier
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| eyre!("Commitment failed: {}", e))?;
|
||||
|
||||
// Check the proposed configuration
|
||||
let request = verifier.request();
|
||||
let TlsCommitProtocolConfig::Mpc(mpc_config) = request.protocol() else {
|
||||
return Err(eyre!("Only MPC protocol is supported"));
|
||||
};
|
||||
|
||||
// Validate the proposed configuration
|
||||
if mpc_config.max_sent_data() > max_sent_data {
|
||||
return Err(eyre!(
|
||||
"Prover requested max_sent_data {} exceeds limit {}",
|
||||
mpc_config.max_sent_data(),
|
||||
max_sent_data
|
||||
));
|
||||
}
|
||||
if mpc_config.max_recv_data() > max_recv_data {
|
||||
return Err(eyre!(
|
||||
"Prover requested max_recv_data {} exceeds limit {}",
|
||||
mpc_config.max_recv_data(),
|
||||
max_recv_data
|
||||
));
|
||||
}
|
||||
|
||||
info!(
|
||||
"Accepting TLS commitment with max_sent={}, max_recv={}",
|
||||
mpc_config.max_sent_data(),
|
||||
mpc_config.max_recv_data()
|
||||
);
|
||||
|
||||
// Accept and run the commitment protocol
|
||||
let verifier = verifier
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| eyre!("Accept failed: {}", e))?
|
||||
.run()
|
||||
.await
|
||||
.map_err(|e| eyre!("Run failed: {}", e))?;
|
||||
|
||||
info!("TLS connection complete, starting verification");
|
||||
|
||||
// Verify the proof
|
||||
let verifier = verifier
|
||||
.verify()
|
||||
.await
|
||||
.map_err(|e| eyre!("Verification failed: {}", e))?;
|
||||
|
||||
let (
|
||||
VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
},
|
||||
verifier,
|
||||
) = verifier
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| eyre!("Accept verification failed: {}", e))?;
|
||||
|
||||
// Close the verifier
|
||||
verifier
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to close verifier: {}", e))?;
|
||||
|
||||
// Close the session handle
|
||||
handle.close();
|
||||
|
||||
// Wait for the driver to complete
|
||||
driver_task
|
||||
.await
|
||||
.map_err(|e| eyre!("Driver task failed: {}", e))?
|
||||
.map_err(|e| eyre!("Session driver error: {}", e))?;
|
||||
|
||||
info!("verify() returned successfully - prover sent all data");
|
||||
|
||||
let server_name =
|
||||
|
||||
Reference in New Issue
Block a user