mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-27 16:08:00 -05:00
Compare commits
6 Commits
main
...
new-tutori
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2383d7b3d1 | ||
|
|
c9c94ce03a | ||
|
|
9682acb0f8 | ||
|
|
b4a1cc8b2e | ||
|
|
09e107a871 | ||
|
|
3ea0300f65 |
158
PLUGIN.md
158
PLUGIN.md
@@ -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
878
VERIFIER.md
Normal 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
819
package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
32
packages/tutorial/.eslintrc.json
Normal file
32
packages/tutorial/.eslintrc.json
Normal 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
6
packages/tutorial/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
public/plugins/*.js
|
||||
7
packages/tutorial/.prettierrc.json
Normal file
7
packages/tutorial/.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
29
packages/tutorial/Dockerfile
Normal file
29
packages/tutorial/Dockerfile
Normal 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
|
||||
236
packages/tutorial/build-plugins.js
Normal file
236
packages/tutorial/build-plugins.js
Normal 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`);
|
||||
29
packages/tutorial/docker-compose.yml
Normal file
29
packages/tutorial/docker-compose.yml
Normal 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
|
||||
@@ -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://<host></code>:
|
||||
<pre><code>wstcp --bind-addr 127.0.0.1:55688 <host>: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>
|
||||
|
||||
17
packages/tutorial/nginx.conf
Normal file
17
packages/tutorial/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
45
packages/tutorial/package.json
Normal file
45
packages/tutorial/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
packages/tutorial/postcss.config.js
Normal file
6
packages/tutorial/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
packages/tutorial/public/favicon.ico
Normal file
BIN
packages/tutorial/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
67
packages/tutorial/src/App.tsx
Normal file
67
packages/tutorial/src/App.tsx
Normal 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;
|
||||
76
packages/tutorial/src/components/challenges/HintSystem.tsx
Normal file
76
packages/tutorial/src/components/challenges/HintSystem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
131
packages/tutorial/src/components/challenges/InteractiveQuiz.tsx
Normal file
131
packages/tutorial/src/components/challenges/InteractiveQuiz.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
22
packages/tutorial/src/components/layout/Footer.tsx
Normal file
22
packages/tutorial/src/components/layout/Footer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
21
packages/tutorial/src/components/layout/Header.tsx
Normal file
21
packages/tutorial/src/components/layout/Header.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
80
packages/tutorial/src/components/layout/Sidebar.tsx
Normal file
80
packages/tutorial/src/components/layout/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
40
packages/tutorial/src/components/shared/Button.tsx
Normal file
40
packages/tutorial/src/components/shared/Button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
73
packages/tutorial/src/components/shared/CodeEditor.tsx
Normal file
73
packages/tutorial/src/components/shared/CodeEditor.tsx
Normal 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" />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
54
packages/tutorial/src/components/shared/ConsoleOutput.tsx
Normal file
54
packages/tutorial/src/components/shared/ConsoleOutput.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
27
packages/tutorial/src/components/shared/ProgressBar.tsx
Normal file
27
packages/tutorial/src/components/shared/ProgressBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
35
packages/tutorial/src/components/shared/StatusBadge.tsx
Normal file
35
packages/tutorial/src/components/shared/StatusBadge.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
119
packages/tutorial/src/context/TutorialContext.tsx
Normal file
119
packages/tutorial/src/context/TutorialContext.tsx
Normal 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;
|
||||
};
|
||||
30
packages/tutorial/src/hooks/useCodeValidation.ts
Normal file
30
packages/tutorial/src/hooks/useCodeValidation.ts
Normal 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 };
|
||||
};
|
||||
54
packages/tutorial/src/hooks/usePluginExecution.ts
Normal file
54
packages/tutorial/src/hooks/usePluginExecution.ts
Normal 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 };
|
||||
};
|
||||
48
packages/tutorial/src/hooks/useStepProgress.ts
Normal file
48
packages/tutorial/src/hooks/useStepProgress.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
10
packages/tutorial/src/main.tsx
Normal file
10
packages/tutorial/src/main.tsx
Normal 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>
|
||||
);
|
||||
159
packages/tutorial/src/pages/Challenge.tsx
Normal file
159
packages/tutorial/src/pages/Challenge.tsx
Normal 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'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>"CHF":"275_000_000"</code> or{' '}
|
||||
<code>"CHF":"125_000_000"</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>"CHF":"50_000_000"</code> and other currency balances)
|
||||
</li>
|
||||
<li>
|
||||
The naive verifier concatenates all revealed parts - what happens if you reveal{' '}
|
||||
<code>"CHF":"50_000_000"</code> and{' '}
|
||||
<code>"EUR":"225_000_000"</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've successfully exploited the naive verifier! This demonstrates why proper
|
||||
verification logic is critical.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
69
packages/tutorial/src/pages/Completion.tsx
Normal file
69
packages/tutorial/src/pages/Completion.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
93
packages/tutorial/src/pages/Concepts.tsx
Normal file
93
packages/tutorial/src/pages/Concepts.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
94
packages/tutorial/src/pages/Setup.tsx
Normal file
94
packages/tutorial/src/pages/Setup.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
293
packages/tutorial/src/pages/SwissBankAdvanced.tsx
Normal file
293
packages/tutorial/src/pages/SwissBankAdvanced.tsx
Normal 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'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>
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL',
|
||||
params: { type: 'json', path: 'accounts.USD' }
|
||||
}
|
||||
</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>
|
||||
{ type: 'SENT', part: 'HEADERS', action:
|
||||
'REVEAL', params: { key: 'cookie' } }
|
||||
</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>
|
||||
{ type: 'RECV', part: 'HEADERS', action:
|
||||
'REVEAL', params: { key: 'date' } }
|
||||
</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's
|
||||
available:
|
||||
</p>
|
||||
<div className="bg-white p-2 rounded space-y-1">
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL'
|
||||
} // See all response body
|
||||
</p>
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'SENT', part: 'HEADERS', action:
|
||||
'REVEAL' } // See all request headers
|
||||
</p>
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'RECV', part: 'HEADERS', action:
|
||||
'REVEAL' } // 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: { type: 'json', path: 'parent.child' }
|
||||
</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: { key: 'header-name' }
|
||||
</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'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>
|
||||
);
|
||||
};
|
||||
167
packages/tutorial/src/pages/SwissBankBasic.tsx
Normal file
167
packages/tutorial/src/pages/SwissBankBasic.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
81
packages/tutorial/src/pages/TwitterExample.tsx
Normal file
81
packages/tutorial/src/pages/TwitterExample.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
71
packages/tutorial/src/pages/Welcome.tsx
Normal file
71
packages/tutorial/src/pages/Welcome.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
122
packages/tutorial/src/styles/index.css
Normal file
122
packages/tutorial/src/styles/index.css
Normal 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;
|
||||
}
|
||||
103
packages/tutorial/src/types.ts
Normal file
103
packages/tutorial/src/types.ts
Normal 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;
|
||||
}
|
||||
69
packages/tutorial/src/utils/checks.ts
Normal file
69
packages/tutorial/src/utils/checks.ts
Normal 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.',
|
||||
},
|
||||
];
|
||||
};
|
||||
10
packages/tutorial/src/utils/config.ts
Normal file
10
packages/tutorial/src/utils/config.ts
Normal 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';
|
||||
},
|
||||
};
|
||||
71
packages/tutorial/src/utils/storage.ts
Normal file
71
packages/tutorial/src/utils/storage.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
238
packages/tutorial/src/utils/validation.ts
Normal file
238
packages/tutorial/src/utils/validation.ts
Normal 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
|
||||
];
|
||||
16
packages/tutorial/tailwind.config.js
Normal file
16
packages/tutorial/tailwind.config.js
Normal 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: [],
|
||||
};
|
||||
22
packages/tutorial/tsconfig.json
Normal file
22
packages/tutorial/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
packages/tutorial/tsconfig.node.json
Normal file
10
packages/tutorial/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "build-plugins.js"]
|
||||
}
|
||||
34
packages/tutorial/vite.config.ts
Normal file
34
packages/tutorial/vite.config.ts
Normal 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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user