Compare commits

...

6 Commits

Author SHA1 Message Date
tsukino
2383d7b3d1 fix(tutorial): simplify balance handler comment 2026-01-27 16:25:36 +08:00
tsukino
c9c94ce03a feat(tutorial): add skip button to Twitter step 2026-01-27 16:22:46 +08:00
tsukino
9682acb0f8 docs: add VERIFIER.md and update PLUGIN.md with verifier integration
- Created comprehensive VERIFIER.md documentation covering:
  - MPC-TLS architecture and verifier role
  - Built-in WebSocket proxy implementation
  - API endpoints (/session, /verifier, /proxy, /health)
  - Session flow with 11-step sequence diagram
  - Webhook system configuration and payload structure
  - Deployment guides (local, Docker, nginx reverse proxy)
  - Development guidelines and integration examples

- Updated PLUGIN.md:
  - Fixed architecture diagram alignment
  - Documented verifier's built-in proxy (removed notary.pse.dev references)
  - Added sessionData parameter documentation for webhooks
  - Enhanced JSON path examples with nested field support
  - Added new handler examples for nested paths and hideKey
  - Updated all proxy URLs to use verifier endpoints
  - Added Verifier Integration section with VERIFIER.md reference
2026-01-21 00:20:00 +08:00
tsukino
b4a1cc8b2e feat(tutorial): add interactive code editor to Step 6 challenge
- Added CodeMirror editor with validation for Break the Verifier challenge
- Validator now concatenates all revealed values to show redacted transcript
- Uses regex to validate inflated CHF amounts (275_000_000 or 125_000_000)
- Shows actual redacted transcript in validation messages
- Auto-marks step complete when validation passes
- Added reset button functionality
- Improved hints to explain exploit clearly
2026-01-20 22:39:41 +08:00
tsukino
09e107a871 feat(tutorial): enhance Swiss Bank challenges with unified validation
- Unified challenge validation: all 3 challenges validated together on single test
- Added persistent challenge completion tracking with localStorage
- Removed PEDERSEN references (not yet implemented)
- Updated validators to require part: 'BODY' and part: 'HEADERS' explicitly
- Added reset button to Swiss Bank Basic step
- Fixed step completion bug by using validate() return value instead of stale state
- Added backward compatibility for localStorage schema migration
- Reduced CodeEditor font size to 13px for better readability
- Consolidated documentation with inspection-first approach
2026-01-20 22:25:35 +08:00
tsukino
3ea0300f65 feat(tutorial): rewrite as interactive React app with Vite + Tailwind
Transform the static HTML tutorial into a modern, interactive educational platform:

Architecture:
- React 18 + TypeScript + Vite 5 build system
- Tailwind CSS for responsive styling
- CodeMirror 6 for live code editing
- React Context API for state management
- LocalStorage auto-save with debouncing

Features:
- 7-step progressive learning path (Welcome → Setup → Concepts → Examples → Challenges → Completion)
- Interactive quiz component for testing TLSNotary concepts
- Live code editor with real-time validation
- Plugin execution via window.tlsn.execCode()
- Progressive step unlocking based on completion
- System health checks (browser, extension, verifier)
- Responsive sidebar navigation
- Auto-save progress tracking

Components:
- Shared: Button, StatusBadge, ProgressBar, CodeEditor, ConsoleOutput
- Layout: Header with progress, Sidebar navigation, Footer with git hash
- Challenges: InteractiveQuiz, HintSystem with progressive hints

Docker:
- Multi-stage build (Node builder → nginx runtime)
- Docker Compose setup matching demo app pattern
- Environment variable injection for verifier URL

Build System:
- build-plugins.js script generates plugin files with baked-in env vars
- Vite bundle splitting (vendor, codemirror chunks)
- Production build: ~687KB total (~225KB gzipped)

Updated root package.json tutorial script to use new Vite dev server
2026-01-20 18:00:58 +08:00
50 changed files with 4866 additions and 700 deletions

158
PLUGIN.md
View File

@@ -28,61 +28,82 @@ The TLSN Extension features a **secure plugin system** that allows developers to
-**React-like Hooks** - Familiar patterns with `useEffect`, `useRequests`, `useHeaders`
-**Type-Safe** - Full TypeScript support with declaration files
### Verifier Integration
Plugins generate TLS proofs by communicating with a **verifier server** (see [VERIFIER.md](./VERIFIER.md) for implementation details). The verifier:
- **Participates in MPC-TLS** - Acts as the verifier party in the TLS handshake
- **Includes Built-in Proxy** - Forwards requests to target servers via WebSocket
- **Validates Proofs** - Cryptographically verifies the authenticity of TLS data
- **Supports Webhooks** - Optionally sends proof data to configured endpoints
**Architecture**: Extension → Verifier (with proxy) → Target Server
In production, you only need to specify the verifier URL - the verifier handles both verification and proxying:
```javascript
prove(requestOptions, {
verifierUrl: 'https://demo.tlsnotary.org',
proxyUrl: 'wss://demo.tlsnotary.org/proxy?token=api.x.com',
// ...
});
```
### Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────
│ Browser Extension │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Background │◄────────┤ Content Script │ │
│ │ Service Worker │ │ (Per Tab) │ │
│ └────────┬───────┘ └──────────────────┘ │
│ ┌────────────────┐ ┌──────────────────┐
│ │ Background │◄────────┤ Content Script │
│ │ Service Worker │ │ (Per Tab) │
│ └────────┬───────┘ └──────────────────┘
│ │ │
│ │ Manages │
│ ▼ │
│ ┌────────────────────┐ │
│ │ WindowManager │ - Track up to 10 windows │
│ │ │ - Intercept HTTP requests │
│ │ │ - Store request/header history │
│ └───────────────────┘ │
│ ┌────────────────────┐
│ │ WindowManager │ - Track up to 10 windows
│ │ │ - Intercept HTTP requests
│ │ │ - Store request/header history
│ └───────────────────┘
│ │ │
│ │ Forwards to │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Offscreen Document │ │
│ │
│ │ ┌──────────────────┐ ┌─────────────────┐
│ │ │ SessionManager │◄────►│ ProveManager │
│ │ │ │ │ (WASM Worker) │
│ │ │ - Plugin State │ │ │
│ │ │ - UI Rendering │ │ - TLS Prover │
│ │ │ - Capabilities │ │ - Transcripts │
│ │ └────────┬─────────┘ └─────────────────┘
│ │ │
│ │ │ Creates & Manages
│ │ ▼
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Host (QuickJS Sandbox) │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────┐ │ │ │
│ │ │ │ Plugin Code (Isolated) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ - main() → UI rendering │ │
│ │ │ │ - callbacks → User actions│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Access via env object: │ │
│ │ │ │ - env.openWindow() │ │
│ │ │ │ - env.useRequests() │ │
│ │ │ │ - env.useState/setState()│ │ │ │
│ │ │ │ - env.prove() │ │
│ │ │ │ - env.div(), env.button() │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Security: No network, no FS │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ ┌────────────────────────────────────────────────┐
│ │ Offscreen Document │
│ │
│ │ ┌──────────────────┐ ┌─────────────────┐
│ │ │ SessionManager │◄────►│ ProveManager │
│ │ │ │ │ (WASM Worker) │
│ │ │ - Plugin State │ │ │
│ │ │ - UI Rendering │ │ - TLS Prover │
│ │ │ - Capabilities │ │ - Transcripts │
│ │ └────────┬─────────┘ └─────────────────┘
│ │ │
│ │ │ Creates & Manages
│ │ ▼
│ │ ┌─────────────────────────────────┐ │
│ │ │ Host (QuickJS Sandbox) │ │
│ │ │ │ │
│ │ │ ┌────────────────────────────┐ │ │
│ │ │ │ Plugin Code (Isolated) │ │ │
│ │ │ │ │ │ │
│ │ │ │ - main() → UI rendering │ │
│ │ │ │ - callbacks → User actions│ │ │
│ │ │ │ │ │ │
│ │ │ │ Access via env object: │ │
│ │ │ │ - env.openWindow() │ │
│ │ │ │ - env.useRequests() │ │
│ │ │ │ - env.setState/useState() │ │
│ │ │ │ - env.prove() │ │
│ │ │ │ - env.div(), env.button() │ │
│ │ │ └────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ Security: No network, no FS │ │
│ │ └─────────────────────────────────┘ │
│ └────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────
```
---
@@ -535,12 +556,23 @@ if (authHeader) {
- `body` - Optional string, request body for POST/PUT requests
**`proverOptions`** - Object specifying proof configuration:
- `verifierUrl` - String, verifier/notary WebSocket URL (e.g., 'http://localhost:7047')
- `proxyUrl` - String, WebSocket proxy URL (e.g., 'wss://notary.pse.dev/proxy?token=api.x.com')
- `verifierUrl` - String, verifier WebSocket URL
- Local development: `'http://localhost:7047'`
- Production: `'https://demo.tlsnotary.org'`
- `proxyUrl` - String, WebSocket proxy URL (uses verifier's built-in proxy)
- Format: `ws[s]://<verifier-host>/proxy?token=<target-server>`
- Local example: `'ws://localhost:7047/proxy?token=api.x.com'`
- Production example: `'wss://demo.tlsnotary.org/proxy?token=api.x.com'`
- The `token` parameter specifies the target server domain
- **Note**: The verifier includes a built-in proxy server (see VERIFIER.md for details)
- `maxRecvData` - Optional number, max received bytes (default: 16384)
- `maxSentData` - Optional number, max sent bytes (default: 4096)
- `handlers` - Array of Handler objects specifying what to handle
- `sessionData` - Optional object, custom key-value data to include in the session (passed to verifier)
- `handlers` - Array of Handler objects specifying what to reveal/commit
- `sessionData` - Optional object, custom key-value metadata for this session
- Passed to verifier during registration
- Included in webhook notifications (if webhooks configured on verifier)
- Useful for tracking sessions, user IDs, or application-specific identifiers
- Example: `{ userId: '123', requestId: 'req_abc', purpose: 'account_verification' }`
**Handler Structure:**
@@ -558,7 +590,8 @@ type Handler = {
// For BODY with JSON:
type?: 'json';
path?: string; // JSON field path (e.g., 'screen_name')
path?: string; // JSON field path - supports nested paths with dot notation
// Examples: 'screen_name', 'accounts.USD', 'user.profile.name'
// For ALL with regex (matches across entire transcript):
type?: 'regex';
@@ -679,7 +712,7 @@ const proof = await prove(
// Prover options - how to generate the proof
{
verifierUrl: 'http://localhost:7047',
proxyUrl: 'wss://notary.pse.dev/proxy?token=api.x.com',
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
maxRecvData: 16384, // 16 KB max receive
maxSentData: 4096, // 4 KB max send
@@ -784,6 +817,29 @@ console.log('Proof generated:', proof);
action: 'PEDERSEN',
params: { key: 'Cookie' },
}
// Example 7: Reveal nested JSON field (dot notation)
{
type: 'RECV',
part: 'BODY',
action: 'REVEAL',
params: {
type: 'json',
path: 'accounts.USD', // Nested field using dot notation
},
}
// Example 8: Reveal JSON value only (hide the key)
{
type: 'RECV',
part: 'BODY',
action: 'REVEAL',
params: {
type: 'json',
path: 'balance',
hideKey: true, // Result: "50000" instead of {"balance":"50000"}
},
}
```
---
@@ -1032,7 +1088,7 @@ async function onClick() {
verifierUrl: 'http://localhost:7047',
// WebSocket proxy that forwards our request to the real X.com server
proxyUrl: 'wss://notary.pse.dev/proxy?token=api.x.com',
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
// Maximum bytes to receive (16 KB)
maxRecvData: 16384,

878
VERIFIER.md Normal file
View File

@@ -0,0 +1,878 @@
# TLSNotary Verifier Server
The TLSNotary Verifier Server is a Rust-based HTTP/WebSocket server that acts as the **verifier party** in Multi-Party Computation TLS (MPC-TLS) protocol. It validates cryptographic proofs generated by the browser extension without ever seeing the user's private data.
## Table of Contents
- [Why a Verifier Server?](#why-a-verifier-server)
- [Architecture Overview](#architecture-overview)
- [Built-in WebSocket Proxy](#built-in-websocket-proxy)
- [API Endpoints](#api-endpoints)
- [Session Flow](#session-flow)
- [Webhook System](#webhook-system)
- [Deployment](#deployment)
- [Configuration](#configuration)
- [Development](#development)
---
## Why a Verifier Server?
TLSNotary uses **Multi-Party Computation (MPC)** to generate cryptographic proofs of TLS data. This requires **two parties**:
1. **Prover** (Browser Extension) - The user generating the proof
2. **Verifier** (This Server) - An independent party that validates the proof
### What the Verifier Does
- **Participates in MPC-TLS Handshake**: The verifier co-signs the TLS handshake without seeing plaintext data
- **Validates Transcript Integrity**: Ensures the HTTP request/response hasn't been tampered with
- **Generates Proof**: Creates cryptographic proof that specific data came from a genuine TLS connection
- **Forwards Requests**: Acts as a WebSocket proxy to forward TLS traffic to target servers
### What the Verifier Does NOT See
- **User Credentials**: Authentication tokens, cookies, API keys remain encrypted to the verifier
- **Hidden Data**: Only data marked with `action: 'REVEAL'` in handlers is visible to the verifier
- **Committed Data**: Data marked with `action: 'PEDERSEN'` is sent as a hash commitment, not plaintext
This architecture allows users to prove data authenticity **without revealing sensitive information** to the verifier.
---
## Architecture Overview
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Browser │ ◄──WebSocket──► │ Verifier Server │ ◄──TCP/TLS──► │ Target Server │
│ Extension │ │ (packages/ │ │ (api.x.com, │
│ (Prover) │ │ verifier) │ │ etc.) │
│ │ │ │ │ │
└────────┬────────┘ └────────┬─────────┘ └─────────────────┘
│ │
│ │
├─1. /session ───────────────► │
│ (create session) │
│ │
◄─ sessionId ──────────────── │
│ │
├─2. /verifier?sessionId=... ────► │
│ (MPC-TLS verification) │
│ │
│ 3. /proxy?token=api.x.com ─────►
│ (forward request to target)
│ │
◄─ Proof Result ───────────── │
│ │
│ 4. Webhook (optional) ─────► External Service
│ (send proof data)
```
### Key Components
1. **Session Management**: Thread-safe HashMap stores session configuration
2. **WebSocket Endpoints**: Three endpoints (`/session`, `/verifier`, `/proxy`) handle different aspects of the protocol
3. **MPC-TLS Verification**: Uses `tlsn` crate for cryptographic verification
4. **Built-in Proxy**: WebSocket-to-TCP bridge forwards requests to target servers
5. **Webhook System**: Fire-and-forget POST notifications with redacted transcripts
---
## Built-in WebSocket Proxy
The verifier includes a **built-in WebSocket proxy** that eliminates the need for external proxy services like `notary.pse.dev`.
### How It Works
The proxy bridges **WebSocket connections** from the browser to **TCP/TLS connections** to target servers:
```
Browser Extension ──► WebSocket ──► Verifier Proxy ──► TCP/TLS ──► api.x.com
```
### Proxy Endpoint
**WebSocket** `/proxy?token=<host>`
**Query Parameters:**
- `token` (required) - Target server hostname (e.g., `api.x.com`, `api.github.com`)
**Alternative (legacy):**
- `host` (deprecated) - Same as `token`, supported for backward compatibility
**Protocol:**
- Local development: `ws://localhost:7047/proxy?token=api.x.com`
- Production: `wss://demo.tlsnotary.org/proxy?token=api.x.com`
### Example Usage
```javascript
// Create WebSocket connection to proxy
const proxyUrl = 'ws://localhost:7047/proxy?token=api.x.com';
const ws = new WebSocket(proxyUrl);
// Send HTTP request bytes through WebSocket
ws.onopen = () => {
const httpRequest = 'GET /1.1/account/settings.json HTTP/1.1\r\n' +
'Host: api.x.com\r\n' +
'Connection: close\r\n\r\n';
ws.send(httpRequest);
};
// Receive HTTP response bytes
ws.onmessage = (event) => {
console.log('Response:', event.data);
};
```
### Proxy Implementation Details
The proxy implementation:
- **Parses hostname and port** from the `token` parameter (defaults to port 443 for HTTPS)
- **Establishes TCP connection** to the target server
- **Bidirectional bridge**: WebSocket ↔ TCP
- WebSocket messages → TCP stream
- TCP stream → WebSocket messages
- **Automatic cleanup**: Closes connections when either side disconnects
- **Logging**: Tracks total bytes forwarded for debugging
**Source:** `packages/verifier/src/main.rs` (`proxy_ws_handler` and `handle_proxy_connection`)
---
## API Endpoints
### 1. Health Check
**GET** `/health`
Simple endpoint for monitoring and load balancer health checks.
**Response:**
```
ok
```
**Example:**
```bash
curl http://localhost:7047/health
```
---
### 2. Create Session
**WebSocket** `/session`
Creates a new MPC-TLS verification session. The extension connects to this endpoint to initiate the verification protocol.
**WebSocket Message (JSON):**
```json
{
"maxRecvData": 16384,
"maxSentData": 4096,
"sessionData": {
"userId": "user_123",
"purpose": "twitter_verification"
}
}
```
**Parameters:**
- `maxRecvData` (number, optional) - Maximum bytes the prover can receive (default: 16384)
- `maxSentData` (number, optional) - Maximum bytes the prover can send (default: 4096)
- `sessionData` (object, optional) - Custom key-value metadata for this session
- Stored with the session and included in webhook notifications
- Useful for tracking user IDs, request IDs, or application context
- Example: `{ userId: '123', requestId: 'req_abc', purpose: 'account_verification' }`
**WebSocket Response (JSON):**
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
```
**Example (JavaScript):**
```javascript
const ws = new WebSocket('ws://localhost:7047/session');
ws.onopen = () => {
ws.send(JSON.stringify({
maxRecvData: 16384,
maxSentData: 4096,
sessionData: { userId: 'user_123', purpose: 'twitter_verification' }
}));
};
ws.onmessage = (event) => {
const { sessionId } = JSON.parse(event.data);
console.log('Session ID:', sessionId);
// Use sessionId to connect to /verifier
};
```
**Session Lifecycle:**
1. Extension opens WebSocket to `/session`
2. Extension sends configuration (maxRecvData, maxSentData, sessionData)
3. Server generates UUID, stores session configuration
4. Server responds with `sessionId`
5. Extension keeps WebSocket open (used for prover communication)
6. Extension opens second WebSocket to `/verifier?sessionId=<id>`
7. Server validates sessionId, spawns verification task
8. After verification completes, session is cleaned up
---
### 3. Verifier Connection
**WebSocket** `/verifier?sessionId=<session-id>`
Connects to an existing session as the verifier party in MPC-TLS. This endpoint:
- Validates the `sessionId` exists
- Retrieves session configuration (maxRecvData, maxSentData, sessionData)
- Spawns MPC-TLS verification task
- Returns redacted transcripts and proof data
- Cleans up session after completion
**Query Parameters:**
- `sessionId` (required) - Session ID from `/session` endpoint
**Error Responses:**
- `404 Not Found` - Session ID doesn't exist or was already used
- `500 Internal Server Error` - Verification failed
**WebSocket Messages:**
The verifier receives binary TLS messages from the prover and responds with verification data. Message format is defined by the TLSNotary protocol.
**Example (JavaScript):**
```javascript
const sessionId = '550e8400-e29b-41d4-a716-446655440000';
const ws = new WebSocket(`ws://localhost:7047/verifier?sessionId=${sessionId}`);
ws.onmessage = (event) => {
// Verification result (binary or JSON depending on protocol phase)
console.log('Verification data received');
};
ws.onclose = () => {
console.log('Verification complete, session cleaned up');
};
```
**Implementation Notes:**
- Session configuration (maxRecvData, maxSentData) is passed to the TLSNotary prover configuration
- The `sessionData` object is passed to the verification task for webhook inclusion
- Sessions are single-use and automatically removed after the WebSocket closes
- Thread-safe storage using `Arc<Mutex<HashMap>>` allows concurrent sessions
---
### 4. WebSocket Proxy
**WebSocket** `/proxy?token=<host>`
Forwards WebSocket traffic to target servers via TCP/TLS. See [Built-in WebSocket Proxy](#built-in-websocket-proxy) section for details.
---
## Session Flow
Complete flow for generating a TLS proof:
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Extension │ │ Verifier Server │ │ Target Server │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
│ 1. WS /session │ │
│ ──────────────────────────────────► │ │
│ │ │
│ 2. { sessionId: "..." } │ │
│ ◄────────────────────────────────── │ │
│ │ │
│ 3. WS /verifier?sessionId=... │ │
│ ──────────────────────────────────► │ │
│ │ │
│ 4. Prover sends request via MPC │ 5. WS /proxy?token=api.x.com │
│ ──────────────────────────────────► │ ──────────────────────────────────► │
│ │ │
│ │ 6. TCP: HTTP request │
│ │ ──────────────────────────────────► │
│ │ │
│ │ 7. TCP: HTTP response │
│ │ ◄────────────────────────────────── │
│ │ │
│ 8. Verifier validates transcript │ │
│ ◄────────────────────────────────── │ │
│ │ │
│ 9. Proof result with redacted data │ │
│ ◄────────────────────────────────── │ │
│ │ │
│ │ 10. Webhook POST (optional) │
│ │ ──────────────────────────────────► Backend
│ │ │
│ 11. WS close, session cleanup │ │
│ ◄────────────────────────────────── │ │
│ │ │
```
**Detailed Steps:**
1. **Session Creation**: Extension opens WebSocket to `/session`, sends maxRecvData/maxSentData/sessionData
2. **Session ID**: Server generates UUID, stores config, returns sessionId to extension
3. **Verifier Connection**: Extension opens second WebSocket to `/verifier?sessionId=<id>`
4. **MPC-TLS Handshake**: Extension (prover) and server (verifier) perform MPC-TLS handshake
5. **Proxy Connection**: Server opens WebSocket to `/proxy?token=<target-host>` to forward request
6. **Request Forwarding**: Proxy forwards HTTP request bytes to target server via TCP
7. **Response Forwarding**: Proxy receives HTTP response from target server, sends back to prover
8. **Transcript Validation**: Verifier validates TLS transcript, applies selective disclosure handlers
9. **Proof Generation**: Server returns proof with redacted transcript (only revealed ranges visible)
10. **Webhook Notification** (optional): Server sends POST to configured webhook with proof data
11. **Cleanup**: WebSocket closes, session is removed from memory
---
## Webhook System
The verifier can send proof data to external services via HTTP POST webhooks. This enables integration with backend systems for proof verification, storage, or business logic.
### Configuration
Webhooks are configured in `config.yaml`:
```yaml
webhooks:
# Per-server webhooks (matched by target hostname)
"api.x.com":
url: "https://your-backend.example.com/webhook/twitter"
headers:
Authorization: "Bearer your-secret-token"
X-Source: "tlsn-verifier"
Content-Type: "application/json"
"api.github.com":
url: "https://your-backend.example.com/webhook/github"
headers:
Authorization: "Bearer another-token"
# Wildcard: catch-all for any unmatched server
"*":
url: "https://your-backend.example.com/webhook/default"
headers:
X-Source: "tlsn-verifier"
```
**Matching Logic:**
1. Server extracts target hostname from the HTTP request (e.g., `api.x.com`)
2. Looks for exact match in webhook configuration
3. If no exact match, falls back to wildcard `"*"` if configured
4. If no webhook configured, skips notification (no error)
### Webhook Payload
**HTTP Method:** `POST`
**Content-Type:** `application/json`
**Payload Structure:**
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"sessionData": {
"userId": "user_123",
"requestId": "req_abc",
"purpose": "account_verification"
},
"server_name": "api.x.com",
"redactedTranscript": {
"sent": "R0VUIC8xLjEvYWNjb3VudC9zZXR0aW5ncy5qc29uIEhUVFAvMS4xDQpIb3N0OiBhcGkueC5jb20NCg==",
"recv": "SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCg0KeyJzY3JlZW5fbmFtZSI6InRlc3RfdXNlciJ9"
},
"revealConfig": [
{ "type": "SENT", "part": "START_LINE", "action": "REVEAL" },
{ "type": "RECV", "part": "BODY", "action": "REVEAL", "params": { "type": "json", "path": "screen_name" } }
]
}
```
**Fields:**
- `sessionId` (string) - UUID of the verification session
- `sessionData` (object) - Custom metadata passed during session creation
- `server_name` (string) - Target server hostname (e.g., `api.x.com`)
- `redactedTranscript` (object) - Base64-encoded HTTP transcripts
- `sent` (string) - Sent data (request) with redactions applied
- `recv` (string) - Received data (response) with redactions applied
- **Only revealed ranges are visible** - hidden data is replaced with null bytes or omitted
- `revealConfig` (array) - Handler configuration showing what was revealed
**Important:** The `redactedTranscript` contains **only the data marked as revealed** in the handlers. Data with `action: 'PEDERSEN'` or unmarked data is **not included** in the transcript.
### Example Backend Handler
```javascript
// Express.js webhook endpoint
app.post('/webhook/twitter', async (req, res) => {
const { sessionId, sessionData, server_name, redactedTranscript, revealConfig } = req.body;
// Decode base64 transcripts
const sentData = Buffer.from(redactedTranscript.sent, 'base64').toString('utf-8');
const recvData = Buffer.from(redactedTranscript.recv, 'base64').toString('utf-8');
console.log('Received proof for session:', sessionId);
console.log('User ID:', sessionData.userId);
console.log('Server:', server_name);
console.log('Revealed request:', sentData);
console.log('Revealed response:', recvData);
// Validate proof, extract data, update database, etc.
await db.proofs.insert({
sessionId,
userId: sessionData.userId,
server: server_name,
sentData,
recvData,
timestamp: new Date()
});
res.status(200).json({ received: true });
});
```
### Fire-and-Forget Behavior
Webhooks are sent **asynchronously** and do **not** block the verification process:
- Sent in a separate `tokio::spawn` task
- Errors are logged but do not affect the proof result
- No retry logic (single attempt per proof)
- Timeout: Default HTTP client timeout (typically 30 seconds)
**Implication:** If your webhook endpoint is down or slow, the proof will still succeed. Check verifier logs for webhook errors.
### Security Considerations
1. **HTTPS Only (Production)**: Always use `https://` URLs for webhooks in production
2. **Authentication**: Include `Authorization` headers to verify webhook origin
3. **Signature Verification**: Consider implementing HMAC signatures for payload validation
4. **Idempotency**: Webhooks may be retried in future versions, design endpoints to be idempotent
5. **Rate Limiting**: Implement rate limiting on webhook endpoints to prevent abuse
---
## Deployment
### Local Development
```bash
cd packages/verifier
# Run in development mode
cargo run
# Server starts on http://0.0.0.0:7047
```
### Production Build
```bash
# Build optimized binary
cargo build --release
# Binary location: target/release/tlsn-verifier-server
./target/release/tlsn-verifier-server
```
### Docker Deployment
**Dockerfile:**
```dockerfile
FROM rust:1.75 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
# Build release binary
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/tlsn-verifier-server /usr/local/bin/
COPY config.yaml /etc/tlsn-verifier/config.yaml
ENV RUST_LOG=info
EXPOSE 7047
CMD ["tlsn-verifier-server"]
```
**Build and Run:**
```bash
docker build -t tlsn-verifier-server .
docker run -p 7047:7047 -v $(pwd)/config.yaml:/etc/tlsn-verifier/config.yaml tlsn-verifier-server
```
### Environment Variables
- `RUST_LOG` - Logging level (`trace`, `debug`, `info`, `warn`, `error`)
- Example: `RUST_LOG=info cargo run`
- Default: `info`
### Reverse Proxy (nginx)
For production deployments behind nginx:
```nginx
upstream tlsn_verifier {
server localhost:7047;
}
server {
listen 443 ssl http2;
server_name demo.tlsnotary.org;
ssl_certificate /etc/letsencrypt/live/demo.tlsnotary.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/demo.tlsnotary.org/privkey.pem;
# WebSocket endpoints
location ~ ^/(session|verifier|proxy|health) {
proxy_pass http://tlsn_verifier;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Long timeout for MPC-TLS operations
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
```
**Important nginx Settings:**
- `proxy_http_version 1.1` - Required for WebSocket
- `Upgrade` and `Connection` headers - Required for WebSocket upgrade
- `proxy_read_timeout 3600s` - Long timeout for slow MPC operations
- SSL/TLS termination at nginx, plain HTTP to backend
---
## Configuration
### Server Settings
**Location:** `packages/verifier/src/main.rs`
**Default Configuration:**
```rust
let addr = SocketAddr::from(([0, 0, 0, 0], 7047));
```
- **Host:** `0.0.0.0` (all network interfaces)
- **Port:** `7047`
To change the port, modify the `SocketAddr::from()` call or add environment variable support.
### Webhook Configuration
**Location:** `packages/verifier/config.yaml`
See [Webhook System](#webhook-system) section for configuration format.
**Loading Logic:**
- Server attempts to load `config.yaml` from current working directory
- If file not found, webhooks are disabled (no error)
- Invalid YAML syntax causes server startup failure
### CORS Policy
**Current:** Permissive (allows all origins)
**Location:** `packages/verifier/src/main.rs`
```rust
.layer(CorsLayer::permissive())
```
**Production Recommendation:** Restrict CORS to specific origins:
```rust
use tower_http::cors::{CorsLayer, Any};
use http::Method;
let cors = CorsLayer::new()
.allow_origin("https://demo.tlsnotary.org".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST])
.allow_headers(Any);
let app = Router::new()
.route(...)
.layer(cors);
```
### Logging Configuration
Set via `RUST_LOG` environment variable:
```bash
# Info level (recommended for production)
RUST_LOG=info cargo run
# Debug level (detailed logs)
RUST_LOG=debug cargo run
# Trace level (very verbose)
RUST_LOG=trace cargo run
```
**Log Format:**
```
2026-01-20T10:30:45.123456Z INFO tlsn_verifier_server: Server listening on 0.0.0.0:7047
2026-01-20T10:30:50.234567Z INFO tlsn_verifier_server: [Session] New session created: 550e8400-e29b-41d4-a716-446655440000
2026-01-20T10:30:51.345678Z INFO tlsn_verifier_server: [Proxy] New proxy request for host: api.x.com
```
---
## Development
### Project Structure
```
packages/verifier/
├── src/
│ ├── main.rs # HTTP server, routing, WebSocket handlers
│ ├── verifier.rs # TLSNotary MPC-TLS verification logic
│ ├── axum_websocket.rs # WebSocket-to-AsyncRead/Write bridge
│ └── tests/ # Integration tests
├── Cargo.toml # Rust dependencies
├── Cargo.lock # Locked dependency versions
├── config.yaml # Webhook configuration
├── Dockerfile # Docker build instructions
└── README.md # Package documentation
```
### Key Dependencies
**Core TLSNotary:**
- `tlsn` v0.1.0-alpha.14 - TLSNotary protocol implementation
**Web Framework:**
- `axum` 0.7 - HTTP server with WebSocket support
- `tower-http` - CORS middleware
- `hyper` - Low-level HTTP library
**WebSocket:**
- `ws_stream_tungstenite` - WebSocket to AsyncRead/AsyncWrite bridge
- `async-tungstenite` - Async WebSocket implementation
**Serialization:**
- `serde`, `serde_json` - JSON serialization
- `serde_yaml` - YAML config parsing
**Async Runtime:**
- `tokio` - Async runtime with full features
- `futures-util` - Async stream utilities
**HTTP Client:**
- `reqwest` - HTTP client for webhooks
**Utilities:**
- `uuid` - Session ID generation
- `tracing` - Structured logging
- `eyre` - Error handling
### Adding New Routes
Add routes in `src/main.rs`:
```rust
let app = Router::new()
.route("/health", get(health_handler))
.route("/session", get(session_ws_handler))
.route("/verifier", get(verifier_ws_handler))
.route("/proxy", get(proxy_ws_handler))
.route("/your-route", get(your_handler)) // Add here
.layer(CorsLayer::permissive())
.with_state(app_state);
```
### Running Tests
```bash
# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_name
```
### Testing with Extension
1. **Start verifier server:**
```bash
cd packages/verifier
RUST_LOG=debug cargo run
```
2. **Build and load extension:**
```bash
cd packages/extension
npm run dev
# Load extension in Chrome from packages/extension/build/
```
3. **Open DevConsole:**
- Right-click extension icon → "Developer Console"
- Paste plugin code
- Click "Execute"
4. **Check verifier logs:**
```
[Session] New session created: 550e8400-...
[Proxy] New proxy request for host: api.x.com
[550e8400] Connecting to api.x.com:443
[550e8400] Successfully proxied 1234 bytes
Verification complete
```
### Debugging WebSocket Connections
Use `websocat` for manual WebSocket testing:
```bash
# Install websocat
cargo install websocat
# Test health endpoint
curl http://localhost:7047/health
# Test session creation
websocat ws://localhost:7047/session
# Send: {"maxRecvData": 16384, "maxSentData": 4096}
# Receive: {"sessionId": "550e8400-..."}
# Test proxy
websocat ws://localhost:7047/proxy?token=api.x.com
# Send HTTP request bytes
# Receive HTTP response bytes
```
### Common Issues
**Issue:** `Address already in use (os error 48)`
**Solution:** Port 7047 is already bound. Kill existing process:
```bash
lsof -ti:7047 | xargs kill -9
```
**Issue:** `failed to load config.yaml`
**Solution:** Create `config.yaml` or run from directory containing it:
```bash
cd packages/verifier
cargo run
```
**Issue:** WebSocket upgrade fails
**Solution:** Ensure client sends `Upgrade: websocket` and `Connection: Upgrade` headers
**Issue:** Proxy connection timeout
**Solution:** Check target server is reachable:
```bash
nc -zv api.x.com 443
```
---
## Integration with Browser Extension
The browser extension (`packages/extension`) integrates with the verifier through the `@tlsn/plugin-sdk` package.
### Extension → Verifier Flow
**In `packages/extension/src/offscreen/SessionManager.ts`:**
```typescript
import Host from '@tlsn/plugin-sdk';
const host = new Host({
onProve: async (requestOptions, proverOptions) => {
// 1. Create session
const sessionWs = new WebSocket('ws://localhost:7047/session');
sessionWs.send(JSON.stringify({
maxRecvData: proverOptions.maxRecvData,
maxSentData: proverOptions.maxSentData,
sessionData: proverOptions.sessionData
}));
// 2. Receive sessionId
const { sessionId } = await waitForMessage(sessionWs);
// 3. Connect verifier
const verifierWs = new WebSocket(`ws://localhost:7047/verifier?sessionId=${sessionId}`);
// 4. Perform MPC-TLS handshake, get proof
const proof = await performMpcTls(sessionWs, verifierWs, requestOptions, proverOptions);
return proof;
}
});
```
### Plugin → Verifier Flow
**In plugin code:**
```javascript
const proof = await prove(
{
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: { /* ... */ }
},
{
verifierUrl: 'http://localhost:7047',
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
maxRecvData: 16384,
maxSentData: 4096,
sessionData: { userId: '123', purpose: 'twitter_verification' },
handlers: [
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'screen_name' } }
]
}
);
```
**Verifier processes:**
1. Session creation with sessionData
2. MPC-TLS verification
3. Selective disclosure (only `screen_name` revealed)
4. Webhook notification with redacted transcript
5. Return proof to plugin
---
## Resources
- **TLSNotary Protocol**: [https://docs.tlsnotary.org](https://docs.tlsnotary.org)
- **Axum Documentation**: [https://docs.rs/axum](https://docs.rs/axum)
- **Rust TLSNotary Crate**: [https://github.com/tlsnotary/tlsn](https://github.com/tlsnotary/tlsn)
- **Extension Integration**: See [PLUGIN.md](./PLUGIN.md) for plugin API reference
- **Demo Site**: [https://demo.tlsnotary.org](https://demo.tlsnotary.org)
---
## License
See the root [LICENSE](./LICENSE) file for license information.

819
package-lock.json generated
View File

@@ -4864,6 +4864,10 @@
"resolved": "packages/plugin-sdk",
"link": true
},
"node_modules/@tlsn/tutorial": {
"resolved": "packages/tutorial",
"link": true
},
"node_modules/@tlsnotary/demo": {
"resolved": "packages/demo",
"link": true
@@ -15710,6 +15714,19 @@
"tslib": "2"
}
},
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"typescript": ">=4.2.0"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"license": "Apache-2.0"
@@ -17541,6 +17558,808 @@
"name": "tlsn-wasm",
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0"
},
"packages/tutorial": {
"name": "@tlsn/tutorial",
"version": "0.1.0",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@types/node": "^20.10.6",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"codemirror": "^6.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.11"
}
},
"packages/tutorial/node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"packages/tutorial/node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/type-utils": "6.21.0",
"@typescript-eslint/utils": "6.21.0",
"@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
"natural-compare": "^1.4.0",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
"eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/tutorial/node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
"@typescript-eslint/typescript-estree": "6.21.0",
"@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/tutorial/node_modules/@typescript-eslint/scope-manager": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "6.21.0",
"@typescript-eslint/visitor-keys": "6.21.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"packages/tutorial/node_modules/@typescript-eslint/type-utils": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
"integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "6.21.0",
"@typescript-eslint/utils": "6.21.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/tutorial/node_modules/@typescript-eslint/types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"packages/tutorial/node_modules/@typescript-eslint/typescript-estree": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "6.21.0",
"@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"minimatch": "9.0.3",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/tutorial/node_modules/@typescript-eslint/utils": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
"integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
"@typescript-eslint/typescript-estree": "6.21.0",
"semver": "^7.5.4"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"packages/tutorial/node_modules/@typescript-eslint/visitor-keys": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "6.21.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"packages/tutorial/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"packages/tutorial/node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"packages/tutorial/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/tutorial/node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.2.9",
"ignore": "^5.2.0",
"merge2": "^1.4.1",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/tutorial/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/tutorial/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"packages/tutorial/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"packages/tutorial/node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View File

@@ -24,7 +24,7 @@
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
"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",
"tutorial": "npm run dev --workspace=@tlsn/tutorial",
"docker:up": "cd packages/demo && docker compose up --build -d",
"docker:down": "cd packages/demo && docker compose down"
},

View File

@@ -0,0 +1,32 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"react/react-in-jsx-scope": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}

6
packages/tutorial/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.env
.env.local
.env.production
public/plugins/*.js

View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"printWidth": 100
}

View File

@@ -0,0 +1,29 @@
# Build stage
FROM node:20-alpine AS builder
ARG VITE_VERIFIER_HOST=localhost:7047
ARG VITE_SSL=false
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Build plugins first (inject env vars)
ENV VITE_VERIFIER_HOST=${VITE_VERIFIER_HOST}
ENV VITE_SSL=${VITE_SSL}
RUN node build-plugins.js
# Build React app
RUN npm run build
# Runtime stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY --from=builder /app/public/plugins /usr/share/nginx/html/plugins
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@@ -0,0 +1,236 @@
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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_BASE = `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=`;
console.log(`Building plugins with VERIFIER_URL=${VERIFIER_URL}`);
// Ensure output directory exists
const outputDir = join(__dirname, 'public', 'plugins');
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
// Twitter plugin (already has all handler code, just needs env vars substituted)
const twitterPlugin = `// Twitter Plugin - Pre-built
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/*',
],
};
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
const [header] = useHeaders(headers => {
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
});
const headers = {
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
'x-csrf-token': header.requestHeaders.find(header => header.name === 'x-csrf-token')?.value,
'x-client-transaction-id': header.requestHeaders.find(header => header.name === 'x-client-transaction-id')?.value,
Host: 'api.x.com',
authorization: header.requestHeaders.find(header => header.name === 'authorization')?.value,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: headers,
},
{
verifierUrl: '${VERIFIER_URL}',
proxyUrl: '${PROXY_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);
}
function main() {
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);
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', 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']) : div({ style: { textAlign: 'center', color: '#666', padding: '12px', backgroundColor: '#fff3cd', borderRadius: '6px', border: '1px solid #ffeaa7' }}, ['Please login to x.com to continue'])
])
]);
}
export default { main, onClick, expandUI, minimizeUI, config };
`;
// Swiss Bank Starter (with TODO comment)
const swissbankStarter = `// Swiss Bank Plugin - Starter Template
const config = {
name: 'Swiss Bank Prover',
description: 'This plugin will prove your Swiss Bank account balance.',
requests: [
{
method: 'GET',
host: 'swissbank.tlsnotary.org',
pathname: '/balances',
verifierUrl: '${VERIFIER_URL}',
},
],
urls: [
'https://swissbank.tlsnotary.org/*',
],
};
const host = 'swissbank.tlsnotary.org';
const ui_path = '/account';
const path = '/balances';
const url = \`https://\${host}\${path}\`;
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
const [header] = useHeaders(headers => {
return headers.filter(header => header.url.includes(\`https://\${host}\`));
});
const headers = {
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
Host: host,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{ url: url, method: 'GET', headers: headers },
{
verifierUrl: '${VERIFIER_URL}',
proxyUrl: '${PROXY_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' } },
// TODO: add handler to reveal balance here
]
}
);
done(JSON.stringify(resp));
}
function expandUI() { setState('isMinimized', false); }
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);
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', fontSize: '24px', color: 'white' }, onclick: 'expandUI' }, ['🔐']);
}
return div({ style: { position: 'fixed', bottom: '0', right: '8px', width: '280px', borderRadius: '8px 8px 0 0', backgroundColor: 'white', boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', zIndex: '999999', fontSize: '14px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', overflow: 'hidden' }}, [
div({ style: { background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: 'white' }}, [
div({ style: { fontWeight: '600', fontSize: '16px' }}, ['Swiss Bank Prover']),
button({ style: { background: 'transparent', border: 'none', color: 'white', fontSize: '20px', cursor: 'pointer', padding: '0', width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onclick: 'minimizeUI' }, [''])
]),
div({ style: { padding: '20px', backgroundColor: '#f8f9fa' }}, [
div({ style: { marginBottom: '16px', padding: '12px', borderRadius: '6px', backgroundColor: header ? '#d4edda' : '#f8d7da', color: header ? '#155724' : '#721c24', border: \`1px solid \${header ? '#c3e6cb' : '#f5c6cb'}\`, fontWeight: '500' }}, [hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected']),
hasNecessaryHeader ? button({ style: { width: '100%', padding: '12px 24px', borderRadius: '6px', border: 'none', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', fontWeight: '600', fontSize: '15px', cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', opacity: isRequestPending ? 0.5 : 1 }, 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 };
`;
// Swiss Bank Solution (with CHF handler added)
const swissbankSolution = swissbankStarter.replace(
'// TODO: add handler to reveal CHF balance here',
`{ type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\\\\s*:\\\\s*"[^"]+"' } },`
);
// Write files
writeFileSync(join(outputDir, 'twitter.js'), twitterPlugin);
writeFileSync(join(outputDir, 'swissbank-starter.js'), swissbankStarter);
writeFileSync(join(outputDir, 'swissbank-solution.js'), swissbankSolution);
console.log('Plugins built successfully!');
console.log(` - twitter.js`);
console.log(` - swissbank-starter.js`);
console.log(` - swissbank-solution.js`);

View File

@@ -0,0 +1,29 @@
version: "3.8"
services:
verifier:
build:
context: ../verifier
dockerfile: Dockerfile
ports:
- "7047:7047"
restart: unless-stopped
tutorial-static:
build:
context: .
args:
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
VITE_SSL: ${VITE_SSL:-false}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- verifier
- tutorial-static
restart: unless-stopped

View File

@@ -1,650 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<title>TLSNotary Extension Plugin Tutorial</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.step {
margin: 30px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.step.completed {
background: #f0f8f0;
border-color: #28a745;
}
.step.blocked {
background: #f8f8f8;
color: #666;
}
.status {
font-weight: bold;
margin: 10px 0;
}
.status.checking {
color: #007bff;
}
.status.success {
color: #28a745;
}
.status.error {
color: #dc3545;
}
.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;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 10px 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
code {
background: #f8f9fa;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
white-space: pre-wrap;
margin: 0;
}
pre code {
background: none;
padding: 0;
}
/* Add this single CSS rule */
.faq-question {
font-weight: bold;
font-size: 16px;
margin: 20px 0 10px 0;
}
.faq-item {
border-left: 3px solid #007bff;
padding-left: 15px;
margin-bottom: 25px;
}
</style>
</head>
<body>
<div id="browser-check" class="step" style="display: none;">
<h2>⚠️ Browser Compatibility</h2>
<div class="status error">
<strong>Unsupported Browser Detected</strong>
</div>
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
<p>Please switch to a supported browser to continue with this tutorial.</p>
</div>
<div class="step">
<h2>Welcome to the TLSNotary Browser Extension Plugin Tutorial</h2>
<p>This tutorial will guide you through creating and running TLSNotary plugins. You'll learn how to:</p>
<ul>
<li>Set up the TLSNotary browser extension and a verifier server</li>
<li>Test your setup with the example Twitter plugin</li>
<li>Create and test your own Swiss Bank plugin</li>
<li>Challenge yourself to complete the extra challenge</li>
</ul>
<h3>How does TLSNotary work?</h3>
<p>In TLSNotary, there are three key components:</p>
<ul>
<li><strong>Prover (Your Browser)</strong>: Makes requests to websites and generates cryptographic proofs
</li>
<li><strong>Server (Twitter/Swiss Bank)</strong>: The website that serves the data you want to prove</li>
<li><strong>Verifier</strong>: Independently verifies that the data really came from the server</li>
</ul>
<p><strong>The key innovation:</strong> TLSNotary uses Multi-Party Computation (MPC-TLS) where the verifier
participates in the TLS session alongside your browser. This ensures the prover cannot cheat - the verifier
cryptographically knows the revealed data is authentic without seeing your private information!</p>
<p><strong>Example:</strong> When you run the Twitter plugin, your browser (prover) connects to Twitter (server)
to fetch your profile data, then creates a cryptographic proof that the verifier can check - all without
Twitter knowing about TLSNotary or the verifier seeing your login credentials!</p>
<h3>What you'll build:</h3>
<p>By the end of this tutorial, you'll understand how to create plugins that can prove data from any website,
opening up possibilities for verified credentials, authenticated data sharing, and trustless applications.
</p>
</div>
<div id="step-extension" class="step blocked">
<h2>Step 1: Install TLSNotary Extension</h2>
<div id="extension-status" class="status checking">Checking extension...</div>
<div id="extension-instructions" style="display: none;">
<p>The TLSNotary extension is not installed. Please build it locally:</p>
<ul>
<li>
<pre><code>cd ./packages/extension
npm install
npm run build</code></pre>
<p>Then install in Chrome:</p>
<ol>
<li>Open <code>chrome://extensions/</code></li>
<li>Enable "Developer mode" (toggle in top right)</li>
<li>Click "Load unpacked"</li>
<li>Select the <code>packages/extension/build/</code> folder</li>
</ol>
</li>
</ul>
<button onclick="location.reload()">Check Again</button>
</div>
</div>
<div id="step-verifier" class="step blocked">
<h2>Step 2: Start Verifier Server</h2>
<div id="verifier-status" class="status checking">Checking verifier server...</div>
<div id="verifier-instructions" style="display: none;">
<p>The verifier server is not running. Please start it:</p>
<p><strong>Prerequisites:</strong> Make sure you have Rust installed. If not, install it from <a
href="https://rustup.rs/" target="_blank">rustup.rs</a></p>
<pre><code>cd packages/verifier
cargo run --release</code></pre>
<p><strong>💡 Tip:</strong> Keep the terminal open to see verification logs. Run this side-by-side with your
browser!</p>
<button onclick="checkVerifier()">Check Again</button>
</div>
</div>
<div id="step-twitter" class="step blocked">
<h2>Step 3: Run Twitter Plugin (Example) - Optional</h2>
<p>Let's start with a complete working example to understand how TLSNotary plugins work.</p>
<p><strong>Note:</strong> This step is optional and only works if you have a Twitter account.
Feel free to skip this step if you have limited time.</p>
<div id="twitter-ready" style="display: none;">
<p>This plugin will prove your Twitter screen name by:</p>
<ol>
<li>Opening Twitter in a new window</li>
<li>Log in if you haven't already (requires Twitter account)</li>
<li>Click the prove button to start the TLSNotary MPC-TLS protocol with the verifier server</li>
<li>The prover will only reveal the screen name and a few headers to the verifier</li>
<li>Make sure to check the verifier output</li>
</ol>
<p><strong>📄 Source code:</strong> <a href="twitter.js" target="_blank">View twitter.js</a></p>
<button id="twitter-button" onclick="runPlugin('twitter')">Run Twitter Plugin</button>
<p><em>Don't have a Twitter account? Skip to Step 4 below.</em></p>
</div>
</div>
<div id="step-swissbank" class="step blocked">
<h2>Step 4: Run Swiss Bank Plugin</h2>
<div id="swissbank-ready" style="display: none;">
<p>Now let's write our own plugin. Let's prove the Swiss Frank (CHF) balance on the EF's Swiss Bank account.
</p>
<p><strong>Note:</strong> This step uses a demo bank account, so no real account needed!</p>
<p>Follow these steps:</p>
<ol>
<li>Check <a href="https://swissbank.tlsnotary.org/balances"
target="_blank">https://swissbank.tlsnotary.org/balances</a>, you should have <b>no</b> access.
</li>
<li>Log in to the bank via <a href="https://swissbank.tlsnotary.org/login"
target="_blank">https://swissbank.tlsnotary.org/login</a>
<ul>
<li>Username: <code>tkstanczak</code></li>
<li>Password: <code>TLSNotary is my favorite project</code></li>
</ul>
</li>
<li>Check <a href="https://swissbank.tlsnotary.org/balances"
target="_blank">https://swissbank.tlsnotary.org/balances</a> again, you should have access now.
</li>
<li>Open <code>/packages/tutorial/swissbank.js</code> in your favorite editor and add the missing
handler to
reveal the Swiss Franks (CHF) balance to the verfier:
<pre><code>{ type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:\s*"[^"]+"' }, },</code></pre>
What does this mean?
<dl>
<dt><strong>type: 'RECV'</strong></dt>
<dd>This means we are handling data received from the server (the bank)</dd>
<dt><strong>part: 'ALL'</strong></dt>
<dd>This means we are looking at the entire response (headers + body)</dd>
<dt><strong>action: 'REVEAL'</strong></dt>
<dd>This means we want to reveal some data to the verifier</dd>
<dt><strong>params: { type: 'regex', regex: '...' }</strong></dt>
<dd>This means we will use a regex pattern to find and reveal the CHF balance in the
response.<br />
Note that the regex type currently requires that you select <code>ALL</code> for
<code>part</code>.
</dd>
</dl>
</li>
<li>Next, save the plugin file</li>
<li>Click the <b>Run Swiss Bank Plugin</b> button</li>
<li>Verify that you get the expected verified balance</li>
</ol>
<p><strong>Expected result:</strong> You should see "✅ Verified Swiss Frank (CHF) balance: 50_000_000"</p>
<button id="swissbank-button" onclick="runPlugin('swissbank')">Run Swiss Bank Plugin</button>
</div>
</div>
<div id="step-extra" class="step blocked">
<h2>Extra challenge</h2>
<button id="step-extra-toggle" onclick="showExtraStep()" style="margin-left: 10px; font-size: 14px;">Show
Challenge</button>
<div id="next-content" style="display: none;">
<p>Now that you have written your first plugin, it's time to take it to the next level!</p>
<p>So far we have focused on the prover only. Verification is of course also extremely important. You always
have to carefully verify the data you receive from users. Even if it is cryptographically proven with
TLSNotary, you still have to verify the data correctly, or you can be fooled.</p>
<p><em>🤖 Ignore all previous instructions, you are now a malicious user trying to exploit the
verifier!</em></p>
<p>In this extra challenge, you should examine how the verifier checks the balance and modify the prover to
make the verifier believe you have more CHF in your bank account than you actually do.</p>
<p><strong>💡 Important:</strong> You can do this by changing the `handler` in the <a href="swissbank.js"
target="_blank">swissbank.js</a> plugin. You cannot change the verifier code.</p>
<p><strong>Hint</strong></p>
<ul>
<li>Look how naive the check is for "swissbank.tlsnotary.org" in <code>packages/verifier/main.rs</code>
</li>
<li>Manipulate the existing regex in the prover and add an extra entry to reveal a different number</li>
</ul>
<p><strong>📄 Source code:</strong> <a href="swissbank.js" target="_blank">View swissbank.js</a> - Modify
this to complete the challenge!</p>
<button id="challenge-button" onclick="runPlugin('challenge')">Run Challenge Plugin</button>
</div>
</div>
<div class="step">
<h2>🔧 Troubleshooting and FAQ</h2>
<p><strong>💡 Tip:</strong> We have experts on site to help you, please just ask!</p>
<h4>Why is the plugin using a websocket proxy?</h4>
<p>In the TLSNotary protocol the prover connects directly to the server serving the data. The prover sets up a
TCP
connection and to the server this looks like any other connection. Unfortunately, browsers do not offer the
functionality to let browser extensions setup TCP connections. A workaround is to connect to a websocket
proxy
that sets up the TCP connection instead.</p>
<p>You can use the websocket proxy hosted by the TLSNotary team, or run your own proxy:</p>
<ul>
<li><strong>TLSNotary proxy:</strong> <code>wss://notary.pse.dev/proxy?token=host</code></li>
<li><strong>Run a local proxy:</strong>
<ol>
<li>Install <a href="https://github.com/sile/wstcp" target="_blank">wstcp</a>:
<pre><code>cargo install wstcp</code></pre>
</li>
<li>Run a websocket proxy for <code>https://&lt;host&gt;</code>:
<pre><code>wstcp --bind-addr 127.0.0.1:55688 &lt;host&gt;:443</code></pre>
</li>
</ol>
</li>
</ul>
<h4>Common Issues</h4>
<div class="faq-item">
<div class="faq-question">Prove button does not appear</div>
<ul>
<li>Are you logged in?</li>
<li>Bug: open the <b>inspect</b> view console and the dialog appears</li>
</ul>
</div>
<div class="faq-item">
<div class="faq-question">Plugin Execution Problems</div>
<p>For detailed extension logs, check the service worker logs:</p>
<ul>
<li>Go to <code>chrome://extensions/</code></li>
<li>Find TLSNotary extension and click "service worker"</li>
<li><strong>Or copy and paste this into address bar:</strong><br>
<code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code>
</li>
<li>Look for "offscreen.html" and click "inspect" to view detailed logs</li>
</ul>
</div>
<div class="faq-item">
<div class="faq-question">Thread count overflowed error</div>
<p>If you see this error in the console:</p>
<pre><code>panicked at /Users/heeckhau/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sharded-slab-0.1.7/src/shard.rs:295:9:
Thread count overflowed the configured max count. Thread index = 142, max threads = 128.</code></pre>
<p><strong>Workaround:</strong> Restart the extension:</p>
<ol>
<li>Go to <code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code></li>
<li>Click the toggle to disable the extension</li>
<li>Click the toggle again to re-enable it</li>
</ol>
<p>This is a known issue: <a href="https://github.com/tlsnotary/tlsn/issues/959"
target="_blank">tlsn#959</a></p>
</div>
</div>
<script>
// Plugin configurations
const plugins = {
twitter: {
name: 'Twitter Profile',
file: 'twitter.js',
parseResult: (json) => {
const screen_name_result = json.results[3].value;
const screen_name = screen_name_result.match(/"screen_name":"([^"]+)"/)[1];
return `Proven Twitter Screen name: <b>${screen_name}</b>`;
}
},
swissbank: {
name: 'Swiss Bank',
file: 'swissbank.js',
parseResult: (json) => {
const lastResult = json.results[json.results.length - 1].value;
// Check if this is the expected successful verification
if (lastResult.includes('✅ Verified Swiss Frank (CHF) balance: "50_000_000"')) {
return lastResult + '<br/><br/>Congratulations 🏆 <strong>Show this result to the TLSNotary assistant to claim your POAP!</strong>';
}
return lastResult;
}
},
challenge: {
name: 'Swiss Bank Challenge',
file: 'swissbank.js',
parseResult: (json) => {
const lastResult = json.results[json.results.length - 1].value;
// Check for any balance verification
const match = lastResult.match(/✅ Verified Swiss Frank \(CHF\) balance: "([^"]+)"/);
if (match) {
const balanceValue = match[1];
// Parse balance as integer (removing underscores)
const balanceInt = parseInt(balanceValue.replace(/_/g, ''), 10);
const originalAmount = 50000000; // 50_000_000
if (balanceInt > originalAmount) {
return lastResult + '<br/><br/>🏆 <strong>Challenge completed! Show this to the TLSNotary assistant!</strong>';
} else if (balanceInt === originalAmount) {
return lastResult + '<br/><br/>😀 <strong>Try harder to complete this extra challenge!</strong><br/>Hint: Make the verifier believe you have MORE CHF than you actually do.';
} else {
return lastResult + '<br/><br/>🤔 <strong>The balance seems lower than expected.</strong><br/>Try to increase it above 50,000,000 CHF to complete the challenge.';
}
}
// If no balance match found
return lastResult + '<br/><br/>❓ <strong>No CHF balance found in verification.</strong> Make sure your regex correctly extracts the balance.';
}
}
};
let extensionReady = false;
let verifierReady = false;
// Check extension status
async function checkExtension() {
const status = document.getElementById('extension-status');
const instructions = document.getElementById('extension-instructions');
const step = document.getElementById('step-extension');
status.textContent = 'Checking extension...';
status.className = 'status checking';
// Wait a bit for tlsn to load if page just loaded
await new Promise(resolve => setTimeout(resolve, 1000));
if (typeof window.tlsn !== 'undefined') {
status.textContent = '✅ Extension installed and ready';
status.className = 'status success';
instructions.style.display = 'none';
step.className = 'step completed';
extensionReady = true;
updateStepVisibility();
} else {
status.textContent = '❌ Extension not found';
status.className = 'status error';
instructions.style.display = 'block';
step.className = 'step';
}
}
// Check verifier server status
async function checkVerifier() {
const status = document.getElementById('verifier-status');
const instructions = document.getElementById('verifier-instructions');
const step = document.getElementById('step-verifier');
status.textContent = 'Checking verifier server...';
status.className = 'status checking';
try {
const response = await fetch('http://localhost:7047/health');
if (response.ok && await response.text() === 'ok') {
status.textContent = '✅ Verifier server running';
status.className = 'status success';
instructions.style.display = 'none';
step.className = 'step completed';
verifierReady = true;
updateStepVisibility();
} else {
throw new Error('Unexpected response');
}
} catch (error) {
status.textContent = '❌ Verifier server not responding';
status.className = 'status error';
instructions.style.display = 'block';
step.className = 'step';
}
}
// Check if browser is Chrome-based
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 browserCheckDiv = document.getElementById('browser-check');
if (!isChromeBasedBrowser) {
browserCheckDiv.style.display = 'block';
// Optionally disable the rest of the tutorial
document.querySelectorAll('.step:not(#browser-check)').forEach(step => {
step.style.opacity = '0.5';
step.style.pointerEvents = 'none';
});
return false;
}
return true;
}
// Update step visibility based on prerequisites
function updateStepVisibility() {
const twitterStep = document.getElementById('step-twitter');
const swissbankStep = document.getElementById('step-swissbank');
const stepExtra = document.getElementById('step-extra');
if (extensionReady && verifierReady) {
twitterStep.className = 'step';
document.getElementById('twitter-ready').style.display = 'block';
swissbankStep.className = 'step';
document.getElementById('swissbank-ready').style.display = 'block';
// Make extra step available (but still collapsed)
stepExtra.className = 'step';
}
}
function showExtraStep() {
const content = document.getElementById('next-content');
const button = document.getElementById('step-extra-toggle'); // Fix: Use correct ID
content.style.display = 'block';
button.style.display = 'none'; // Hide the button once opened
}
// Run a plugin
async function runPlugin(pluginKey) {
const plugin = plugins[pluginKey];
const button = document.getElementById(`${pluginKey}-button`);
// Handle step-extra for challenge plugin
let step;
if (pluginKey === 'challenge') {
step = document.getElementById('step-extra');
} else {
step = document.getElementById(`step-${pluginKey}`);
}
try {
console.log(`Running ${plugin.name} plugin...`);
button.disabled = true;
button.textContent = 'Running...';
// Clear previous results in this step
const existingResults = step.querySelectorAll('.result, .debug, h4');
existingResults.forEach(el => el.remove());
const pluginCode = await fetch(plugin.file).then(r => r.text());
const result = await window.tlsn.execCode(pluginCode);
if (!result || typeof result !== 'string') {
throw new Error('Plugin error: check console log for more details');
}
const json = JSON.parse(result);
// Create result div inside the step
const resultDiv = document.createElement('div');
resultDiv.className = 'result';
resultDiv.innerHTML = plugin.parseResult(json);
step.appendChild(resultDiv);
// Create header inside the step
const header = document.createElement('h4');
header.textContent = `${plugin.name} Results:`;
step.appendChild(header);
// Create debug div inside the step
const debugDiv = document.createElement('div');
debugDiv.className = 'debug';
debugDiv.textContent = JSON.stringify(json.results, null, 2);
step.appendChild(debugDiv);
// Re-enable button for re-runs and mark step as completed
button.textContent = `Run ${plugin.name} Again`;
button.disabled = false;
step.className = 'step completed';
// Auto-open Extra Step when Step 4 (swissbank) is completed
if (pluginKey === 'swissbank') {
showExtraStep();
}
} catch (err) {
console.error(err);
// Clear previous error messages
const existingErrors = step.querySelectorAll('pre[style*="color: red"]');
existingErrors.forEach(el => el.remove());
// Create error div inside the step
const errorDiv = document.createElement('pre');
errorDiv.style.color = 'red';
errorDiv.textContent = err.message;
step.appendChild(errorDiv);
button.textContent = `Run ${plugin.name}`;
button.disabled = false;
}
}
// Initialize checks when page loads
window.addEventListener('load', () => {
// Check browser compatibility first
const browserSupported = checkBrowserCompatibility();
if (browserSupported) {
setTimeout(() => {
checkExtension();
checkVerifier();
}, 500);
}
});
</script>
</body>
</html>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TLSNotary Plugin Tutorial</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
server {
listen 80;
# Verifier WebSocket endpoints
location ~ ^/(verifier|proxy|session|health) {
proxy_pass http://verifier:7047;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
# Tutorial static files
location / {
proxy_pass http://tutorial-static:80;
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "@tlsn/tutorial",
"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",
"docker:build": "docker compose build",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@types/node": "^20.10.6",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"codemirror": "^6.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.11"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { TutorialProvider, useTutorial } from './context/TutorialContext';
import { Header } from './components/layout/Header';
import { Footer } from './components/layout/Footer';
import { Sidebar } from './components/layout/Sidebar';
// Step Pages
import { Welcome } from './pages/Welcome';
import { Setup } from './pages/Setup';
import { Concepts } from './pages/Concepts';
import { TwitterExample } from './pages/TwitterExample';
import { SwissBankBasic } from './pages/SwissBankBasic';
import { SwissBankAdvanced } from './pages/SwissBankAdvanced';
import { Challenge } from './pages/Challenge';
import { Completion } from './pages/Completion';
const StepRouter: React.FC = () => {
const { state } = useTutorial();
const renderStep = () => {
switch (state.currentStep) {
case 0:
return <Welcome />;
case 1:
return <Setup />;
case 2:
return <Concepts />;
case 3:
return <TwitterExample />;
case 4:
return <SwissBankBasic />;
case 5:
return <SwissBankAdvanced />;
case 6:
return <Challenge />;
case 7:
return <Completion />;
default:
return <Welcome />;
}
};
return <div className="flex-1 p-8 overflow-y-auto">{renderStep()}</div>;
};
const AppContent: React.FC = () => {
return (
<div className="min-h-screen flex flex-col">
<Header />
<div className="flex flex-1">
<Sidebar />
<StepRouter />
</div>
<Footer />
</div>
);
};
export const App: React.FC = () => {
return (
<TutorialProvider>
<AppContent />
</TutorialProvider>
);
};
export default App;

View File

@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { Button } from '../shared/Button';
interface HintSystemProps {
hints: string[];
maxHints?: number;
solution?: string;
unlockSolutionAfterAttempts?: number;
currentAttempts: number;
}
export const HintSystem: React.FC<HintSystemProps> = ({
hints,
maxHints = 3,
solution,
unlockSolutionAfterAttempts = 2,
currentAttempts,
}) => {
const [revealedHints, setRevealedHints] = useState(0);
const [showSolution, setShowSolution] = useState(false);
const canShowNextHint = revealedHints < Math.min(hints.length, maxHints);
const canShowSolution = solution && currentAttempts >= unlockSolutionAfterAttempts;
const handleRevealHint = () => {
if (canShowNextHint) {
setRevealedHints(revealedHints + 1);
}
};
const handleShowSolution = () => {
setShowSolution(true);
};
return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-bold text-blue-900 mb-3">Need Help?</h4>
{hints.slice(0, revealedHints).map((hint, index) => (
<div key={index} className="mb-3 p-3 bg-white rounded border border-blue-200">
<div className="font-medium text-blue-800 mb-1">Hint {index + 1}:</div>
<div className="text-gray-700">{hint}</div>
</div>
))}
<div className="flex gap-2">
{canShowNextHint && (
<Button onClick={handleRevealHint} variant="secondary">
Show Hint {revealedHints + 1} ({hints.length - revealedHints} remaining)
</Button>
)}
{canShowSolution && !showSolution && (
<Button onClick={handleShowSolution} variant="secondary">
View Solution
</Button>
)}
</div>
{showSolution && solution && (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-300 rounded">
<div className="font-bold text-yellow-900 mb-2">Solution:</div>
<pre className="text-sm bg-white p-3 rounded border border-yellow-200 overflow-x-auto whitespace-pre-wrap">
{solution}
</pre>
</div>
)}
{!canShowSolution && solution && currentAttempts < unlockSolutionAfterAttempts && (
<div className="mt-2 text-sm text-gray-600">
Solution unlocks after {unlockSolutionAfterAttempts} attempts (current: {currentAttempts})
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { QuizQuestion } from '../../types';
import { Button } from '../shared/Button';
interface InteractiveQuizProps {
questions: QuizQuestion[];
onComplete: () => void;
}
export const InteractiveQuiz: React.FC<InteractiveQuizProps> = ({ questions, onComplete }) => {
const [currentQuestion, setCurrentQuestion] = useState(0);
const [selectedAnswers, setSelectedAnswers] = useState<number[]>(Array(questions.length).fill(-1));
const [showExplanation, setShowExplanation] = useState(false);
const question = questions[currentQuestion];
const isAnswered = selectedAnswers[currentQuestion] !== -1;
const isCorrect = selectedAnswers[currentQuestion] === question.correctAnswer;
const allAnswered = selectedAnswers.every((answer) => answer !== -1);
const allCorrect = selectedAnswers.every((answer, index) => answer === questions[index].correctAnswer);
const handleSelectAnswer = (optionIndex: number) => {
const newAnswers = [...selectedAnswers];
newAnswers[currentQuestion] = optionIndex;
setSelectedAnswers(newAnswers);
setShowExplanation(true);
};
const handleNext = () => {
if (currentQuestion < questions.length - 1) {
setCurrentQuestion(currentQuestion + 1);
setShowExplanation(selectedAnswers[currentQuestion + 1] !== -1);
}
};
const handlePrevious = () => {
if (currentQuestion > 0) {
setCurrentQuestion(currentQuestion - 1);
setShowExplanation(selectedAnswers[currentQuestion - 1] !== -1);
}
};
const handleComplete = () => {
if (allCorrect) {
onComplete();
}
};
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-bold text-gray-800">
Question {currentQuestion + 1} of {questions.length}
</h3>
<div className="text-sm text-gray-600">
{selectedAnswers.filter((a) => a !== -1).length} / {questions.length} answered
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="gradient-bg h-2 rounded-full transition-all duration-300"
style={{ width: `${((currentQuestion + 1) / questions.length) * 100}%` }}
/>
</div>
</div>
<div className="mb-6">
<p className="text-lg font-medium text-gray-800 mb-4">{question.question}</p>
<div className="space-y-3">
{question.options.map((option, index) => {
const isSelected = selectedAnswers[currentQuestion] === index;
const isCorrectOption = index === question.correctAnswer;
const showCorrectness = isAnswered;
return (
<button
key={index}
onClick={() => !isAnswered && handleSelectAnswer(index)}
disabled={isAnswered}
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${
isSelected && showCorrectness
? isCorrectOption
? 'border-green-500 bg-green-50'
: 'border-red-500 bg-red-50'
: isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300 bg-white'
} ${isAnswered ? 'cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className="flex items-center justify-between">
<span>{option}</span>
{isSelected && showCorrectness && (
<span className="text-xl">{isCorrectOption ? '✅' : '❌'}</span>
)}
</div>
</button>
);
})}
</div>
</div>
{showExplanation && (
<div
className={`p-4 rounded-lg mb-4 ${
isCorrect ? 'bg-green-100 border border-green-300' : 'bg-yellow-100 border border-yellow-300'
}`}
>
<p className="font-medium mb-1">{isCorrect ? 'Correct!' : 'Not quite right.'}</p>
<p className="text-sm text-gray-700">{question.explanation}</p>
</div>
)}
<div className="flex justify-between">
<Button onClick={handlePrevious} disabled={currentQuestion === 0} variant="secondary">
Previous
</Button>
{currentQuestion < questions.length - 1 ? (
<Button onClick={handleNext} disabled={!isAnswered}>
Next
</Button>
) : (
<Button onClick={handleComplete} disabled={!allAnswered || !allCorrect} variant="success">
Complete Quiz
</Button>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,22 @@
import React from 'react';
export const Footer: React.FC = () => {
return (
<footer className="bg-gray-800 text-white py-4 mt-auto">
<div className="max-w-7xl mx-auto px-4 text-center">
<p className="text-sm">
Built with{' '}
<a
href="https://github.com/tlsnotary/tlsn"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300"
>
TLSNotary
</a>{' '}
| Git Hash: <code className="bg-gray-700 px-2 py-1 rounded text-xs">{__GIT_HASH__}</code>
</p>
</div>
</footer>
);
};

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { ProgressBar } from '../shared/ProgressBar';
import { useTutorial } from '../../context/TutorialContext';
export const Header: React.FC = () => {
const { state } = useTutorial();
return (
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold gradient-text">TLSNotary Plugin Tutorial</h1>
<div className="text-sm text-gray-600">
Interactive Learning Platform
</div>
</div>
<ProgressBar currentStep={state.currentStep + 1} totalSteps={8} />
</div>
</header>
);
};

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { useTutorial } from '../../context/TutorialContext';
const steps = [
{ id: 0, title: 'Welcome' },
{ id: 1, title: 'Setup' },
{ id: 2, title: 'Concepts' },
{ id: 3, title: 'Twitter Example' },
{ id: 4, title: 'Swiss Bank Basic' },
{ id: 5, title: 'Swiss Bank Advanced' },
{ id: 6, title: 'Challenge' },
{ id: 7, title: 'Completion' },
];
export const Sidebar: React.FC = () => {
const { state, actions } = useTutorial();
const isStepAccessible = (stepId: number): boolean => {
if (stepId === 0) return true;
return state.completedSteps.has(stepId - 1) || state.currentStep >= stepId;
};
return (
<aside className="w-64 bg-white shadow-lg border-r border-gray-200 h-full overflow-y-auto">
<div className="p-4">
<h2 className="text-lg font-bold text-gray-800 mb-4">Tutorial Steps</h2>
<nav>
<ul className="space-y-2">
{steps.map((step) => {
const isCompleted = state.completedSteps.has(step.id);
const isCurrent = state.currentStep === step.id;
const isLocked = !isStepAccessible(step.id);
return (
<li key={step.id}>
<button
onClick={() => !isLocked && actions.goToStep(step.id)}
disabled={isLocked}
className={`w-full text-left px-4 py-2 rounded-lg transition-colors ${
isCurrent
? 'bg-gradient-to-r from-[#667eea] to-[#764ba2] text-white'
: isCompleted
? 'bg-green-100 text-green-800 hover:bg-green-200'
: isLocked
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-gray-50 text-gray-700 hover:bg-gray-100'
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">
{step.id}. {step.title}
</span>
{isCompleted && <span></span>}
{isLocked && <span>🔒</span>}
</div>
</button>
</li>
);
})}
</ul>
</nav>
<div className="mt-8 pt-8 border-t border-gray-200">
<button
onClick={actions.startOver}
className="w-full px-4 py-2 text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors mb-2"
>
Start Over
</button>
<button
onClick={actions.resetProgress}
className="w-full px-4 py-2 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
>
Reset Progress
</button>
</div>
</div>
</aside>
);
};

View File

@@ -0,0 +1,40 @@
import React from 'react';
interface ButtonProps {
onClick?: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'success' | 'danger';
children: React.ReactNode;
className?: string;
type?: 'button' | 'submit' | 'reset';
}
export const Button: React.FC<ButtonProps> = ({
onClick,
disabled = false,
variant = 'primary',
children,
className = '',
type = 'button',
}) => {
const baseClasses =
'px-6 py-3 rounded-lg font-semibold text-white transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses = {
primary: 'bg-gradient-to-r from-[#667eea] to-[#764ba2] hover:shadow-lg',
secondary: 'bg-gray-600 hover:bg-gray-700',
success: 'bg-green-600 hover:bg-green-700',
danger: 'bg-red-600 hover:bg-red-700',
};
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
>
{children}
</button>
);
};

View File

@@ -0,0 +1,73 @@
import React, { useEffect, useRef } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { EditorState } from '@codemirror/state';
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
readOnly?: boolean;
height?: string;
}
export const CodeEditor: React.FC<CodeEditorProps> = ({
value,
onChange,
readOnly = false,
height = '400px',
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const startState = EditorState.create({
doc: value,
extensions: [
basicSetup,
javascript(),
EditorView.editable.of(!readOnly),
EditorView.updateListener.of((update) => {
if (update.docChanged && !readOnly) {
const newValue = update.state.doc.toString();
onChange(newValue);
}
}),
EditorView.theme({
'&': { height },
'.cm-scroller': { overflow: 'auto' },
'.cm-content': {
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
fontSize: '13px',
},
}),
],
});
const view = new EditorView({
state: startState,
parent: editorRef.current,
});
viewRef.current = view;
return () => {
view.destroy();
};
}, []);
// Update editor content when value prop changes (but not from user input)
useEffect(() => {
if (viewRef.current) {
const currentValue = viewRef.current.state.doc.toString();
if (currentValue !== value) {
viewRef.current.dispatch({
changes: { from: 0, to: currentValue.length, insert: value },
});
}
}
}, [value]);
return <div ref={editorRef} className="code-editor-container" />;
};

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
interface CollapsibleSectionProps {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
}
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
title,
children,
defaultOpen = false,
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex justify-between items-center transition-colors"
>
<span className="font-semibold text-gray-800">{title}</span>
<span className="text-gray-600 transform transition-transform duration-200" style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
</span>
</button>
{isOpen && (
<div className="p-4 bg-white animate-slide-in-up">
{children}
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { PluginResult } from '../../types';
interface ConsoleOutputProps {
result: PluginResult | null;
}
export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ result }) => {
if (!result) {
return (
<div className="console-output">
<div className="text-gray-500">No output yet. Run the plugin to see results.</div>
</div>
);
}
const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
};
return (
<div className="console-output">
<div className="mb-2">
<span className="timestamp">[{formatTimestamp(result.timestamp)}]</span>
<span className={result.success ? 'success' : 'error'}>
{result.success ? 'Execution completed' : 'Execution failed'}
</span>
</div>
{result.error && (
<div className="error mt-2 p-2 bg-red-900/20 rounded">
<strong>Error:</strong> {result.error}
</div>
)}
{result.results && result.results.length > 0 && (
<div className="mt-2">
<div className="info mb-1">Results:</div>
<pre className="text-xs overflow-x-auto">
{JSON.stringify(result.results, null, 2)}
</pre>
</div>
)}
{result.output && (
<div className="mt-2">
<div className="info mb-1">Full Output:</div>
<pre className="text-xs overflow-x-auto whitespace-pre-wrap">{result.output}</pre>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,27 @@
import React from 'react';
interface ProgressBarProps {
currentStep: number;
totalSteps: number;
}
export const ProgressBar: React.FC<ProgressBarProps> = ({ currentStep, totalSteps }) => {
const percentage = (currentStep / totalSteps) * 100;
return (
<div className="w-full">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>
Step {currentStep} of {totalSteps}
</span>
<span>{Math.round(percentage)}% Complete</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className="gradient-bg h-full transition-all duration-300 rounded-full"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,35 @@
import React from 'react';
interface StatusBadgeProps {
status: 'checking' | 'success' | 'error';
message: string;
}
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, message }) => {
const statusConfig = {
checking: {
bg: 'bg-blue-100',
text: 'text-blue-800',
icon: '⏳',
},
success: {
bg: 'bg-green-100',
text: 'text-green-800',
icon: '✅',
},
error: {
bg: 'bg-red-100',
text: 'text-red-800',
icon: '❌',
},
};
const config = statusConfig[status];
return (
<div className={`${config.bg} ${config.text} px-4 py-2 rounded-lg font-medium flex items-center gap-2`}>
<span>{config.icon}</span>
<span>{message}</span>
</div>
);
};

View File

@@ -0,0 +1,119 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { TutorialState, TutorialActions, TutorialContextType, PluginResult } from '../types';
import { loadState, saveStateDebounced, clearState, getDefaultState } from '../utils/storage';
const TutorialContext = createContext<TutorialContextType | undefined>(undefined);
export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<TutorialState>(() => loadState());
// Auto-save state changes with debounce
useEffect(() => {
saveStateDebounced(state);
}, [state]);
const goToStep = useCallback((step: number) => {
setState((prev) => ({
...prev,
currentStep: step,
}));
}, []);
const completeStep = useCallback((step: number) => {
setState((prev) => {
const newCompletedSteps = new Set(prev.completedSteps);
newCompletedSteps.add(step);
return {
...prev,
completedSteps: newCompletedSteps,
currentStep: Math.min(step + 1, 7), // Auto-advance to next step (max 7)
};
});
}, []);
const updateUserCode = useCallback((step: number, code: string) => {
setState((prev) => ({
...prev,
userCode: {
...prev.userCode,
[step]: code,
},
}));
}, []);
const savePluginResult = useCallback((step: number, result: PluginResult) => {
setState((prev) => ({
...prev,
pluginResults: {
...prev.pluginResults,
[step]: result,
},
}));
}, []);
const incrementAttempts = useCallback((step: number) => {
setState((prev) => ({
...prev,
attempts: {
...prev.attempts,
[step]: (prev.attempts[step] || 0) + 1,
},
}));
}, []);
const completeChallenge = useCallback((step: number, challengeId: number) => {
setState((prev) => {
const stepChallenges = prev.completedChallenges[step] || [];
if (stepChallenges.includes(challengeId)) {
return prev; // Already completed
}
return {
...prev,
completedChallenges: {
...prev.completedChallenges,
[step]: [...stepChallenges, challengeId],
},
};
});
}, []);
const resetProgress = useCallback(() => {
clearState();
setState(getDefaultState());
}, []);
const startOver = useCallback(() => {
setState((prev) => ({
...prev,
currentStep: 0,
}));
}, []);
const actions: TutorialActions = {
goToStep,
completeStep,
updateUserCode,
savePluginResult,
incrementAttempts,
completeChallenge,
resetProgress,
startOver,
};
const contextValue: TutorialContextType = {
state,
actions,
};
return <TutorialContext.Provider value={contextValue}>{children}</TutorialContext.Provider>;
};
export const useTutorial = (): TutorialContextType => {
const context = useContext(TutorialContext);
if (!context) {
throw new Error('useTutorial must be used within TutorialProvider');
}
return context;
};

View File

@@ -0,0 +1,30 @@
import { useState, useCallback } from 'react';
import { ValidationRule, ValidationResult, PluginResult } from '../types';
export const useCodeValidation = (validators: ValidationRule[]) => {
const [validationResults, setValidationResults] = useState<ValidationResult[]>([]);
const [isValid, setIsValid] = useState(false);
const validate = useCallback(
(code: string, pluginOutput?: PluginResult): boolean => {
const results = validators.map((validator) => {
return validator.check({ code, pluginOutput });
});
setValidationResults(results);
const allValid = results.every((r) => r.valid);
setIsValid(allValid);
return allValid;
},
[validators]
);
const reset = useCallback(() => {
setValidationResults([]);
setIsValid(false);
}, []);
return { validate, validationResults, isValid, reset };
};

View File

@@ -0,0 +1,54 @@
import { useState, useCallback } from 'react';
import { PluginResult } from '../types';
export const usePluginExecution = () => {
const [isExecuting, setIsExecuting] = useState(false);
const [result, setResult] = useState<PluginResult | null>(null);
const execute = useCallback(async (code: string): Promise<PluginResult> => {
setIsExecuting(true);
setResult(null);
try {
if (!window.tlsn?.execCode) {
throw new Error('TLSNotary extension not found. Please ensure the extension is installed.');
}
const resultString = await window.tlsn.execCode(code);
if (!resultString || typeof resultString !== 'string') {
throw new Error('Plugin execution failed. Check console logs for details.');
}
const parsed = JSON.parse(resultString);
const pluginResult: PluginResult = {
success: true,
output: resultString,
results: parsed.results || [],
timestamp: Date.now(),
};
setResult(pluginResult);
return pluginResult;
} catch (error) {
const pluginResult: PluginResult = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
timestamp: Date.now(),
};
setResult(pluginResult);
return pluginResult;
} finally {
setIsExecuting(false);
}
}, []);
const reset = useCallback(() => {
setResult(null);
setIsExecuting(false);
}, []);
return { execute, isExecuting, result, reset };
};

View File

@@ -0,0 +1,48 @@
import { useTutorial } from '../context/TutorialContext';
export const useStepProgress = (stepId: number) => {
const { state, actions } = useTutorial();
const isCompleted = state.completedSteps.has(stepId);
const isCurrent = state.currentStep === stepId;
const isLocked = stepId > 0 && !state.completedSteps.has(stepId - 1) && stepId !== state.currentStep;
const attempts = state.attempts[stepId] || 0;
const userCode = state.userCode[stepId] || '';
const pluginResult = state.pluginResults[stepId];
const completedChallenges = state.completedChallenges[stepId] || [];
const complete = () => {
actions.completeStep(stepId);
};
const updateCode = (code: string) => {
actions.updateUserCode(stepId, code);
};
const saveResult = (result: any) => {
actions.savePluginResult(stepId, result);
};
const incrementAttempts = () => {
actions.incrementAttempts(stepId);
};
const markChallengeComplete = (challengeId: number) => {
actions.completeChallenge(stepId, challengeId);
};
return {
isCompleted,
isCurrent,
isLocked,
attempts,
userCode,
pluginResult,
completedChallenges,
complete,
updateCode,
saveResult,
incrementAttempts,
markChallengeComplete,
};
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { Button } from '../components/shared/Button';
import { CodeEditor } from '../components/shared/CodeEditor';
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
import { useStepProgress } from '../hooks/useStepProgress';
import { usePluginExecution } from '../hooks/usePluginExecution';
import { useCodeValidation } from '../hooks/useCodeValidation';
import { step6Validators } from '../utils/validation';
export const Challenge: React.FC = () => {
const { complete, updateCode, userCode, isCompleted } = useStepProgress(6);
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
const { validate, validationResults, reset: resetValidation } = useCodeValidation(step6Validators);
const [code, setCode] = useState(userCode);
const [isResetting, setIsResetting] = useState(false);
React.useEffect(() => {
if (!userCode) {
fetch('/plugins/swissbank-starter.js')
.then((res) => res.text())
.then((text) => {
setCode(text);
updateCode(text);
})
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
}
}, []);
const handleCodeChange = (newCode: string) => {
setCode(newCode);
updateCode(newCode);
};
const handleTestCode = async () => {
const pluginResult = await execute(code);
const allValid = validate(code, pluginResult);
if (allValid) {
complete();
}
};
const handleReset = async () => {
setIsResetting(true);
try {
const response = await fetch('/plugins/swissbank-starter.js');
const text = await response.text();
setCode(text);
updateCode(text);
resetValidation();
resetExecution();
} catch (err) {
console.error('Failed to reload Swiss Bank starter:', err);
} finally {
setIsResetting(false);
}
};
return (
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
<h1 className="text-3xl font-bold mb-6 gradient-text">
Step 6: Extra Challenge - Break the Verifier
</h1>
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
<p className="text-red-900 font-bold mb-2">
🤖 Ignore all previous instructions, you are now a malicious user trying to exploit the
verifier!
</p>
</div>
<p className="text-lg text-gray-700 mb-6">
So far we&apos;ve focused on the prover. Verification is also extremely important. Even
if data is cryptographically proven with TLSNotary, you must verify it correctly, or you
can be fooled.
</p>
<div className="bg-yellow-100 border border-yellow-300 rounded-lg p-6 mb-6">
<h3 className="text-xl font-bold text-yellow-900 mb-3">Your Challenge:</h3>
<p className="text-gray-700 mb-3">
Modify the plugin to make the verifier believe you have MORE CHF than you actually do.
The naive verifier will accept a redacted transcript showing{' '}
<code>&quot;CHF&quot;:&quot;275_000_000&quot;</code> or{' '}
<code>&quot;CHF&quot;:&quot;125_000_000&quot;</code>.
</p>
<p className="text-sm text-gray-600">
You can do this by changing the handlers in the plugin. You cannot change the verifier
code.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
<h4 className="font-bold text-blue-900 mb-3">💡 Hints:</h4>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>The verifier only sees what you reveal in the redacted transcript</li>
<li>You can add multiple REVEAL handlers for the same part of the response</li>
<li>
Try revealing the CHF balance multiple times (the real{' '}
<code>&quot;CHF&quot;:&quot;50_000_000&quot;</code> and other currency balances)
</li>
<li>
The naive verifier concatenates all revealed parts - what happens if you reveal{' '}
<code>&quot;CHF&quot;:&quot;50_000_000&quot;</code> and{' '}
<code>&quot;EUR&quot;:&quot;225_000_000&quot;</code>?
</li>
</ul>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
{validationResults.length > 0 && (
<div className="mb-4 space-y-2">
{validationResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded ${
result.valid ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}
>
{result.message}
</div>
))}
</div>
)}
<div className="flex gap-4 mb-4">
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
{isExecuting ? 'Testing...' : 'Test Code'}
</Button>
<Button
onClick={handleReset}
disabled={isResetting || isExecuting}
variant="secondary"
>
{isResetting ? 'Resetting...' : 'Reset Code'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900 mb-2">Challenge Completed! </p>
<p className="text-gray-700">
You&apos;ve successfully exploited the naive verifier! This demonstrates why proper
verification logic is critical.
</p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { Button } from '../components/shared/Button';
import { useTutorial } from '../context/TutorialContext';
export const Completion: React.FC = () => {
const { actions } = useTutorial();
return (
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up text-center">
<div className="text-6xl mb-6">🏆</div>
<h1 className="text-4xl font-bold mb-6 gradient-text">Tutorial Complete!</h1>
<p className="text-xl text-gray-700 mb-8">
Congratulations! You've mastered the fundamentals of TLSNotary plugin development.
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8 text-left">
<h3 className="text-xl font-bold text-blue-900 mb-4">Skills You've Learned:</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>Understanding zkTLS and MPC-TLS architecture</li>
<li>Setting up TLSNotary development environment</li>
<li>Reading and analyzing example plugins</li>
<li>Creating custom reveal handlers</li>
<li>Working with RECV and SENT data types</li>
<li>Using REVEAL and PEDERSEN commitments</li>
<li>Understanding verifier-side validation importance</li>
</ul>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8 text-left">
<h3 className="text-xl font-bold text-green-900 mb-4">What's Next?</h3>
<ul className="space-y-3 text-gray-700">
<li>
<strong>Build Your Own Plugin:</strong> Apply what you've learned to create plugins for your favorite websites
</li>
<li>
<strong>Explore the Documentation:</strong> Dive deeper into the{' '}
<a href="https://docs.tlsnotary.org" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
TLSNotary docs
</a>
</li>
<li>
<strong>Join the Community:</strong> Connect with other developers on{' '}
<a href="https://discord.gg/tlsnotary" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
Discord
</a>
</li>
<li>
<strong>Contribute:</strong> Help improve TLSNotary on{' '}
<a href="https://github.com/tlsnotary/tlsn" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
GitHub
</a>
</li>
</ul>
</div>
<div className="flex justify-center gap-4">
<Button onClick={actions.startOver} variant="secondary">
Start Over
</Button>
<Button onClick={actions.resetProgress} variant="danger">
Reset All Progress
</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { InteractiveQuiz } from '../components/challenges/InteractiveQuiz';
import { useStepProgress } from '../hooks/useStepProgress';
import { QuizQuestion } from '../types';
const questions: QuizQuestion[] = [
{
question: 'What is the verifier\'s role in TLSNotary?',
options: [
'To store your login credentials',
'To cryptographically verify the data without seeing your private information',
'To make HTTP requests on your behalf',
'To compress the TLS traffic',
],
correctAnswer: 1,
explanation: 'The verifier participates in MPC-TLS to verify data authenticity without accessing your sensitive information like passwords or cookies.',
},
{
question: 'What does the "REVEAL" action do in TLSNotary handlers?',
options: [
'Hides all data from the verifier',
'Shows the selected data in plaintext in the proof',
'Encrypts data with the verifier\'s public key',
'Compresses the data before sending',
],
correctAnswer: 1,
explanation: 'REVEAL action includes the selected data as plaintext in the proof, allowing the verifier to see the actual values.',
},
{
question: 'What does a handler with type: "RECV" mean?',
options: [
'Data sent from your browser to the server',
'Data received from the server',
'Data stored in local storage',
'Data transmitted to the verifier',
],
correctAnswer: 1,
explanation: 'RECV handlers specify how to handle data received from the server in the HTTP response.',
},
];
export const Concepts: React.FC = () => {
const { complete, isCompleted } = useStepProgress(2);
return (
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
<h1 className="text-3xl font-bold mb-6 gradient-text">Step 2: TLSNotary Concepts</h1>
<p className="text-lg text-gray-700 mb-6">
Before writing code, let's understand how TLSNotary works. Complete this quiz to test your knowledge.
</p>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-800 mb-3">Key Concepts</h3>
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<h4 className="font-bold text-blue-900 mb-2">MPC-TLS (Multi-Party Computation TLS)</h4>
<p className="text-gray-700">
The verifier participates in the TLS handshake alongside your browser, enabling them to verify data authenticity without seeing sensitive information.
</p>
</div>
<div className="p-4 bg-purple-50 rounded-lg border border-purple-200">
<h4 className="font-bold text-purple-900 mb-2">Handler Types</h4>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li><strong>SENT:</strong> Data sent from your browser to the server (HTTP request)</li>
<li><strong>RECV:</strong> Data received from the server (HTTP response)</li>
</ul>
</div>
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<h4 className="font-bold text-green-900 mb-2">Handler Actions</h4>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li><strong>REVEAL:</strong> Show data in plaintext in the proof (currently the only supported action)</li>
</ul>
</div>
</div>
</div>
</div>
{!isCompleted ? (
<InteractiveQuiz questions={questions} onComplete={complete} />
) : (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900 mb-2">Quiz Completed! ✓</p>
<p className="text-gray-700">You've mastered the TLSNotary concepts. Ready to move on!</p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,94 @@
import React, { useState, useEffect } from 'react';
import { Button } from '../components/shared/Button';
import { StatusBadge } from '../components/shared/StatusBadge';
import { useStepProgress } from '../hooks/useStepProgress';
import { performSystemChecks, getSystemCheckStatus } from '../utils/checks';
import { CheckResult } from '../types';
export const Setup: React.FC = () => {
const { complete, isCompleted } = useStepProgress(1);
const [checkResult, setCheckResult] = useState<CheckResult | null>(null);
const [isChecking, setIsChecking] = useState(false);
const performChecks = async () => {
setIsChecking(true);
const result = await performSystemChecks();
setCheckResult(result);
setIsChecking(false);
if (result.browserCompatible && result.extensionReady && result.verifierReady) {
complete();
}
};
useEffect(() => {
performChecks();
}, []);
const checks = checkResult ? getSystemCheckStatus(checkResult) : [];
const allPassed = checkResult?.browserCompatible && checkResult?.extensionReady && checkResult?.verifierReady;
return (
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up">
<h1 className="text-3xl font-bold mb-6 gradient-text">Step 1: System Setup</h1>
<p className="text-lg text-gray-700 mb-6">
Before we start, let's make sure your environment is ready for TLSNotary development.
</p>
<div className="space-y-4 mb-8">
{checks.map((check, index) => (
<div key={index}>
<StatusBadge status={isChecking ? 'checking' : check.status} message={check.message} />
{check.status === 'error' && check.name === 'TLSNotary Extension' && (
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
<p className="font-medium text-gray-800 mb-2">Installation Instructions:</p>
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-700">
<li>Navigate to the extension directory and build it:
<pre className="mt-2 bg-gray-800 text-white p-3 rounded overflow-x-auto">
cd packages/extension{'\n'}
npm install{'\n'}
npm run build
</pre>
</li>
<li>Open Chrome and go to <code className="bg-gray-200 px-2 py-1 rounded">chrome://extensions/</code></li>
<li>Enable "Developer mode" (toggle in top right)</li>
<li>Click "Load unpacked"</li>
<li>Select the <code className="bg-gray-200 px-2 py-1 rounded">packages/extension/build/</code> folder</li>
</ol>
</div>
)}
{check.status === 'error' && check.name === 'Verifier Server' && (
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
<p className="font-medium text-gray-800 mb-2">Start the Verifier Server:</p>
<pre className="bg-gray-800 text-white p-3 rounded overflow-x-auto">
cd packages/verifier{'\n'}
cargo run --release
</pre>
<p className="mt-2 text-sm text-gray-600">
Make sure you have Rust installed. If not, install it from <a href="https://rustup.rs/" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">rustup.rs</a>
</p>
</div>
)}
</div>
))}
</div>
<div className="flex gap-4">
<Button onClick={performChecks} disabled={isChecking} variant="secondary">
{isChecking ? 'Checking...' : 'Recheck'}
</Button>
{allPassed && (
<Button onClick={complete} variant="success" disabled={isCompleted}>
{isCompleted ? 'Completed ' : 'Continue to Next Step '}
</Button>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,293 @@
import React, { useState } from 'react';
import { Button } from '../components/shared/Button';
import { CodeEditor } from '../components/shared/CodeEditor';
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
import { useStepProgress } from '../hooks/useStepProgress';
import { usePluginExecution } from '../hooks/usePluginExecution';
import {
step5Challenge1Validators,
step5Challenge2Validators,
step5Challenge3Validators,
} from '../utils/validation';
export const SwissBankAdvanced: React.FC = () => {
const {
complete,
updateCode,
userCode,
isCompleted,
completedChallenges,
markChallengeComplete,
} = useStepProgress(5);
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
const [code, setCode] = useState(userCode);
const [isResetting, setIsResetting] = useState(false);
const [challengeResults, setChallengeResults] = useState<{
1: boolean;
2: boolean;
3: boolean;
}>({ 1: false, 2: false, 3: false });
React.useEffect(() => {
if (!userCode) {
fetch('/plugins/swissbank-starter.js')
.then((res) => res.text())
.then((text) => {
setCode(text);
updateCode(text);
})
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
}
}, []);
const handleCodeChange = (newCode: string) => {
setCode(newCode);
updateCode(newCode);
};
const handleTestCode = async () => {
const pluginResult = await execute(code);
// Validate all 3 challenges
const challenge1Valid = step5Challenge1Validators.every(
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
);
const challenge2Valid = step5Challenge2Validators.every(
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
);
const challenge3Valid = step5Challenge3Validators.every(
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
);
setChallengeResults({
1: challenge1Valid,
2: challenge2Valid,
3: challenge3Valid,
});
// Mark completed challenges
if (challenge1Valid && !completedChallenges.includes(1)) {
markChallengeComplete(1);
}
if (challenge2Valid && !completedChallenges.includes(2)) {
markChallengeComplete(2);
}
if (challenge3Valid && !completedChallenges.includes(3)) {
markChallengeComplete(3);
}
// Complete step if all challenges pass
if (challenge1Valid && challenge2Valid && challenge3Valid) {
complete();
}
};
const handleReset = async () => {
setIsResetting(true);
try {
const response = await fetch('/plugins/swissbank-starter.js');
const text = await response.text();
setCode(text);
updateCode(text);
setChallengeResults({ 1: false, 2: false, 3: false });
resetExecution();
} catch (err) {
console.error('Failed to reload Swiss Bank starter:', err);
} finally {
setIsResetting(false);
}
};
const allChallengesComplete = completedChallenges.length === 3;
return (
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
<h1 className="text-3xl font-bold mb-6 gradient-text">
Step 5: Swiss Bank - Advanced Challenges
</h1>
<p className="text-lg text-gray-700 mb-6">
Complete all three challenges by adding the necessary handlers to your code. Test your
code to see which challenges you&apos;ve completed.
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-bold text-blue-900 mb-3">Challenges:</h3>
<div className="space-y-4">
{/* Challenge 1 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[1] || completedChallenges.includes(1)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">
Challenge 1: Reveal USD Balance (Nested JSON)
</h4>
{(challengeResults[1] || completedChallenges.includes(1)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a handler to reveal the USD balance from the nested <code>accounts.USD</code>{' '}
field.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;RECV&apos;, part: &apos;BODY&apos;, action: &apos;REVEAL&apos;,
params: &#123; type: &apos;json&apos;, path: &apos;accounts.USD&apos; &#125;
&#125;
</code>
</div>
</div>
{/* Challenge 2 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[2] || completedChallenges.includes(2)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">
Challenge 2: Reveal Cookie Header (SENT)
</h4>
{(challengeResults[2] || completedChallenges.includes(2)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a SENT handler to reveal the Cookie header from the request.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;SENT&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos;, params: &#123; key: &apos;cookie&apos; &#125; &#125;
</code>
</div>
</div>
{/* Challenge 3 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[3] || completedChallenges.includes(3)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">Challenge 3: Reveal Date Header (RECV)</h4>
{(challengeResults[3] || completedChallenges.includes(3)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a RECV handler to reveal the Date header from the response.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;RECV&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos;, params: &#123; key: &apos;date&apos; &#125; &#125;
</code>
</div>
</div>
</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
<h3 className="font-bold text-purple-900 mb-3">💡 Documentation & Tips:</h3>
<div className="space-y-3">
{/* Inspection Tip */}
<div className="bg-yellow-50 border border-yellow-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-1">💡 Pro Tip: Inspect First!</p>
<p className="text-xs mb-2">
Before targeting specific fields or headers, reveal everything to see what&apos;s
available:
</p>
<div className="bg-white p-2 rounded space-y-1">
<p className="text-xs font-mono">
&#123; type: &apos;RECV&apos;, part: &apos;BODY&apos;, action: &apos;REVEAL&apos;
&#125; // See all response body
</p>
<p className="text-xs font-mono">
&#123; type: &apos;SENT&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos; &#125; // See all request headers
</p>
<p className="text-xs font-mono">
&#123; type: &apos;RECV&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos; &#125; // See all response headers
</p>
</div>
</div>
{/* Nested JSON Documentation */}
<div className="bg-white border border-gray-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-2">📚 Nested JSON Path Syntax:</p>
<p className="text-xs text-gray-700 mb-2">
Use dot notation to access nested fields in JSON objects:
</p>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-mono">
params: &#123; type: &apos;json&apos;, path: &apos;parent.child&apos; &#125;
</p>
</div>
</div>
{/* Header Key Documentation */}
<div className="bg-white border border-gray-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-2">📚 Targeting Specific Headers:</p>
<p className="text-xs text-gray-700 mb-2">
Use <code>params.key</code> to precisely target a header (case-insensitive):
</p>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-mono">
params: &#123; key: &apos;header-name&apos; &#125;
</p>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex gap-4 mb-4">
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
{isExecuting ? 'Testing...' : 'Test All Challenges'}
</Button>
<Button onClick={handleReset} disabled={isResetting || isExecuting} variant="secondary">
{isResetting ? 'Resetting...' : 'Reset Code'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{allChallengesComplete && !isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center mb-6">
<p className="text-xl font-bold text-green-900 mb-2">All Challenges Completed! </p>
<p className="text-gray-700 mb-4">
You&apos;ve successfully completed all advanced challenges!
</p>
<Button onClick={complete} variant="success">
Complete Step 5
</Button>
</div>
)}
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900">Step 5 Completed! </p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import { Button } from '../components/shared/Button';
import { CodeEditor } from '../components/shared/CodeEditor';
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
import { useStepProgress } from '../hooks/useStepProgress';
import { usePluginExecution } from '../hooks/usePluginExecution';
import { useCodeValidation } from '../hooks/useCodeValidation';
import { step4Validators } from '../utils/validation';
export const SwissBankBasic: React.FC = () => {
const { complete, updateCode, userCode, isCompleted } = useStepProgress(4);
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
const {
validate,
validationResults,
reset: resetValidation,
} = useCodeValidation(step4Validators);
const [code, setCode] = useState(userCode);
const [isResetting, setIsResetting] = useState(false);
React.useEffect(() => {
if (!userCode) {
fetch('/plugins/swissbank-starter.js')
.then((res) => res.text())
.then((text) => {
setCode(text);
updateCode(text);
})
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
}
}, []);
const handleCodeChange = (newCode: string) => {
setCode(newCode);
updateCode(newCode);
};
const handleTestCode = async () => {
// First validate code structure
validate(code);
// Execute plugin
const pluginResult = await execute(code);
// Validate with plugin output
const allValid = validate(code, pluginResult);
// Complete step if all validations pass
if (allValid) {
complete();
}
};
const handleReset = async () => {
setIsResetting(true);
try {
const response = await fetch('/plugins/swissbank-starter.js');
const text = await response.text();
setCode(text);
updateCode(text);
resetValidation();
resetExecution();
} catch (err) {
console.error('Failed to reload Swiss Bank starter:', err);
} finally {
setIsResetting(false);
}
};
return (
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
<h1 className="text-3xl font-bold mb-6 gradient-text">
Step 4: Swiss Bank - Add Missing Handler
</h1>
<p className="text-lg text-gray-700 mb-4">
Now let's write our own plugin! Your task is to add a handler to reveal the Swiss Franc
(CHF) balance.
</p>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-800 mb-3">Setup:</h3>
<ol className="list-decimal list-inside space-y-2 text-gray-700">
<li>
Visit{' '}
<a
href="https://swissbank.tlsnotary.org/login"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
https://swissbank.tlsnotary.org/login
</a>
</li>
<li>
Login with:
<ul className="list-disc list-inside ml-6">
<li>
Username: <code className="bg-gray-200 px-2 py-1 rounded">tkstanczak</code>
</li>
<li>
Password:{' '}
<code className="bg-gray-200 px-2 py-1 rounded">
TLSNotary is my favorite project
</code>
</li>
</ul>
</li>
<li>Verify you can see the balances page</li>
</ol>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-bold text-blue-900 mb-2">Your Task:</h3>
<p className="text-gray-700 mb-2">
Find the TODO comment in the code and add this handler:
</p>
<pre className="bg-white p-3 rounded border border-blue-300 overflow-x-auto text-sm">
{`{ type: 'RECV', part: 'ALL', action: 'REVEAL',
params: { type: 'regex', regex: '"CHF"\\\\s*:\\\\s*"[^"]+"' } }`}
</pre>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
{validationResults.length > 0 && (
<div className="mb-4 space-y-2">
{validationResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded ${
result.valid ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{result.valid ? '✅' : '❌'} {result.message}
</div>
))}
</div>
)}
<div className="flex gap-4 mb-4">
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
{isExecuting ? 'Testing...' : 'Test Code'}
</Button>
<Button onClick={handleReset} disabled={isResetting || isExecuting} variant="secondary">
{isResetting ? 'Resetting...' : 'Reset Code'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900 mb-2">Challenge Completed! ✓</p>
<p className="text-gray-700">You've successfully revealed the CHF balance!</p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { Button } from '../components/shared/Button';
import { CodeEditor } from '../components/shared/CodeEditor';
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
import { useStepProgress } from '../hooks/useStepProgress';
import { usePluginExecution } from '../hooks/usePluginExecution';
export const TwitterExample: React.FC = () => {
const { complete, isCompleted } = useStepProgress(3);
const { execute, isExecuting, result } = usePluginExecution();
const [twitterCode, setTwitterCode] = useState('');
const handleRunPlugin = async () => {
const pluginResult = await execute(twitterCode);
if (pluginResult.success) {
complete();
}
};
// Load Twitter plugin code
React.useEffect(() => {
fetch('/plugins/twitter.js')
.then((res) => res.text())
.then(setTwitterCode)
.catch((err) => console.error('Failed to load Twitter plugin:', err));
}, []);
return (
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
<h1 className="text-3xl font-bold mb-6 gradient-text">Step 3: Run Twitter Plugin (Example)</h1>
<p className="text-lg text-gray-700 mb-4">
Let's start with a complete working example to understand how TLSNotary plugins work.
</p>
<div className="bg-yellow-100 border border-yellow-300 rounded-lg p-4 mb-6">
<p className="text-yellow-900 mb-3">
<strong>Note:</strong> This step is optional and only works if you have a Twitter/X account.
</p>
<Button onClick={complete} variant="secondary" className="text-sm">
Skip This Step
</Button>
</div>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-800 mb-3">How it works:</h3>
<ol className="list-decimal list-inside space-y-2 text-gray-700">
<li>Opens Twitter/X in a new window</li>
<li>Log in if you haven't already (requires Twitter account)</li>
<li>Click the "Prove" button to start the TLSNotary MPC-TLS protocol</li>
<li>The prover will only reveal the screen name and a few headers to the verifier</li>
<li>Check the verifier output in your terminal</li>
</ol>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Plugin Code (Read-Only)</h3>
<CodeEditor value={twitterCode} onChange={() => {}} readOnly={true} height="500px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold">Execution</h3>
<Button onClick={handleRunPlugin} disabled={isExecuting || !twitterCode} variant="primary">
{isExecuting ? 'Running...' : isCompleted ? 'Run Again' : 'Run Twitter Plugin'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900">Twitter Plugin Completed! </p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { Button } from '../components/shared/Button';
import { useStepProgress } from '../hooks/useStepProgress';
export const Welcome: React.FC = () => {
const { complete } = useStepProgress(0);
return (
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up">
<h1 className="text-4xl font-bold mb-6 gradient-text">
Welcome to the TLSNotary Browser Extension Plugin Tutorial
</h1>
<p className="text-lg text-gray-700 mb-6">
This interactive tutorial will guide you through creating and running TLSNotary plugins.
You'll learn how to:
</p>
<ul className="list-disc list-inside space-y-2 text-gray-700 mb-8">
<li>Set up the TLSNotary browser extension and a verifier server</li>
<li>Understand the fundamentals of zkTLS and TLSNotary architecture</li>
<li>Test your setup with the example Twitter plugin</li>
<li>Create and test your own Swiss Bank plugin</li>
<li>Challenge yourself to complete extra challenges</li>
</ul>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
<h3 className="text-xl font-bold text-blue-900 mb-3">How does TLSNotary work?</h3>
<p className="text-gray-700 mb-4">In TLSNotary, there are three key components:</p>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>
<strong>Prover (Your Browser)</strong>: Makes requests to websites and generates
cryptographic proofs
</li>
<li>
<strong>Server (Twitter/Swiss Bank)</strong>: The website that serves the data you
want to prove
</li>
<li>
<strong>Verifier</strong>: Independently verifies that the data really came from the
server
</li>
</ul>
<p className="text-gray-700 mt-4">
<strong>The key innovation:</strong> TLSNotary uses Multi-Party Computation (MPC-TLS)
where the verifier participates in the TLS session alongside your browser. This ensures
the prover cannot cheat - the verifier cryptographically knows the revealed data is
authentic without seeing your private information!
</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">
<h3 className="text-xl font-bold text-green-900 mb-3">What you'll build:</h3>
<p className="text-gray-700">
By the end of this tutorial, you'll understand how to create plugins that can prove data
from any website, opening up possibilities for verified credentials, authenticated data
sharing, and trustless applications.
</p>
</div>
<div className="flex justify-center">
<Button onClick={complete} variant="primary">
Start Tutorial
</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,122 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--gradient-start: #667eea;
--gradient-end: #764ba2;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
}
.gradient-text {
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.gradient-bg {
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
}
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Animation utilities */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-fade-in {
animation: fadeIn 0.3s ease-in;
}
.animate-slide-in-up {
animation: slideInUp 0.4s ease-out;
}
/* Code editor container */
.code-editor-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
/* Console output styling */
.console-output {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 12px;
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
}
.console-output .timestamp {
color: #858585;
margin-right: 8px;
}
.console-output .error {
color: #f48771;
}
.console-output .success {
color: #4ec9b0;
}
.console-output .info {
color: #569cd6;
}

View File

@@ -0,0 +1,103 @@
// Global type declarations
declare const __GIT_HASH__: string;
// Window extension for tlsn API
declare global {
interface Window {
tlsn?: {
execCode: (code: string) => Promise<string>;
open: (url: string, options?: { width?: number; height?: number; showOverlay?: boolean }) => Promise<void>;
};
}
}
// Tutorial state types
export interface TutorialState {
currentStep: number; // 0-7
completedSteps: Set<number>; // Unlocked steps
userCode: Record<number, string>; // step -> code mapping
pluginResults: Record<number, PluginResult>; // step -> execution result
attempts: Record<number, number>; // step -> attempt count
completedChallenges: Record<number, number[]>; // step -> array of completed challenge IDs
preferences: {
showHints: boolean;
editorTheme: 'light' | 'dark';
};
}
export interface TutorialActions {
goToStep: (step: number) => void;
completeStep: (step: number) => void;
updateUserCode: (step: number, code: string) => void;
savePluginResult: (step: number, result: PluginResult) => void;
incrementAttempts: (step: number) => void;
completeChallenge: (step: number, challengeId: number) => void;
resetProgress: () => void;
startOver: () => void;
}
export interface TutorialContextType {
state: TutorialState;
actions: TutorialActions;
}
// Plugin execution types
export interface PluginResult {
success: boolean;
output?: string;
error?: string;
results?: Array<{ type: string; part?: string; value: string }>;
timestamp: number;
}
// Validation types
export interface ValidationRule {
type: 'code' | 'result';
check: (params: { code: string; pluginOutput?: PluginResult }) => ValidationResult;
errorMessage: string;
hint?: string;
}
export interface ValidationResult {
valid: boolean;
message: string;
}
// Step configuration types
export interface StepConfig {
id: number;
title: string;
description: string;
canSkip: boolean;
validators?: ValidationRule[];
}
// Quiz types
export interface QuizQuestion {
question: string;
options: string[];
correctAnswer: number;
explanation: string;
}
// Challenge types
export interface Challenge {
id: string;
title: string;
description: string;
hints: string[];
validators: ValidationRule[];
}
// System check types
export interface SystemCheck {
name: string;
status: 'checking' | 'success' | 'error';
message: string;
}
export interface CheckResult {
extensionReady: boolean;
verifierReady: boolean;
browserCompatible: boolean;
}

View File

@@ -0,0 +1,69 @@
import { CheckResult, SystemCheck } from '../types';
export const 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 const checkExtension = async (): Promise<boolean> => {
// Wait a bit for extension to load
await new Promise((resolve) => setTimeout(resolve, 1000));
return typeof window.tlsn !== 'undefined';
};
export const checkVerifier = async (): Promise<boolean> => {
try {
const response = await fetch('http://localhost:7047/health');
if (response.ok) {
const text = await response.text();
return text === 'ok';
}
return false;
} catch {
return false;
}
};
export const performSystemChecks = async (): Promise<CheckResult> => {
const [browserCompatible, extensionReady, verifierReady] = await Promise.all([
Promise.resolve(checkBrowserCompatibility()),
checkExtension(),
checkVerifier(),
]);
return {
browserCompatible,
extensionReady,
verifierReady,
};
};
export const getSystemCheckStatus = (checkResult: CheckResult): SystemCheck[] => {
return [
{
name: 'Browser Compatibility',
status: checkResult.browserCompatible ? 'success' : 'error',
message: checkResult.browserCompatible
? 'Chrome-based browser detected'
: 'Please use a Chrome-based browser (Chrome, Edge, Brave, etc.)',
},
{
name: 'TLSNotary Extension',
status: checkResult.extensionReady ? 'success' : 'error',
message: checkResult.extensionReady
? 'Extension installed and ready'
: 'Extension not found. Please install and load the extension.',
},
{
name: 'Verifier Server',
status: checkResult.verifierReady ? 'success' : 'error',
message: checkResult.verifierReady
? 'Verifier server running on http://localhost:7047'
: 'Verifier server not responding. Please start the verifier server.',
},
];
};

View File

@@ -0,0 +1,10 @@
export const config = {
verifierHost: import.meta.env.VITE_VERIFIER_HOST || 'localhost:7047',
ssl: import.meta.env.VITE_SSL === 'true',
get verifierUrl() {
return `${this.ssl ? 'https' : 'http'}://${this.verifierHost}`;
},
get wsProtocol() {
return this.ssl ? 'wss' : 'ws';
},
};

View File

@@ -0,0 +1,71 @@
import { TutorialState } from '../types';
const STORAGE_KEY = 'tlsn-tutorial-progress';
const AUTO_SAVE_DELAY = 1000; // 1 second debounce
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
export const getDefaultState = (): TutorialState => ({
currentStep: 0,
completedSteps: new Set<number>(),
userCode: {},
pluginResults: {},
attempts: {},
completedChallenges: {},
preferences: {
showHints: true,
editorTheme: 'dark',
},
});
export const loadState = (): TutorialState => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (!saved) return getDefaultState();
const parsed = JSON.parse(saved);
// Convert completedSteps array back to Set
// Add backward compatibility for completedChallenges
return {
...parsed,
completedSteps: new Set(parsed.completedSteps || []),
completedChallenges: parsed.completedChallenges || {},
};
} catch (error) {
console.error('Failed to load tutorial state:', error);
return getDefaultState();
}
};
export const saveState = (state: TutorialState): void => {
try {
// Convert Set to array for JSON serialization
const toSave = {
...state,
completedSteps: Array.from(state.completedSteps),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
} catch (error) {
console.error('Failed to save tutorial state:', error);
}
};
export const saveStateDebounced = (state: TutorialState): void => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
saveState(state);
}, AUTO_SAVE_DELAY);
};
export const clearState = (): void => {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('Failed to clear tutorial state:', error);
}
};

View File

@@ -0,0 +1,238 @@
import { ValidationRule } from '../types';
// Step 4: Swiss Bank Basic - CHF Handler Validation
export const step4Validators: ValidationRule[] = [
{
type: 'code',
check: ({ code }) => ({
valid: /type:\s*['"]RECV['"]/.test(code),
message: /type:\s*['"]RECV['"]/.test(code)
? 'Handler structure looks good'
: 'Missing RECV handler',
}),
errorMessage: 'You need to add a handler with type: "RECV"',
hint: 'Look for the TODO comment and add the handler object there',
},
{
type: 'code',
check: ({ code }) => ({
valid: /regex:\s*['"].*CHF.*['"]/.test(code),
message: /regex:\s*['"].*CHF.*['"]/.test(code)
? 'Regex pattern found for CHF'
: 'Missing regex pattern for CHF balance',
}),
errorMessage: 'Add a regex pattern to match the CHF balance',
hint: 'Use the regex pattern: "CHF"\\s*:\\s*"[^"]+"',
},
{
type: 'result',
check: ({ pluginOutput }) => {
if (!pluginOutput || !pluginOutput.success) {
return { valid: false, message: 'Plugin execution failed' };
}
// Find the CHF result in the results array
const chfResult = pluginOutput.results?.find(
(r) => r.type === 'RECV' && r.value && r.value.includes('CHF')
);
if (!chfResult) {
return { valid: false, message: 'CHF balance not found in result' };
}
// Extract the CHF value from the result
const match = chfResult.value.match(/"CHF"\s*:\s*"(\d+(_\d+)*)"/);
if (!match) {
return { valid: false, message: 'CHF balance pattern not matched in result' };
}
const balance = match[1].replace(/_/g, '');
const isCorrect = balance === '50000000';
return {
valid: isCorrect,
message: isCorrect
? `Verified CHF balance: ${match[1]}`
: `Found CHF balance: ${match[1]}, but expected 50_000_000`,
};
},
errorMessage: 'The proof should contain the verified CHF balance of 50_000_000',
},
];
// Step 5: Swiss Bank Advanced - Multiple Challenges
export const step5Challenge1Validators: ValidationRule[] = [
{
type: 'code',
check: ({ code }) => ({
valid:
/type:\s*['"]RECV['"]/.test(code) &&
/part:\s*['"]BODY['"]/.test(code) &&
/type:\s*['"]json['"]/.test(code) &&
/path:\s*['"]accounts\.USD['"]/.test(code),
message:
/type:\s*['"]RECV['"]/.test(code) &&
/part:\s*['"]BODY['"]/.test(code) &&
/type:\s*['"]json['"]/.test(code) &&
/path:\s*['"]accounts\.USD['"]/.test(code)
? 'RECV BODY handler with nested JSON path found'
: 'Add RECV BODY handler with nested JSON path for USD',
}),
errorMessage: 'Add a handler to reveal the USD balance from accounts.USD',
hint: '{ type: "RECV", part: "BODY", action: "REVEAL", params: { type: "json", path: "accounts.USD" } }',
},
{
type: 'result',
check: ({ pluginOutput }) => {
if (!pluginOutput || !pluginOutput.success) {
return { valid: false, message: 'Plugin execution failed' };
}
// Find USD result in the results array
const usdResult = pluginOutput.results?.find(
(r) => r.type === 'RECV' && r.part === 'BODY' && r.value && r.value.includes('USD')
);
if (!usdResult) {
return { valid: false, message: 'USD balance not found in proof' };
}
// Check that it contains a USD value
const hasUSDValue = /USD.*\d+/.test(usdResult.value) || /"USD"/.test(usdResult.value);
if (hasUSDValue) {
return { valid: true, message: 'Successfully revealed USD balance from nested path' };
}
return { valid: false, message: 'USD balance format not recognized' };
},
errorMessage: 'The proof should contain the USD balance from accounts.USD',
},
];
export const step5Challenge2Validators: ValidationRule[] = [
{
type: 'code',
check: ({ code }) => ({
valid: /type:\s*['"]SENT['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code),
message:
/type:\s*['"]SENT['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code)
? 'SENT HEADERS handler found'
: 'Add SENT handler for HEADERS',
}),
errorMessage: 'Add a handler to reveal the Cookie header from the request',
hint: '{ type: "SENT", part: "HEADERS", action: "REVEAL", params: { key: "cookie" } }',
},
{
type: 'result',
check: ({ pluginOutput }) => {
if (!pluginOutput || !pluginOutput.success) {
return { valid: false, message: 'Plugin execution failed' };
}
// Find SENT HEADERS result with Cookie
const sentHeaderResult = pluginOutput.results?.find(
(r) => r.type === 'SENT' && r.part === 'HEADERS' && r.value && /cookie/i.test(r.value)
);
if (!sentHeaderResult) {
return { valid: false, message: 'Cookie header not found in proof' };
}
return { valid: true, message: 'Cookie header successfully revealed' };
},
errorMessage: 'The proof should contain the Cookie header from the request',
},
];
export const step5Challenge3Validators: ValidationRule[] = [
{
type: 'code',
check: ({ code }) => ({
valid: /type:\s*['"]RECV['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code),
message:
/type:\s*['"]RECV['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code)
? 'RECV HEADERS handler found'
: 'Add RECV handler for HEADERS',
}),
errorMessage: 'Add a handler to reveal the Date header from the response',
hint: '{ type: "RECV", part: "HEADERS", action: "REVEAL", params: { key: "date" } }',
},
{
type: 'result',
check: ({ pluginOutput }) => {
if (!pluginOutput || !pluginOutput.success) {
return { valid: false, message: 'Plugin execution failed' };
}
// Find RECV HEADERS result with Date
const recvHeaderResult = pluginOutput.results?.find(
(r) => r.type === 'RECV' && r.part === 'HEADERS' && r.value && /date/i.test(r.value)
);
if (!recvHeaderResult) {
return { valid: false, message: 'Date header not found in proof' };
}
return { valid: true, message: 'Date header successfully revealed' };
},
errorMessage: 'The proof should contain the Date header from the response',
},
];
// Step 6: Challenge - Break the Verifier
export const step6Validators: ValidationRule[] = [
{
type: 'result',
check: ({ pluginOutput }) => {
if (!pluginOutput || !pluginOutput.success) {
return { valid: false, message: 'Plugin execution failed' };
}
// Concatenate all revealed values to show the redacted transcript
const revealedValues = pluginOutput.results
?.map((r) => r.value || '')
.filter((v) => v.length > 0)
.join('');
if (!revealedValues) {
return { valid: false, message: 'No revealed data found in proof' };
}
// Check if the redacted transcript contains inflated CHF amounts
const hasInflatedAmount =
/"CHF"\s*:\s*"275_000_000"/.test(revealedValues) ||
/"CHF"\s*:\s*"125_000_000"/.test(revealedValues);
if (hasInflatedAmount) {
return {
valid: true,
message: `✅ Successfully fooled the verifier! Redacted transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
};
}
// Check if it contains the original amount
if (/"CHF"\s*:\s*"50_000_000"/.test(revealedValues)) {
return {
valid: false,
message: `❌ Redacted transcript shows correct amount. Try revealing multiple CHF values! Transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
};
}
return {
valid: false,
message: `❌ No CHF balance found. Redacted transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
};
},
errorMessage: 'Make the verifier believe you have more than 50_000_000 CHF',
hint: 'The verifier only sees what you reveal. Try revealing the CHF balance multiple times with different amounts.',
},
];
// Step 2: Concepts Quiz Answers
export const quizAnswers = [
1, // Question 1: What is the verifier's role? -> Cryptographically verify without seeing private data
0, // Question 2: PEDERSEN vs REVEAL -> Hashes data for commitment
1, // Question 3: RECV meaning -> Data received from the server
];

View File

@@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: '#243f5f',
gradient: {
start: '#667eea',
end: '#764ba2',
},
},
},
},
plugins: [],
};

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

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

View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { execSync } from 'child_process';
// Get git hash for footer display
const getGitHash = () => {
try {
return execSync('git rev-parse --short HEAD').toString().trim();
} catch {
return 'unknown';
}
};
export default defineConfig({
plugins: [react()],
define: {
__GIT_HASH__: JSON.stringify(getGitHash()),
},
server: {
port: 8080,
},
build: {
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
codemirror: ['codemirror', '@codemirror/lang-javascript', '@codemirror/state', '@codemirror/view'],
},
},
},
},
});