Compare commits

..

9 Commits

Author SHA1 Message Date
tsukino
b4a1cc8b2e feat(tutorial): add interactive code editor to Step 6 challenge
- Added CodeMirror editor with validation for Break the Verifier challenge
- Validator now concatenates all revealed values to show redacted transcript
- Uses regex to validate inflated CHF amounts (275_000_000 or 125_000_000)
- Shows actual redacted transcript in validation messages
- Auto-marks step complete when validation passes
- Added reset button functionality
- Improved hints to explain exploit clearly
2026-01-20 22:39:41 +08:00
tsukino
09e107a871 feat(tutorial): enhance Swiss Bank challenges with unified validation
- Unified challenge validation: all 3 challenges validated together on single test
- Added persistent challenge completion tracking with localStorage
- Removed PEDERSEN references (not yet implemented)
- Updated validators to require part: 'BODY' and part: 'HEADERS' explicitly
- Added reset button to Swiss Bank Basic step
- Fixed step completion bug by using validate() return value instead of stale state
- Added backward compatibility for localStorage schema migration
- Reduced CodeEditor font size to 13px for better readability
- Consolidated documentation with inspection-first approach
2026-01-20 22:25:35 +08:00
tsukino
3ea0300f65 feat(tutorial): rewrite as interactive React app with Vite + Tailwind
Transform the static HTML tutorial into a modern, interactive educational platform:

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

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

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

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

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

Updated root package.json tutorial script to use new Vite dev server
2026-01-20 18:00:58 +08:00
Hendrik Eeckhaut
af9148ee4a fix: removed old tlsn-js dependency (#224) 2026-01-20 09:39:36 +01:00
Hendrik Eeckhaut
6331f03940 feat(demo): show git hash in footer (#222) 2026-01-20 16:36:35 +08:00
tsukino
3b4554cd2a chore: update lock file and extension version (#223) 2026-01-20 16:29:22 +08:00
Hendrik Eeckhaut
367479fd77 feat: tlsn updated to 0.14 (#221)
* tlsn updated to 0.14

* package lock

* package lock
2026-01-19 17:24:32 +08:00
Hendrik Eeckhaut
d42db516c7 build: fix demo plugins build (#220) 2026-01-14 16:55:22 +01:00
Hendrik Eeckhaut
1b776929d8 Demo fix (#219)
build: Corrected verifier host + build cleanup (demo)
2026-01-14 14:30:52 +01:00
99 changed files with 4641 additions and 3879 deletions

View File

@@ -86,5 +86,5 @@ jobs:
tags: ${{ steps.meta-verifier-webapp.outputs.tags }}
labels: ${{ steps.meta-verifier-webapp.outputs.labels }}
build-args: |
VERIFIER_HOST=demo-staging.tlsnotary.org
SSL=true
VITE_VERIFIER_HOST=demo-staging.tlsnotary.org
VITE_SSL=true

View File

@@ -594,34 +594,28 @@ Docker-based demo environment for testing plugins:
- `twitter.js`, `swissbank.js` - Example plugin files
- `docker-compose.yml` - Docker services configuration
- `nginx.conf` - Reverse proxy configuration
- `start.sh` - Setup script with URL templating
**Docker Services:**
1. `verifier` - TLSNotary verifier server (port 7047)
2. `demo-static` - nginx serving static plugin files
3. `nginx` - Reverse proxy (port 80)
**Environment Variables:**
- `VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
- `SSL` - Use https/wss protocols (default: `false`)
**Environment Variables (via `.env` files or Docker build args):**
- `VITE_VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
- `VITE_SSL` - Use https/wss protocols (default: `false`)
**Usage:**
```bash
# Local development
./start.sh
# Local development with npm
npm run demo
# Production with SSL
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./start.sh
# Docker (detached mode)
npm run docker:up
# Docker detached mode
./start.sh -d
# Docker with custom verifier
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
```
The `start.sh` script:
1. Processes plugin files, replacing `verifierUrl` and `proxyUrl` placeholders
2. Copies processed files to `generated/` directory
3. Starts Docker Compose services
## Important Implementation Notes
### Plugin API Changes

View File

@@ -58,8 +58,7 @@ tlsn-extension/
│ │
│ ├── demo/ # Demo server with Docker setup
│ │ ├── *.js # Example plugin files
│ │ ── docker-compose.yml # Docker services configuration
│ │ └── start.sh # Setup script with configurable URLs
│ │ ── docker-compose.yml # Docker services configuration
│ │
│ ├── tutorial/ # Tutorial examples
│ │ └── *.js # Tutorial plugin files
@@ -116,10 +115,9 @@ Rust-based HTTP/WebSocket server for TLSNotary verification:
#### 5. **demo** - Demo Server
Docker-based demo environment with:
- Pre-configured example plugins (Twitter, SwissBank)
- React + Vite frontend with environment-based configuration
- Docker Compose setup with verifier and nginx
- Configurable verifier URLs via environment variables
- Plugin file generator (`generate.sh`) with SSL support
- Docker startup script (`start.sh`)
- Configurable verifier URLs via `.env` files or Docker build args
#### 6. **tlsn-wasm-pkg** - TLSN WebAssembly Package
Pre-built WebAssembly binaries for TLSNotary functionality in the browser.
@@ -495,25 +493,19 @@ npm run demo
### Environment Variables
Configure the demo for different environments:
The demo uses `.env` files for configuration:
- `.env` - Local development defaults (`localhost:7047`)
- `.env.production` - Production settings (`verifier.tlsnotary.org`, SSL enabled)
For Docker deployments, override via environment variables:
```bash
# Local development (default)
cd packages/demo
./generate.sh && ./start.sh
npm run docker:up
# Production with SSL
cd packages/demo
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
./start.sh
# Docker detached mode
./generate.sh && ./start.sh -d
# Production with custom verifier
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
```
The demo uses two scripts:
- **`generate.sh`** - Generates plugin files with configured verifier URLs (use environment variables here)
- **`start.sh`** - Starts Docker Compose services (assumes `generated/` directory exists)
### Tutorial
```bash

743
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "tlsn-monorepo",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tlsn-monorepo",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -4864,8 +4864,8 @@
"resolved": "packages/plugin-sdk",
"link": true
},
"node_modules/@tlsn/ts-plugin-sample": {
"resolved": "packages/ts-plugin-sample",
"node_modules/@tlsn/tutorial": {
"resolved": "packages/tutorial",
"link": true
},
"node_modules/@tlsnotary/demo": {
@@ -15660,20 +15660,6 @@
"node": ">=14.0.0"
}
},
"node_modules/tlsn-js": {
"version": "0.1.0-alpha.12.0",
"license": "ISC",
"dependencies": {
"tlsn-wasm": "0.1.0-alpha.12"
},
"engines": {
"node": ">= 16.20.2"
}
},
"node_modules/tlsn-js/node_modules/tlsn-wasm": {
"version": "0.1.0-alpha.12",
"license": "MIT OR Apache-2.0"
},
"node_modules/tlsn-wasm": {
"resolved": "packages/tlsn-wasm-pkg",
"link": true
@@ -15728,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"
@@ -17416,7 +17415,7 @@
}
},
"packages/extension": {
"version": "0.1.0.1300",
"version": "0.1.0.1400",
"license": "MIT",
"dependencies": {
"@codemirror/lang-javascript": "^6.2.4",
@@ -17446,7 +17445,6 @@
"redux-thunk": "^2.4.2",
"stream-browserify": "^3.0.0",
"tailwindcss": "^3.3.3",
"tlsn-js": "^0.1.0-alpha.12.0",
"tlsn-wasm": "./lib/tlsn-wasm-pkg/",
"util": "^0.12.5"
},
@@ -17513,10 +17511,13 @@
},
"packages/extension/lib/tlsn-wasm-pkg": {
"name": "tlsn-wasm",
"version": "0.1.0-alpha.13",
"extraneous": true,
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0"
},
"packages/extension/node_modules/tlsn-wasm": {
"resolved": "packages/extension/lib/tlsn-wasm-pkg",
"link": true
},
"packages/plugin-sdk": {
"name": "@tlsn/plugin-sdk",
"version": "0.1.0",
@@ -17555,25 +17556,44 @@
},
"packages/tlsn-wasm-pkg": {
"name": "tlsn-wasm",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0"
},
"packages/ts-plugin-sample": {
"name": "@tlsn/ts-plugin-sample",
"version": "0.1.0-alpha.13",
"license": "MIT",
"packages/tutorial": {
"name": "@tlsn/tutorial",
"version": "0.1.0",
"dependencies": {
"@tlsn/plugin-sdk": "*"
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"esbuild": "^0.24.2",
"typescript": "^5.5.4"
"@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/ts-plugin-sample/node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"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"
],
@@ -17584,13 +17604,13 @@
"aix"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"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"
],
@@ -17601,13 +17621,13 @@
"android"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"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"
],
@@ -17618,13 +17638,13 @@
"android"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"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"
],
@@ -17635,13 +17655,13 @@
"android"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"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"
],
@@ -17652,13 +17672,13 @@
"darwin"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"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"
],
@@ -17669,13 +17689,13 @@
"darwin"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"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"
],
@@ -17686,13 +17706,13 @@
"freebsd"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"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"
],
@@ -17703,13 +17723,13 @@
"freebsd"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"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"
],
@@ -17720,13 +17740,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"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"
],
@@ -17737,13 +17757,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"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"
],
@@ -17754,13 +17774,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"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"
],
@@ -17771,13 +17791,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"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"
],
@@ -17788,13 +17808,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"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"
],
@@ -17805,13 +17825,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"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"
],
@@ -17822,13 +17842,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"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"
],
@@ -17839,13 +17859,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"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"
],
@@ -17856,30 +17876,13 @@
"linux"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"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"
],
@@ -17890,30 +17893,13 @@
"netbsd"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"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"
],
@@ -17924,13 +17910,13 @@
"openbsd"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"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"
],
@@ -17941,13 +17927,13 @@
"sunos"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"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"
],
@@ -17958,13 +17944,13 @@
"win32"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"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"
],
@@ -17975,13 +17961,13 @@
"win32"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"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"
],
@@ -17992,13 +17978,221 @@
"win32"
],
"engines": {
"node": ">=18"
"node": ">=12"
}
},
"packages/ts-plugin-sample/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"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",
@@ -18006,34 +18200,165 @@
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"packages/tutorial/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/tutorial/node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.2.9",
"ignore": "^5.2.0",
"merge2": "^1.4.1",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/tutorial/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/tutorial/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"packages/tutorial/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"packages/tutorial/node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-monorepo",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"private": true,
"description": "TLSN Extension monorepo with plugin SDK",
"license": "MIT",
@@ -22,11 +22,11 @@
"test": "npm run test --workspaces --if-present",
"serve:test": "npm run serve:test --workspace=extension",
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.13 --no-logging",
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.14 --no-logging",
"demo": "npm run dev --workspace=@tlsnotary/demo",
"tutorial": "serve -l 8080 packages/tutorial",
"docker:up": "cd packages/demo && ./start.sh -d",
"docker:down": "cd packages/demo && docker-compose down"
"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"
},
"devDependencies": {
"typescript": "^5.5.4",

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
FROM node:20-alpine AS builder
# Accept build arguments with defaults
ARG VITE_VERIFIER_URL=http://localhost:7047
ARG VITE_PROXY_URL=ws://localhost:7047/proxy?token=
ARG VITE_VERIFIER_HOST=localhost:7047
ARG VITE_SSL=false
WORKDIR /app
@@ -15,8 +15,8 @@ RUN npm install
COPY . .
# Build with environment variables
ENV VITE_VERIFIER_URL=${VITE_VERIFIER_URL}
ENV VITE_PROXY_URL=${VITE_PROXY_URL}
ENV VITE_VERIFIER_HOST=${VITE_VERIFIER_HOST}
ENV VITE_SSL=${VITE_SSL}
RUN npm run build
# Runtime stage

View File

@@ -75,30 +75,26 @@ Run the demo with `npm run demo` from the repository root, or run it with docker
#### Manual Docker Setup
If you want to run the scripts manually:
If you want to run Docker manually:
```bash
cd packages/demo
npm run build # Build the React app first
./generate.sh && ./start.sh
docker compose up --build
```
The demo uses two scripts:
- **`generate.sh`** - Generates plugin files with configured verifier URLs
- **`start.sh`** - Starts Docker Compose services
#### Environment Variables
Configure for different environments:
The demo uses `.env` files for configuration:
- `.env` - Local development defaults (`localhost:7047`)
- `.env.production` - Production settings (`verifier.tlsnotary.org`, SSL enabled)
For Docker deployments, override via environment variables:
```bash
# Local development (default)
npm run build
./generate.sh && ./start.sh
docker compose up --build
# Production with SSL
npm run build
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
./start.sh
# Production with custom verifier
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
```
You can now open the demo by opening http://localhost:8080 in your browser with the TLSNotary extension

View File

@@ -7,6 +7,13 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const plugins = ['twitter', 'swissbank', 'spotify'];
// Build URLs from environment variables (matching config.ts pattern)
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';
const SSL = process.env.VITE_SSL === 'true';
const VERIFIER_URL = `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`;
const PROXY_URL = `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=`;
// Build each plugin separately as plain ES module
for (const plugin of plugins) {
await build({
@@ -28,12 +35,8 @@ for (const plugin of plugins) {
},
},
define: {
VITE_VERIFIER_URL: JSON.stringify(
process.env.VITE_VERIFIER_URL || 'http://localhost:7047'
),
VITE_PROXY_URL: JSON.stringify(
process.env.VITE_PROXY_URL || 'ws://localhost:7047/proxy?token='
),
VITE_VERIFIER_URL: JSON.stringify(VERIFIER_URL),
VITE_PROXY_URL: JSON.stringify(PROXY_URL),
},
});
console.log(`✓ Built ${plugin}.js`);

View File

@@ -15,8 +15,8 @@ services:
build:
context: .
args:
VERIFIER_HOST: ${VERIFIER_HOST:-localhost:7047}
SSL: ${SSL:-false}
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
VITE_SSL: ${VITE_SSL:-false}
restart: unless-stopped
nginx:

View File

@@ -1,105 +0,0 @@
#!/bin/sh
#
# Demo Plugin File Generator
#
# This script generates plugin files with configurable verifier URLs.
# Used both locally and in CI/CD pipelines.
#
# Environment Variables:
# VERIFIER_HOST - Verifier server host (default: localhost:7047)
# SSL - Use https/wss if true (default: false)
#
# Usage:
# ./generate.sh # Local development
# VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh # Production
set -e
cd "$(dirname "$0")"
# Configuration with defaults
VERIFIER_HOST="${VERIFIER_HOST:-localhost:7047}"
SSL="${SSL:-false}"
# Determine protocol based on SSL setting
if [ "$SSL" = "true" ]; then
HTTP_PROTOCOL="https"
WS_PROTOCOL="wss"
else
HTTP_PROTOCOL="http"
WS_PROTOCOL="ws"
fi
VERIFIER_URL="${HTTP_PROTOCOL}://${VERIFIER_HOST}"
PROXY_URL_BASE="${WS_PROTOCOL}://${VERIFIER_HOST}/proxy?token="
echo "========================================"
echo "TLSNotary Demo Plugin Generator"
echo "========================================"
echo "Verifier Host: $VERIFIER_HOST"
echo "SSL Enabled: $SSL"
echo "Verifier URL: $VERIFIER_URL"
echo "Proxy URL: ${PROXY_URL_BASE}<host>"
echo "========================================"
# Create generated directory for processed files
mkdir -p generated
# Function to process a plugin file
process_plugin() {
local input_file="$1"
local output_file="generated/$(basename "$input_file")"
echo "Processing: $input_file -> $output_file"
# Replace verifierUrl and proxyUrl patterns
sed -E \
-e "s|verifierUrl: '[^']*'|verifierUrl: '${VERIFIER_URL}'|g" \
-e "s|verifierUrl: \"[^\"]*\"|verifierUrl: \"${VERIFIER_URL}\"|g" \
-e "s|proxyUrl: 'ws://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
-e "s|proxyUrl: 'wss://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
-e "s|proxyUrl: \"ws://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
-e "s|proxyUrl: \"wss://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
"$input_file" > "$output_file"
}
# Function to process index.html
process_index_html() {
local input_file="$1"
local output_file="generated/$(basename "$input_file")"
echo "Processing: $input_file -> $output_file"
# Replace hardcoded health check URL with configured verifier URL
sed -E \
-e "s|http://localhost:7047/health|${VERIFIER_URL}/health|g" \
"$input_file" > "$output_file"
}
# Process index.html
echo ""
echo "Processing index.html..."
process_index_html "index.html"
# Copy other static files
echo ""
echo "Copying other static files..."
cp favicon.ico generated/ 2>/dev/null || true
# Process plugin files
echo ""
echo "Processing plugin files..."
for plugin_file in *.js; do
if [ -f "$plugin_file" ]; then
process_plugin "$plugin_file"
fi
done
echo ""
echo "Generated files:"
ls -la generated/
echo ""
echo "========================================"
echo "Generation complete!"
echo "========================================"

View File

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

View File

@@ -1225,4 +1225,34 @@ body {
.resource-desc {
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
}
/* App Footer */
.app-footer {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-xl);
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
}
.footer-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.footer-link:hover {
color: white;
text-decoration: underline;
}
.footer-version {
color: rgba(255, 255, 255, 0.6);
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', monospace;
font-size: 0.8125rem;
}

View File

@@ -280,6 +280,20 @@ export function App() {
onOpenExtensionLogs={handleOpenExtensionLogs}
/>
</CollapsibleSection>
<footer className="app-footer">
<a
href="https://github.com/tlsnotary/tlsn-extension/tree/main/packages/demo"
target="_blank"
rel="noopener noreferrer"
className="footer-link"
>
View source on GitHub
</a>
<span className="footer-version">v{__GIT_COMMIT_HASH__}</span>
</footer>
</div>
);
}
declare const __GIT_COMMIT_HASH__: string;

View File

@@ -2,10 +2,9 @@
// Reads from Vite's import.meta.env (populated from .env files)
const VERIFIER_HOST = (import.meta as any).env.VITE_VERIFIER_HOST || 'localhost:7047';
const VERIFIER_PROTOCOL = (import.meta as any).env.VITE_VERIFIER_PROTOCOL || 'http';
const PROXY_PROTOCOL = (import.meta as any).env.VITE_PROXY_PROTOCOL || 'ws';
const SSL = (import.meta as any).env.VITE_SSL === 'true';
export const config = {
verifierUrl: `${VERIFIER_PROTOCOL}://${VERIFIER_HOST}`,
getProxyUrl: (host: string) => `${PROXY_PROTOCOL}://${VERIFIER_HOST}/proxy?token=${host}`,
verifierUrl: `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`,
getProxyUrl: (host: string) => `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=${host}`,
};

View File

@@ -1,3 +1,5 @@
import { config } from './config';
export function checkBrowserCompatibility(): boolean {
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
const isEdge = /Edg/.test(navigator.userAgent);
@@ -15,7 +17,7 @@ export async function checkExtension(): Promise<boolean> {
export async function checkVerifier(): Promise<boolean> {
try {
const response = await fetch('http://localhost:7047/health');
const response = await fetch(`${config.verifierUrl}/health`);
if (response.ok && (await response.text()) === 'ok') {
return true;
}

View File

@@ -1,31 +0,0 @@
#!/bin/sh
#
# Demo Server Startup Script
#
# This script starts the verifier server and demo file server via Docker.
# Note: Run generate.sh first to create plugin files in the generated/ directory.
#
# Usage:
# ./generate.sh && ./start.sh # Generate and start
# ./start.sh # Start only (assumes generated/ exists)
# ./start.sh -d # Start in detached mode
set -e
cd "$(dirname "$0")"
# Check if generated directory exists
if [ ! -d "generated" ]; then
echo "ERROR: generated/ directory not found!"
echo "Please run ./generate.sh first to create plugin files."
exit 1
fi
echo "========================================"
echo "TLSNotary Demo Server"
echo "========================================"
echo "Starting Docker services..."
echo "========================================"
# Start docker compose
docker compose up --build "$@"

View File

@@ -1,7 +1,20 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { execSync } from 'child_process';
// Get git commit hash at build time
const getGitCommitHash = () => {
try {
return execSync('git rev-parse --short HEAD').toString().trim();
} catch {
return 'unknown';
}
};
export default defineConfig({
define: {
__GIT_COMMIT_HASH__: JSON.stringify(getGitCommitHash()),
},
plugins: [react()],
build: {
outDir: 'dist',
@@ -10,5 +23,8 @@ export default defineConfig({
server: {
port: 3000,
open: true,
headers: {
'Cache-Control': 'no-store',
},
},
});

View File

@@ -1,6 +1,6 @@
{
"name": "extension",
"version": "0.1.0.1300",
"version": "0.1.0.1400",
"license": "MIT",
"repository": {
"type": "git",
@@ -47,7 +47,6 @@
"redux-thunk": "^2.4.2",
"stream-browserify": "^3.0.0",
"tailwindcss": "^3.3.3",
"tlsn-js": "^0.1.0-alpha.12.0",
"tlsn-wasm": "./lib/tlsn-wasm-pkg/",
"util": "^0.12.5"
},
@@ -111,4 +110,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"manifest_version": 3,
"version": "0.1.0.13",
"version": "0.1.0.1400",
"name": "TLSNotary",
"description": "A Chrome extension for TLSNotary",
"options_page": "options.html",
@@ -15,18 +15,38 @@
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["contentScript.bundle.js"],
"css": ["content.styles.css"]
"matches": [
"http://*/*",
"https://*/*",
"<all_urls>"
],
"js": [
"contentScript.bundle.js"
],
"css": [
"content.styles.css"
]
}
],
"web_accessible_resources": [
{
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "*.wasm"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
"resources": [
"content.styles.css",
"icon-128.png",
"icon-34.png",
"content.bundle.js",
"*.wasm"
],
"matches": [
"http://*/*",
"https://*/*",
"<all_urls>"
]
}
],
"host_permissions": ["<all_urls>"],
"host_permissions": [
"<all_urls>"
],
"permissions": [
"offscreen",
"webRequest",

View File

@@ -1,6 +1,6 @@
import Host, { Parser } from '@tlsn/plugin-sdk/src';
import { ProveManager } from './ProveManager';
import { Method } from 'tlsn-js';
import type { Method } from '../../../tlsn-wasm-pkg/tlsn_wasm';
import { DomJson, Handler, PluginConfig } from '@tlsn/plugin-sdk/src/types';
import { processHandlers } from './rangeExtractor';
import { logger } from '@tlsn/common';

View File

@@ -5,24 +5,6 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles": {
"import": "./dist/styles.js",
"types": "./dist/styles.d.ts"
},
"./src": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./src/types": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "vite build",
"test": "vitest",

View File

@@ -2,7 +2,9 @@
* Global type declarations for TLSNotary plugin runtime environment
*
* These functions are injected at runtime by the plugin sandbox.
* They are automatically available as globals in TypeScript plugins.
* Import this file in your plugin to get TypeScript support:
*
* /// <reference types="@tlsn/plugin-sdk/globals" />
*/
import type {
@@ -13,123 +15,83 @@ import type {
DomJson,
} from './types';
/**
* Create a div DOM element
*/
export type DivFunction = {
(options?: DomOptions, children?: DomJson[]): DomJson;
(children: DomJson[]): DomJson;
};
/**
* Create a button DOM element
*/
export type ButtonFunction = {
(options?: DomOptions, children?: DomJson[]): DomJson;
(children: DomJson[]): DomJson;
};
/**
* Open a new browser window
*/
export type OpenWindowFunction = (
url: string,
options?: {
width?: number;
height?: number;
showOverlay?: boolean;
}
) => Promise<{
windowId: number;
uuid: string;
tabId: number;
}>;
/**
* React-like effect hook that runs when dependencies change
*/
export type UseEffectFunction = (callback: () => void, deps: any[]) => void;
/**
* Subscribe to intercepted HTTP headers with filtering
*/
export type UseHeadersFunction = (
filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[]
) => InterceptedRequestHeader[];
/**
* Subscribe to intercepted HTTP requests with filtering
*/
export type UseRequestsFunction = (
filter: (requests: InterceptedRequest[]) => InterceptedRequest[]
) => InterceptedRequest[];
/**
* Get state value (does not trigger re-render)
*/
export type UseStateFunction = <T>(key: string, defaultValue: T) => T;
/**
* Set state value (triggers UI re-render)
*/
export type SetStateFunction = <T>(key: string, value: T) => void;
/**
* Generate TLS proof using the unified prove() API
*/
export type ProveFunction = (
requestOptions: {
url: string;
method: string;
headers: Record<string, string | undefined>;
body?: string;
},
proverOptions: {
verifierUrl: string;
proxyUrl: string;
maxRecvData?: number;
maxSentData?: number;
handlers: Handler[];
}
) => Promise<any>;
/**
* Complete plugin execution and return result
*/
export type DoneFunction = (result?: any) => void;
/**
* Complete Plugin API surface available in the QuickJS sandbox
*/
export interface PluginAPI {
div: DivFunction;
button: ButtonFunction;
openWindow: OpenWindowFunction;
useEffect: UseEffectFunction;
useHeaders: UseHeadersFunction;
useRequests: UseRequestsFunction;
useState: UseStateFunction;
setState: SetStateFunction;
prove: ProveFunction;
done: DoneFunction;
}
/**
* Global declarations for plugin environment
*
* These are automatically available in TypeScript plugins without imports.
*/
declare global {
const div: DivFunction;
const button: ButtonFunction;
const openWindow: OpenWindowFunction;
const useEffect: UseEffectFunction;
const useHeaders: UseHeadersFunction;
const useRequests: UseRequestsFunction;
const useState: UseStateFunction;
const setState: SetStateFunction;
const prove: ProveFunction;
const done: DoneFunction;
/**
* Create a div element
*/
function div(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
function div(children?: (DomJson | string)[]): DomJson;
/**
* Create a button element
*/
function button(options?: DomOptions, children?: (DomJson | string)[]): DomJson;
function button(children?: (DomJson | string)[]): DomJson;
/**
* Get or initialize state value (React-like useState)
*/
function useState<T>(key: string, initialValue: T): T;
/**
* Update state value
*/
function setState<T>(key: string, value: T): void;
/**
* Run side effect when dependencies change (React-like useEffect)
*/
function useEffect(effect: () => void, deps: any[]): void;
/**
* Subscribe to intercepted HTTP headers
*/
function useHeaders(
filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[],
): [InterceptedRequestHeader | undefined];
/**
* Subscribe to intercepted HTTP requests
*/
function useRequests(
filter: (requests: InterceptedRequest[]) => InterceptedRequest[],
): [InterceptedRequest | undefined];
/**
* Open a new browser window for user interaction
*/
function openWindow(
url: string,
options?: {
width?: number;
height?: number;
showOverlay?: boolean;
},
): Promise<void>;
/**
* Generate a TLS proof for an HTTP request
*/
function prove(
requestOptions: {
url: string;
method: string;
headers: Record<string, string>;
body?: string;
},
proverOptions: {
verifierUrl: string;
proxyUrl: string;
maxRecvData?: number;
maxSentData?: number;
handlers: Handler[];
},
): Promise<any>;
/**
* Complete plugin execution and return result
*/
function done(result?: any): void;
}
export {};

View File

@@ -796,40 +796,7 @@ export async function extractConfig(code: string): Promise<PluginConfig | null>
}
// Export types
export type {
PluginConfig,
RequestPermission,
Handler,
StartLineHandler,
HeadersHandler,
BodyHandler,
AllHandler,
HandlerType,
HandlerPart,
HandlerAction,
InterceptedRequest,
InterceptedRequestHeader,
DomJson,
DomOptions,
OpenWindowResponse,
WindowMessage,
ExecutionContext,
} from './types';
// Export Plugin API types
export type {
PluginAPI,
DivFunction,
ButtonFunction,
OpenWindowFunction,
UseEffectFunction,
UseHeadersFunction,
UseRequestsFunction,
UseStateFunction,
SetStateFunction,
ProveFunction,
DoneFunction,
} from './globals';
export type { PluginConfig, RequestPermission };
// Re-export LogLevel for consumers
export { LogLevel } from '@tlsn/common';

View File

@@ -1,361 +0,0 @@
/**
* Tailwind-like style utilities for plugin UI components
*/
// =============================================================================
// DESIGN TOKENS
// =============================================================================
/**
* Color palette with Tailwind-like naming
* Non-opinionated color scales from 100-900
*/
const colorTokens = {
// Neutral
'white': '#ffffff',
'black': '#000000',
'transparent': 'transparent',
// Gray scale
'gray-50': '#f9fafb',
'gray-100': '#f3f4f6',
'gray-200': '#e5e7eb',
'gray-300': '#d1d5db',
'gray-400': '#9ca3af',
'gray-500': '#6b7280',
'gray-600': '#4b5563',
'gray-700': '#374151',
'gray-800': '#1f2937',
'gray-900': '#111827',
// Blue
'blue-100': '#dbeafe',
'blue-200': '#bfdbfe',
'blue-300': '#93c5fd',
'blue-400': '#60a5fa',
'blue-500': '#3b82f6',
'blue-600': '#2563eb',
'blue-700': '#1d4ed8',
'blue-800': '#1e40af',
'blue-900': '#1e3a8a',
// Purple
'purple-100': '#f3e8ff',
'purple-200': '#e9d5ff',
'purple-300': '#d8b4fe',
'purple-400': '#c084fc',
'purple-500': '#a855f7',
'purple-600': '#9333ea',
'purple-700': '#7e22ce',
'purple-800': '#6b21a8',
'purple-900': '#581c87',
// Red
'red-100': '#fee2e2',
'red-200': '#fecaca',
'red-300': '#fca5a5',
'red-400': '#f87171',
'red-500': '#ef4444',
'red-600': '#dc2626',
'red-700': '#b91c1c',
'red-800': '#991b1b',
'red-900': '#7f1d1d',
// Yellow
'yellow-100': '#fef3c7',
'yellow-200': '#fde68a',
'yellow-300': '#fcd34d',
'yellow-400': '#fbbf24',
'yellow-500': '#f59e0b',
'yellow-600': '#d97706',
'yellow-700': '#b45309',
'yellow-800': '#92400e',
'yellow-900': '#78350f',
// Orange
'orange-100': '#ffedd5',
'orange-200': '#fed7aa',
'orange-300': '#fdba74',
'orange-400': '#fb923c',
'orange-500': '#f97316',
'orange-600': '#ea580c',
'orange-700': '#c2410c',
'orange-800': '#9a3412',
'orange-900': '#7c2d12',
// Green
'green-100': '#d1fae5',
'green-200': '#a7f3d0',
'green-300': '#6ee7b7',
'green-400': '#34d399',
'green-500': '#10b981',
'green-600': '#059669',
'green-700': '#047857',
'green-800': '#065f46',
'green-900': '#064e3b',
} as const;
/**
* Spacing scale
*/
const spacingTokens = {
'0': '0',
'1': '4px',
'2': '8px',
'3': '12px',
'4': '16px',
'5': '20px',
'6': '24px',
'8': '32px',
'10': '40px',
'12': '48px',
// Named aliases
'xs': '8px',
'sm': '12px',
'md': '16px',
'lg': '20px',
'xl': '24px',
} as const;
/**
* Font sizes
*/
const fontSizeTokens = {
'xs': '12px',
'sm': '14px',
'md': '15px',
'base': '16px',
'lg': '18px',
'xl': '20px',
'2xl': '24px',
} as const;
/**
* Font weights
*/
const fontWeightTokens = {
'normal': '400',
'medium': '500',
'semibold': '600',
'bold': '700',
} as const;
/**
* Border radius
*/
const borderRadiusTokens = {
'none': '0',
'sm': '6px',
'md': '8px',
'lg': '12px',
'full': '9999px',
'circle': '50%',
} as const;
/**
* Box shadows
*/
const shadowTokens = {
'sm': '0 2px 4px rgba(0,0,0,0.1)',
'md': '0 -2px 10px rgba(0,0,0,0.1)',
'lg': '0 4px 8px rgba(0,0,0,0.3)',
'xl': '0 10px 25px rgba(0,0,0,0.2)',
} as const;
// =============================================================================
// TYPE DEFINITIONS
// =============================================================================
type StyleObject = Record<string, string>;
type StyleHelper = StyleObject | false | null | undefined;
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Resolve a color token to its CSS value
*/
function resolveColor(token: string): string {
return colorTokens[token as keyof typeof colorTokens] || token;
}
/**
* Resolve a spacing token to its CSS value
*/
function resolveSpacing(token: string): string {
return spacingTokens[token as keyof typeof spacingTokens] || token;
}
/**
* Resolve a font size token to its CSS value
*/
function resolveFontSize(token: string): string {
return fontSizeTokens[token as keyof typeof fontSizeTokens] || token;
}
/**
* Resolve a font weight token to its CSS value
*/
function resolveFontWeight(token: string): string {
return fontWeightTokens[token as keyof typeof fontWeightTokens] || token;
}
/**
* Resolve a border radius token to its CSS value
*/
function resolveBorderRadius(token: string): string {
return borderRadiusTokens[token as keyof typeof borderRadiusTokens] || token;
}
/**
* Resolve a shadow token to its CSS value
*/
function resolveShadow(token: string): string {
return shadowTokens[token as keyof typeof shadowTokens] || token;
}
// =============================================================================
// STYLE HELPER FUNCTIONS
// =============================================================================
// Color helpers
export const color = (value: string): StyleObject => ({ color: resolveColor(value) });
export const bgColor = (value: string): StyleObject => ({ backgroundColor: resolveColor(value) });
export const borderColor = (value: string): StyleObject => ({ borderColor: resolveColor(value) });
export const bg = bgColor; // Alias
// Spacing helpers - Padding
export const padding = (value: string): StyleObject => ({ padding: resolveSpacing(value) });
export const paddingX = (value: string): StyleObject => {
const val = resolveSpacing(value);
return { paddingLeft: val, paddingRight: val };
};
export const paddingY = (value: string): StyleObject => {
const val = resolveSpacing(value);
return { paddingTop: val, paddingBottom: val };
};
export const paddingTop = (value: string): StyleObject => ({ paddingTop: resolveSpacing(value) });
export const paddingBottom = (value: string): StyleObject => ({ paddingBottom: resolveSpacing(value) });
export const paddingLeft = (value: string): StyleObject => ({ paddingLeft: resolveSpacing(value) });
export const paddingRight = (value: string): StyleObject => ({ paddingRight: resolveSpacing(value) });
// Aliases
export const p = padding;
export const px = paddingX;
export const py = paddingY;
export const pt = paddingTop;
export const pb = paddingBottom;
export const pl = paddingLeft;
export const pr = paddingRight;
// Spacing helpers - Margin
export const margin = (value: string): StyleObject => ({ margin: resolveSpacing(value) });
export const marginX = (value: string): StyleObject => {
const val = resolveSpacing(value);
return { marginLeft: val, marginRight: val };
};
export const marginY = (value: string): StyleObject => {
const val = resolveSpacing(value);
return { marginTop: val, marginBottom: val };
};
export const marginTop = (value: string): StyleObject => ({ marginTop: resolveSpacing(value) });
export const marginBottom = (value: string): StyleObject => ({ marginBottom: resolveSpacing(value) });
export const marginLeft = (value: string): StyleObject => ({ marginLeft: resolveSpacing(value) });
export const marginRight = (value: string): StyleObject => ({ marginRight: resolveSpacing(value) });
// Aliases
export const m = margin;
export const mx = marginX;
export const my = marginY;
export const mt = marginTop;
export const mb = marginBottom;
export const ml = marginLeft;
export const mr = marginRight;
// Typography helpers
export const fontSize = (value: string): StyleObject => ({ fontSize: resolveFontSize(value) });
export const fontWeight = (value: string): StyleObject => ({ fontWeight: resolveFontWeight(value) });
export const textAlign = (value: string): StyleObject => ({ textAlign: value });
export const fontFamily = (value: string): StyleObject => ({ fontFamily: value });
// Layout helpers
export const display = (value: string): StyleObject => ({ display: value });
export const position = (value: string): StyleObject => ({ position: value });
export const width = (value: string): StyleObject => ({ width: value });
export const height = (value: string): StyleObject => ({ height: value });
export const minWidth = (value: string): StyleObject => ({ minWidth: value });
export const minHeight = (value: string): StyleObject => ({ minHeight: value });
export const maxWidth = (value: string): StyleObject => ({ maxWidth: value });
export const maxHeight = (value: string): StyleObject => ({ maxHeight: value });
// Flexbox helpers
export const flex = (value: string = '1'): StyleObject => ({ flex: value });
export const flexDirection = (value: string): StyleObject => ({ flexDirection: value });
export const alignItems = (value: string): StyleObject => ({ alignItems: value });
export const justifyContent = (value: string): StyleObject => ({ justifyContent: value });
export const flexWrap = (value: string): StyleObject => ({ flexWrap: value });
// Positioning helpers
export const top = (value: string): StyleObject => ({ top: resolveSpacing(value) });
export const bottom = (value: string): StyleObject => ({ bottom: resolveSpacing(value) });
export const left = (value: string): StyleObject => ({ left: resolveSpacing(value) });
export const right = (value: string): StyleObject => ({ right: resolveSpacing(value) });
// Border helpers
export const border = (value: string): StyleObject => ({ border: value });
export const borderRadius = (value: string): StyleObject => ({ borderRadius: resolveBorderRadius(value) });
export const borderWidth = (value: string): StyleObject => ({ borderWidth: value });
// Visual helpers
export const boxShadow = (value: string): StyleObject => ({ boxShadow: resolveShadow(value) });
export const opacity = (value: string): StyleObject => ({ opacity: value });
export const overflow = (value: string): StyleObject => ({ overflow: value });
export const zIndex = (value: string): StyleObject => ({ zIndex: value });
// Interaction helpers
export const cursor = (value: string): StyleObject => ({ cursor: value });
export const pointerEvents = (value: string): StyleObject => ({ pointerEvents: value });
// Transition/Animation helpers
export const transition = (value: string = 'all 0.2s ease'): StyleObject => ({ transition: value });
// Background helpers
export const background = (value: string): StyleObject => ({ background: value });
// =============================================================================
// MAIN INLINE STYLE FUNCTION
// =============================================================================
/**
* Combine multiple style helpers into a single style object
* Automatically filters out falsey values for conditional styling
*
* @example
* inlineStyle(
* textAlign('center'),
* color('gray-500'),
* padding('sm'),
* bgColor('yellow-100'),
* isPending && display('none'),
* { borderRadius: '12px' }
* )
*/
export function inlineStyle(...styles: StyleHelper[]): StyleObject {
return styles.reduce<StyleObject>((acc, style) => {
if (style) {
Object.assign(acc, style);
}
return acc;
}, {});
}
// =============================================================================
// EXPORTS
// =============================================================================
/**
* Common font family
*/
export const defaultFontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';

View File

@@ -15,16 +15,26 @@ export default defineConfig({
build: {
target: 'es2020',
lib: {
entry: {
index: path.resolve(__dirname, 'src/index.ts'),
styles: path.resolve(__dirname, 'src/styles.ts'),
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'TLSNPluginSDK',
formats: ['es', 'cjs', 'umd'],
fileName: (format) => {
if (format === 'es') return 'index.js';
if (format === 'cjs') return 'index.cjs';
if (format === 'umd') return 'index.umd.js';
return `index.${format}.js`;
},
formats: ['es'],
},
rollupOptions: {
// Externalize QuickJS and Node.js dependencies
external: ['@sebastianwessel/quickjs', '@jitl/quickjs-ng-wasmfile-release-sync', /^node:.*/, '@tlsn/common'],
external: ['@sebastianwessel/quickjs', '@jitl/quickjs-ng-wasmfile-release-sync', /^node:.*/],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
'@sebastianwessel/quickjs': 'QuickJS',
'@jitl/quickjs-ng-wasmfile-release-sync': 'QuickJSVariant',
},
exports: 'named',
},
},

View File

@@ -2,7 +2,7 @@
"name": "tlsn-wasm",
"type": "module",
"description": "A core WebAssembly package for TLSNotary.",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",

View File

@@ -1,19 +1,8 @@
/* tslint:disable */
/* eslint-disable */
/**
* Initializes the module.
*/
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
/**
* Starts the thread spawner on a dedicated worker thread.
*/
export function startSpawner(): Promise<any>;
export function web_spawn_start_worker(worker: number): void;
export function web_spawn_recover_spawner(spawner: number): Spawner;
export interface CrateLogFilter {
level: LoggingLevel;
name: string;
}
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
export type SpanEvent = "New" | "Close" | "Active";
export interface LoggingConfig {
level: LoggingLevel | undefined;
@@ -21,15 +10,43 @@ export interface LoggingConfig {
span_events: SpanEvent[] | undefined;
}
export type SpanEvent = "New" | "Close" | "Active";
export interface CrateLogFilter {
level: LoggingLevel;
name: string;
}
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
export type Body = JsonValue;
export type NetworkSetting = "Bandwidth" | "Latency";
export type Method = "GET" | "POST" | "PUT" | "DELETE";
export interface Commit {
sent: { start: number; end: number }[];
recv: { start: number; end: number }[];
export interface HttpRequest {
uri: string;
method: Method;
headers: Map<string, number[]>;
body: Body | undefined;
}
export interface HttpResponse {
status: number;
headers: [string, number[]][];
}
export type TlsVersion = "V1_2" | "V1_3";
export interface TranscriptLength {
sent: number;
recv: number;
}
export interface ConnectionInfo {
time: number;
version: TlsVersion;
transcript_length: TranscriptLength;
}
export interface Transcript {
sent: number[];
recv: number[];
}
export interface PartialTranscript {
@@ -39,52 +56,25 @@ export interface PartialTranscript {
recv_authed: { start: number; end: number }[];
}
export interface HttpResponse {
status: number;
headers: [string, number[]][];
export interface Commit {
sent: { start: number; end: number }[];
recv: { start: number; end: number }[];
}
export type Body = JsonValue;
export interface VerifierOutput {
server_name: string | undefined;
connection_info: ConnectionInfo;
transcript: PartialTranscript | undefined;
}
export interface ConnectionInfo {
time: number;
version: TlsVersion;
transcript_length: TranscriptLength;
}
export interface TranscriptLength {
sent: number;
recv: number;
}
export type TlsVersion = "V1_2" | "V1_3";
export interface HttpRequest {
uri: string;
method: Method;
headers: Map<string, number[]>;
body: Body | undefined;
}
export type Method = "GET" | "POST" | "PUT" | "DELETE";
export interface Reveal {
sent: { start: number; end: number }[];
recv: { start: number; end: number }[];
server_identity: boolean;
}
export interface Transcript {
sent: number[];
recv: number[];
export interface VerifierOutput {
server_name: string | undefined;
connection_info: ConnectionInfo;
transcript: PartialTranscript | undefined;
}
export type NetworkSetting = "Bandwidth" | "Latency";
export interface ProverConfig {
server_name: string;
max_sent_data: number;
@@ -104,18 +94,14 @@ export interface VerifierConfig {
max_recv_records_online: number | undefined;
}
export class Prover {
free(): void;
[Symbol.dispose](): void;
/**
* Returns the transcript.
*/
transcript(): Transcript;
/**
* Send the HTTP request to the server.
*/
send_request(ws_proxy_url: string, request: HttpRequest): Promise<HttpResponse>;
constructor(config: ProverConfig);
/**
* Set up the prover.
*
@@ -127,10 +113,13 @@ export class Prover {
* Reveals data to the verifier and finalizes the protocol.
*/
reveal(reveal: Reveal): Promise<void>;
/**
* Returns the transcript.
*/
transcript(): Transcript;
constructor(config: ProverConfig);
}
/**
* Global spawner which spawns closures into web workers.
*/
export class Spawner {
private constructor();
free(): void;
@@ -141,10 +130,10 @@ export class Spawner {
run(url: string): Promise<void>;
intoRaw(): number;
}
export class Verifier {
free(): void;
[Symbol.dispose](): void;
constructor(config: VerifierConfig);
/**
* Verifies the connection and finalizes the protocol.
*/
@@ -153,13 +142,29 @@ export class Verifier {
* Connect to the prover.
*/
connect(prover_url: string): Promise<void>;
constructor(config: VerifierConfig);
}
export class WorkerData {
private constructor();
free(): void;
[Symbol.dispose](): void;
}
/**
* Initializes the module.
*/
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
/**
* Starts the thread spawner on a dedicated worker thread.
*/
export function startSpawner(): Promise<any>;
export function web_spawn_recover_spawner(spawner: number): Spawner;
export function web_spawn_start_worker(worker: number): void;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
@@ -182,12 +187,12 @@ export interface InitOutput {
readonly web_spawn_recover_spawner: (a: number) => number;
readonly web_spawn_start_worker: (a: number) => void;
readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
readonly wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
readonly memory: WebAssembly.Memory;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
@@ -201,6 +206,7 @@ export interface InitOutput {
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.

File diff suppressed because it is too large Load Diff

View File

@@ -19,12 +19,12 @@ export const startSpawner: () => any;
export const web_spawn_recover_spawner: (a: number) => number;
export const web_spawn_start_worker: (a: number) => void;
export const ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
export const wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
export const wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
export const memory: WebAssembly.Memory;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;

View File

@@ -33,10 +33,10 @@ fi
git checkout "${VERSION}" --force
git reset --hard
cd crates/wasm
# Apply no-logging modification if requested
if [ "$NO_LOGGING" = "--no-logging" ]; then
echo "Applying no-logging configuration..."
cd crates/wasm
# Add it to the wasm32 target section (after the section header)
sed -i.bak '/^\[target\.\x27cfg(target_arch = "wasm32")\x27\.dependencies\]$/a\
@@ -45,11 +45,8 @@ tracing = { workspace = true, features = ["release_max_level_off"] }' Cargo.toml
# Clean up backup file
rm Cargo.toml.bak
cd ../..
fi
cd crates/wasm
cargo update
./build.sh
cd ../../

View File

@@ -1,19 +0,0 @@
# Build output
build/
*.js
*.js.map
*.d.ts
# Dependencies
node_modules/
# Editor
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

View File

@@ -1,312 +0,0 @@
# TypeScript Plugin Sample
A TypeScript implementation of the X Profile Prover plugin demonstrating how to write type-safe TLSN plugins.
## Overview
This package shows how to:
- Write TLSN plugins in TypeScript with full type safety
- Import types from `@tlsn/plugin-sdk`
- Compile TypeScript plugins to JavaScript for execution
- Use all plugin API features (prove, openWindow, UI rendering, hooks)
## Quick Start
### Installation
```bash
cd packages/ts-plugin-sample
npm install
```
### Build
```bash
npm run build
```
This bundles `src/index.ts` and `src/config.ts` into a single `build/index.js` file with clean `export default` statement.
### Development Mode
```bash
npm run dev
```
Watches for changes and rebuilds automatically.
### Type Checking
```bash
npm run typecheck
```
Runs TypeScript type checking without emitting files.
## Project Structure
```
ts-plugin-sample/
├── package.json # Dependencies and build scripts
├── tsconfig.json # TypeScript compiler configuration
├── build-wrapper.cjs # Custom build script for clean exports
├── src/
│ ├── index.ts # TypeScript plugin implementation
│ └── config.ts # Plugin configuration
├── build/
│ ├── index.js # Bundled plugin with export default
│ └── index.js.map # Source map for debugging
└── README.md
```
## TypeScript Features
### Type Imports
Import types from the plugin SDK for compile-time checking:
```typescript
import type {
PluginConfig,
RequestPermission,
Handler,
HandlerType,
HandlerPart,
HandlerAction,
InterceptedRequestHeader,
DomJson,
} from '@tlsn/plugin-sdk';
```
### Plugin Config Type Safety
```typescript
const config: PluginConfig = {
name: 'X Profile Prover',
description: 'This plugin will prove your X.com profile.',
version: '0.1.0',
author: 'TLSN Team',
requests: [
{
method: 'GET',
host: 'api.x.com',
pathname: '/1.1/account/settings.json',
verifierUrl: 'https://verifier.tlsnotary.org',
} satisfies RequestPermission,
],
urls: ['https://x.com/*'],
};
```
### Plugin API Globals
The plugin execution environment (QuickJS sandbox) provides these globals:
```typescript
// Declare types for globals injected by the sandbox
declare function div(options?: DomOptions, children?: DomJson[]): DomJson;
declare function button(options?: DomOptions, children?: DomJson[]): DomJson;
declare function openWindow(url: string, options?: {...}): Promise<{...}>;
declare function useEffect(callback: () => void, deps: any[]): void;
declare function useHeaders(filter: (headers: InterceptedRequestHeader[]) => InterceptedRequestHeader[]): InterceptedRequestHeader[];
declare function useState<T>(key: string, defaultValue: T): T;
declare function setState<T>(key: string, value: T): void;
declare function prove(requestOptions: {...}, proverOptions: {...}): Promise<any>;
declare function done(result?: any): void;
```
### Type-Safe Handlers
```typescript
const handlers: Handler[] = [
{
type: 'SENT' as HandlerType,
part: 'START_LINE' as HandlerPart,
action: 'REVEAL' as HandlerAction,
},
{
type: 'RECV' as HandlerType,
part: 'BODY' as HandlerPart,
action: 'REVEAL' as HandlerAction,
params: {
type: 'json',
path: 'screen_name',
},
},
];
```
## Key Differences from JavaScript
### 1. Type Annotations
```typescript
// JavaScript
function onClick() {
const isRequestPending = useState('isRequestPending', false);
// ...
}
// TypeScript
async function onClick(): Promise<void> {
const isRequestPending = useState<boolean>('isRequestPending', false);
// ...
}
```
### 2. Interface Compliance
TypeScript ensures your config matches the `PluginConfig` interface:
```typescript
const config: PluginConfig = {
name: 'X Profile Prover', // ✓ Required
description: 'Proves X profile', // ✓ Required
version: '0.1.0', // ✓ Optional
requests: [...], // ✓ Optional
urls: [...], // ✓ Optional
// TypeScript will error if required fields are missing!
};
```
### 3. Compile-Time Errors
```typescript
// This will error at compile time:
const handler: Handler = {
type: 'INVALID', // ❌ Type '"INVALID"' is not assignable to type 'HandlerType'
part: 'BODY',
action: 'REVEAL',
};
// This will pass:
const handler: Handler = {
type: 'RECV', // ✓ Valid HandlerType
part: 'BODY',
action: 'REVEAL',
};
```
## Build Configuration
### Build Tool: esbuild + Custom Wrapper
The plugin uses **esbuild** with a custom build wrapper:
- **Single file output:** All code bundled into `build/index.js` (7.2KB, 257 lines)
- **ES Module format:** Standard `export default` statement
- **No external imports:** All dependencies bundled inline
- **Inlined enums:** Handler enums included directly (no SDK imports)
- **Source maps:** Generated for debugging (`build/index.js.map`)
- **Fast builds:** ~10ms typical build time
The build wrapper (`build-wrapper.cjs`) transforms the esbuild output to use a clean `export default` statement matching the JavaScript plugin format.
### TypeScript Config (`tsconfig.json`)
TypeScript is used for type checking only (`npm run typecheck`):
- **Target:** ES2020 (modern browser features)
- **Strict:** Full type checking enabled
- **Global types:** Includes SDK globals for plugin API functions
## Loading in Extension
After building, the compiled `build/index.js` can be loaded in the TLSN extension:
1. Build the plugin: `npm run build`
2. The output is `build/index.js` with clean ES module export:
```javascript
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};
```
3. Load and execute in the extension:
```javascript
const pluginCode = fs.readFileSync('build/index.js', 'utf8');
const plugin = await sandbox.eval(pluginCode);
// plugin = { main, onClick, expandUI, minimizeUI, config }
```
4. The plugin executes with full type safety verified at compile time
**Output Characteristics:**
- ✅ Single file with `export default` statement
- ✅ No external imports (all dependencies bundled)
- ✅ Inlined enums (no SDK runtime dependency)
- ✅ ES Module format
- ✅ Matches JavaScript plugin structure
## Comparison with JavaScript Plugin
See `packages/demo/generated/twitter.js` for the equivalent JavaScript implementation.
**Advantages of TypeScript:**
- Compile-time type checking
- IDE autocomplete and IntelliSense
- Catches errors before runtime
- Better documentation via types
- Refactoring safety
**Trade-offs:**
- Requires build step
- Slightly more verbose (type annotations)
- Need to maintain type declarations
## Development Tips
### 1. Use Type Inference
TypeScript can infer many types:
```typescript
// Explicit (verbose)
const header: InterceptedRequestHeader | undefined = useHeaders(...)[0];
// Inferred (cleaner)
const [header] = useHeaders(...); // Type inferred from useHeaders return type
```
### 2. Use `satisfies` for Config
```typescript
// Good: Type-checked but allows literal types
requests: [
{
method: 'GET',
host: 'api.x.com',
// ...
} satisfies RequestPermission,
]
// Also good: Full type annotation
const request: RequestPermission = {
method: 'GET',
// ...
};
```
### 3. Enable Strict Mode
Keep `"strict": true` in `tsconfig.json` for maximum type safety.
### 4. Check Build Errors
```bash
npm run build
# Check for type errors without building
npx tsc --noEmit
```
## Resources
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
- [Plugin SDK Types](../plugin-sdk/src/types.ts)
- [JavaScript Plugin Example](../demo/generated/twitter.js)
- [TLSN Extension Docs](../../CLAUDE.md)
## License
MIT

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env node
/**
* Build wrapper to create clean export default statement
*/
const fs = require('fs');
const { execSync } = require('child_process');
// Run esbuild
console.log('Building with esbuild...');
execSync('esbuild src/index.ts --bundle --format=esm --outfile=build/index.js --sourcemap --external:@sebastianwessel/quickjs --external:@jitl/quickjs-ng-wasmfile-release-sync --external:uuid --external:fast-deep-equal', {
stdio: 'inherit'
});
// Read the generated code
let code = fs.readFileSync('build/index.js', 'utf8');
// Write back
fs.writeFileSync('build/index.js', code);
console.log('✓ Build complete: build/index.js');

View File

@@ -1,33 +0,0 @@
{
"name": "@tlsn/ts-plugin-sample",
"version": "0.1.0-alpha.13",
"description": "TypeScript plugin sample for TLSN extension",
"type": "module",
"main": "build/index.js",
"scripts": {
"build": "node build-wrapper.cjs",
"clean": "rm -rf build",
"dev": "esbuild src/index.ts --bundle --format=esm --outfile=build/index.js --sourcemap --watch",
"typecheck": "tsc --noEmit"
},
"keywords": [
"tlsn",
"plugin",
"typescript",
"example"
],
"author": "TLSN Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tlsnotary/tlsn-extension.git",
"directory": "packages/ts-plugin-sample"
},
"dependencies": {
"@tlsn/plugin-sdk": "*"
},
"devDependencies": {
"esbuild": "^0.24.2",
"typescript": "^5.5.4"
}
}

View File

@@ -1,58 +0,0 @@
/**
* FloatingButton Component
*
* Minimized floating action button
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
position,
bottom,
right,
width,
height,
borderRadius,
bgColor,
boxShadow,
zIndex,
display,
alignItems,
justifyContent,
cursor,
fontSize,
color,
transition,
} from '@tlsn/plugin-sdk/styles';
export interface FloatingButtonProps {
onClick: string;
icon?: string;
}
export function FloatingButton({ onClick, icon = '🔐' }: FloatingButtonProps): DomJson {
return div(
{
style: inlineStyle(
position('fixed'),
bottom('lg'),
right('lg'),
width('60px'),
height('60px'),
borderRadius('circle'),
bgColor('#4CAF50'),
boxShadow('lg'),
zIndex('999999'),
display('flex'),
alignItems('center'),
justifyContent('center'),
cursor('pointer'),
fontSize('2xl'),
color('white'),
transition()
),
onclick: onClick,
},
[icon]
);
}

View File

@@ -1,32 +0,0 @@
/**
* LoginPrompt Component
*
* Displays a message prompting the user to login
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
textAlign,
color,
padding,
bgColor,
borderRadius,
border,
} from '@tlsn/plugin-sdk/styles';
export function LoginPrompt(): DomJson {
return div(
{
style: inlineStyle(
textAlign('center'),
color('gray-600'),
padding('sm'),
bgColor('yellow-100'),
borderRadius('sm'),
border('1px solid #ffeaa7')
),
},
['Please login to x.com to continue']
);
}

View File

@@ -1,75 +0,0 @@
/**
* OverlayHeader Component
*
* Header bar with title and minimize button
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
background,
paddingY,
paddingX,
display,
justifyContent,
alignItems,
color,
fontWeight,
fontSize,
border,
cursor,
padding,
width,
height,
} from '@tlsn/plugin-sdk/styles';
export interface OverlayHeaderProps {
title: string;
onMinimize: string;
}
export function OverlayHeader({ title, onMinimize }: OverlayHeaderProps): DomJson {
return div(
{
style: inlineStyle(
background('linear-gradient(135deg, #667eea 0%, #764ba2 100%)'),
paddingY('sm'),
paddingX('md'),
display('flex'),
justifyContent('space-between'),
alignItems('center'),
color('white')
),
},
[
div(
{
style: inlineStyle(
fontWeight('semibold'),
fontSize('lg')
),
},
[title]
),
button(
{
style: inlineStyle(
background('transparent'),
border('none'),
color('white'),
fontSize('xl'),
cursor('pointer'),
padding('0'),
width('24px'),
height('24px'),
display('flex'),
alignItems('center'),
justifyContent('center')
),
onclick: onMinimize,
},
['']
),
]
);
}

View File

@@ -1,85 +0,0 @@
/**
* PluginOverlay Component
*
* Main plugin UI overlay container
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
position,
bottom,
right,
width,
borderRadius,
bgColor,
boxShadow,
zIndex,
fontSize,
fontFamily,
overflow,
padding,
defaultFontFamily,
} from '@tlsn/plugin-sdk/styles';
import { OverlayHeader } from './OverlayHeader';
import { StatusIndicator } from './StatusIndicator';
import { ProveButton } from './ProveButton';
import { LoginPrompt } from './LoginPrompt';
export interface PluginOverlayProps {
title: string;
isConnected: boolean;
isPending: boolean;
onMinimize: string;
onProve: string;
}
export function PluginOverlay({
title,
isConnected,
isPending,
onMinimize,
onProve,
}: PluginOverlayProps): DomJson {
return div(
{
style: inlineStyle(
position('fixed'),
bottom('0'),
right('xs'),
width('280px'),
borderRadius('md'),
{ borderRadius: '8px 8px 0 0' }, // Custom override for specific corner rounding
bgColor('white'),
boxShadow('md'),
zIndex('999999'),
fontSize('sm'),
fontFamily(defaultFontFamily),
overflow('hidden')
),
},
[
// Header
OverlayHeader({ title, onMinimize }),
// Content area
div(
{
style: inlineStyle(
padding('lg'),
bgColor('gray-100')
),
},
[
// Status indicator
StatusIndicator({ isConnected }),
// Conditional content: button or login prompt
isConnected
? ProveButton({ onClick: onProve, isPending })
: LoginPrompt(),
]
),
]
);
}

View File

@@ -1,49 +0,0 @@
/**
* ProveButton Component
*
* Button for initiating proof generation
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
width,
padding,
background,
color,
border,
borderRadius,
fontSize,
fontWeight,
cursor,
transition,
opacity,
} from '@tlsn/plugin-sdk/styles';
export interface ProveButtonProps {
onClick: string;
isPending: boolean;
}
export function ProveButton({ onClick, isPending }: ProveButtonProps): DomJson {
return button(
{
style: inlineStyle(
width('100%'),
padding('sm'),
background('linear-gradient(135deg, #667eea 0%, #764ba2 100%)'),
color('white'),
border('none'),
borderRadius('sm'),
fontSize('md'),
fontWeight('semibold'),
cursor('pointer'),
transition(),
isPending && opacity('0.6'),
isPending && cursor('not-allowed')
),
onclick: onClick,
},
[isPending ? 'Generating Proof...' : 'Prove']
);
}

View File

@@ -1,61 +0,0 @@
/**
* StatusIndicator Component
*
* Shows connection status with visual indicator
*/
import type { DomJson } from '@tlsn/plugin-sdk';
import {
inlineStyle,
display,
alignItems,
marginBottom,
width,
height,
borderRadius,
bgColor,
marginRight,
fontSize,
color,
} from '@tlsn/plugin-sdk/styles';
export interface StatusIndicatorProps {
isConnected: boolean;
}
export function StatusIndicator({ isConnected }: StatusIndicatorProps): DomJson {
return div(
{
style: inlineStyle(
display('flex'),
alignItems('center'),
marginBottom('md')
),
},
[
// Status dot
div(
{
style: inlineStyle(
width('8px'),
height('8px'),
borderRadius('circle'),
bgColor(isConnected ? '#48bb78' : '#cbd5e0'),
marginRight('2')
),
},
[]
),
// Status text
div(
{
style: inlineStyle(
fontSize('sm'),
color('gray-700')
),
},
[isConnected ? 'Connected' : 'Waiting for connection...']
),
]
);
}

View File

@@ -1,22 +0,0 @@
/**
* Component exports
*
* Centralized export point for all UI components
*/
export { FloatingButton } from './FloatingButton';
export type { FloatingButtonProps } from './FloatingButton';
export { PluginOverlay } from './PluginOverlay';
export type { PluginOverlayProps } from './PluginOverlay';
export { OverlayHeader } from './OverlayHeader';
export type { OverlayHeaderProps } from './OverlayHeader';
export { StatusIndicator } from './StatusIndicator';
export type { StatusIndicatorProps } from './StatusIndicator';
export { ProveButton } from './ProveButton';
export type { ProveButtonProps } from './ProveButton';
export { LoginPrompt } from './LoginPrompt';

View File

@@ -1,24 +0,0 @@
/**
* Plugin Configuration
*
* Defines metadata and permissions for the X Profile Prover plugin.
*/
// Type imports only (stripped at compile time)
import type { PluginConfig, RequestPermission } from '@tlsn/plugin-sdk';
export const config: PluginConfig = {
name: 'X Profile Prover',
description: 'This plugin will prove your X.com profile.',
version: '0.1.0',
author: 'TLSN Team',
requests: [
{
method: 'GET',
host: 'api.x.com',
pathname: '/1.1/account/settings.json',
verifierUrl: 'http://localhost:7047',
} satisfies RequestPermission,
],
urls: ['https://x.com/*'],
};

View File

@@ -1,211 +0,0 @@
/**
* X Profile Prover - TypeScript Plugin Sample
*
* This is a TypeScript implementation of the X.com profile prover plugin.
* It demonstrates how to write type-safe TLSN plugins using TypeScript.
*/
// =============================================================================
// IMPORTS
// =============================================================================
/**
* Import types from the plugin SDK (type-only, stripped at compile time).
*
* The plugin API functions (div, button, openWindow, etc.) are declared globally
* via the SDK type declarations.
*/
import type { Handler, DomJson } from '@tlsn/plugin-sdk';
import { config } from './config';
import { FloatingButton, PluginOverlay } from './components';
// =============================================================================
// HANDLER ENUMS (Inlined for standalone execution)
// =============================================================================
/**
* These enum values are inlined instead of imported to create a standalone
* JavaScript file with no external dependencies.
*/
enum HandlerType {
SENT = 'SENT',
RECV = 'RECV',
}
enum HandlerPart {
START_LINE = 'START_LINE',
PROTOCOL = 'PROTOCOL',
METHOD = 'METHOD',
REQUEST_TARGET = 'REQUEST_TARGET',
STATUS_CODE = 'STATUS_CODE',
HEADERS = 'HEADERS',
BODY = 'BODY',
ALL = 'ALL',
}
enum HandlerAction {
REVEAL = 'REVEAL',
PEDERSEN = 'PEDERSEN',
}
// =============================================================================
// PROOF GENERATION CALLBACK
// =============================================================================
/**
* This function is triggered when the user clicks the "Prove" button.
* It extracts authentication headers from intercepted requests and generates
* a TLSNotary proof using the unified prove() API.
*/
async function onClick(): Promise<void> {
const isRequestPending = useState<boolean>('isRequestPending', false);
if (isRequestPending) return;
setState('isRequestPending', true);
// Step 1: Get the intercepted header from the X.com API request
const [header] = useHeaders((headers) => {
return headers.filter((header) =>
header.url.includes('https://api.x.com/1.1/account/settings.json')
);
});
if (!header) {
setState('isRequestPending', false);
return;
}
// Step 2: Extract authentication headers from the intercepted request
const headers: Record<string, string | undefined> = {
cookie: header.requestHeaders.find((h) => h.name === 'Cookie')?.value,
'x-csrf-token': header.requestHeaders.find((h) => h.name === 'x-csrf-token')?.value,
'x-client-transaction-id': header.requestHeaders.find(
(h) => h.name === 'x-client-transaction-id'
)?.value,
Host: 'api.x.com',
authorization: header.requestHeaders.find((h) => h.name === 'authorization')?.value,
'Accept-Encoding': 'identity',
Connection: 'close',
};
// Step 3: Generate TLS proof using the unified prove() API
const resp = await prove(
// Request options
{
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: headers,
},
// Prover options
{
verifierUrl: 'http://localhost:7047',
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
maxRecvData: 4000,
maxSentData: 2000,
handlers: [
// Reveal the request start line
{
type: HandlerType.SENT,
part: HandlerPart.START_LINE,
action: HandlerAction.REVEAL,
} satisfies Handler,
// Reveal the response start line
{
type: HandlerType.RECV,
part: HandlerPart.START_LINE,
action: HandlerAction.REVEAL,
} satisfies Handler,
// Reveal the 'date' header from the response
{
type: HandlerType.RECV,
part: HandlerPart.HEADERS,
action: HandlerAction.REVEAL,
params: {
key: 'date',
},
} satisfies Handler,
// Reveal the 'screen_name' field from the JSON response body
{
type: HandlerType.RECV,
part: HandlerPart.BODY,
action: HandlerAction.REVEAL,
params: {
type: 'json' as const,
path: 'screen_name',
},
} satisfies Handler,
],
}
);
// Step 4: Complete plugin execution and return the proof result
done(JSON.stringify(resp));
}
/**
* Expand the minimized UI to show full plugin interface
*/
function expandUI(): void {
setState('isMinimized', false);
}
/**
* Minimize the UI to a floating action button
*/
function minimizeUI(): void {
setState('isMinimized', true);
}
// =============================================================================
// MAIN UI FUNCTION
// =============================================================================
/**
* The main() function is called reactively whenever plugin state changes.
* It returns a DOM structure that is rendered as the plugin UI.
*/
function main(): DomJson {
// Subscribe to intercepted headers for the X.com API endpoint
const [header] = useHeaders((headers) =>
headers.filter((header) => header.url.includes('https://api.x.com/1.1/account/settings.json'))
);
const isMinimized = useState<boolean>('isMinimized', false);
const isRequestPending = useState<boolean>('isRequestPending', false);
// Run once on plugin load: Open X.com in a new window
useEffect(() => {
openWindow('https://x.com');
}, []);
// If minimized, show floating action button
if (isMinimized) {
return FloatingButton({ onClick: 'expandUI' });
}
// Render the plugin UI overlay
return PluginOverlay({
title: 'X Profile Prover',
isConnected: !!header,
isPending: isRequestPending,
onMinimize: 'minimizeUI',
onProve: 'onClick',
});
}
// =============================================================================
// PLUGIN EXPORTS
// =============================================================================
/**
* All plugins must export an object with these properties:
* - main: The reactive UI rendering function
* - onClick: Click handler callback for buttons
* - config: Plugin metadata
*
* Additional exported functions (expandUI, minimizeUI) are also available
* as click handlers referenced by the 'onclick' property in DOM elements.
*/
export default {
main,
onClick,
expandUI,
minimizeUI,
config,
};

View File

@@ -1,40 +0,0 @@
{
"compilerOptions": {
/* Language and Environment */
"target": "ES2020",
"lib": ["ES2020"],
/* Modules */
"module": "ES2020",
"moduleResolution": "bundler",
"resolveJsonModule": true,
/* Emit */
"outDir": "./build",
"sourceMap": true,
"removeComments": false,
/* Interop Constraints */
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
/* Completeness */
"skipLibCheck": true
},
"include": [
"src/**/*",
"../plugin-sdk/src/globals.d.ts"
],
"exclude": ["node_modules", "build"]
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
{
"name": "@tlsn/tutorial",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "npm run build:plugins && vite",
"build": "npm run build:plugins && vite build",
"build:plugins": "node build-plugins.js",
"preview": "vite preview",
"docker:build": "docker compose build",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@types/node": "^20.10.6",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"codemirror": "^6.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.11"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
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">
<strong>Note:</strong> This step is optional and only works if you have a Twitter/X account.
Feel free to skip this step if you have limited time.
</p>
</div>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-800 mb-3">How it works:</h3>
<ol className="list-decimal list-inside space-y-2 text-gray-700">
<li>Opens Twitter/X in a new window</li>
<li>Log in if you haven't already (requires Twitter account)</li>
<li>Click the "Prove" button to start the TLSNotary MPC-TLS protocol</li>
<li>The prover will only reveal the screen name and a few headers to the verifier</li>
<li>Check the verifier output in your terminal</li>
</ol>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Plugin Code (Read-Only)</h3>
<CodeEditor value={twitterCode} onChange={() => {}} readOnly={true} height="500px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold">Execution</h3>
<Button onClick={handleRunPlugin} disabled={isExecuting || !twitterCode} variant="primary">
{isExecuting ? 'Running...' : isCompleted ? 'Run Again' : 'Run Twitter Plugin'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900">Twitter Plugin Completed! </p>
</div>
)}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
# TLSNotary dependency
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.13" }
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.14", features = ["mozilla-certs"] }
# HTTP server framework
axum = { version = "0.7", features = ["ws"] }
@@ -46,7 +46,7 @@ eyre = "0.6"
tokio-util = { version = "0.7", features = ["compat"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
regex = "1.12.2"
rangeset = "0.2.0"
rangeset = "0.4.0"
[dev-dependencies]
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }

View File

@@ -13,7 +13,7 @@ A Rust-based HTTP server with WebSocket support for TLSNotary verification opera
## Dependencies
- **tlsn**: v0.1.0-alpha.13 from GitHub - TLSNotary verification library
- **tlsn**: v0.1.0-alpha.14 from GitHub - TLSNotary verification library
- **axum**: Modern web framework with WebSocket support
- **tokio**: Async runtime with full features
- **tokio-util**: Async utilities for stream compatibility

View File

@@ -12,7 +12,7 @@ use axum::{
Router,
};
use axum_websocket::{WebSocket, WebSocketUpgrade};
use rangeset::RangeSet;
use rangeset::prelude::RangeSet;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
@@ -892,13 +892,13 @@ async fn run_verifier_task(
"[{}] Sent data length: {} bytes (authed: {} bytes)",
session_id,
sent_bytes.len(),
transcript.sent_authed().iter().sum::<usize>(),
transcript.sent_authed().len(),
);
info!(
"[{}] Received data length: {} bytes (authed: {} bytes)",
session_id,
recv_bytes.len(),
transcript.received_authed().iter().sum::<usize>()
transcript.received_authed().len()
);
// Wait for RevealConfig to be available (with polling and timeout)

View File

@@ -1,9 +1,11 @@
use eyre::eyre;
use tlsn::{
config::ProtocolConfigValidator,
config::{tls_commit::TlsCommitProtocolConfig, verifier::VerifierConfig},
connection::{DnsName, ServerName},
transcript::PartialTranscript,
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
verifier::VerifierOutput,
webpki::RootCertStore,
Session,
};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::compat::TokioAsyncReadCompatExt;
@@ -23,31 +25,103 @@ pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
max_sent_data, max_recv_data
);
let config_validator = ProtocolConfigValidator::builder()
.max_sent_data(max_sent_data)
.max_recv_data(max_recv_data)
.build()
.unwrap();
// Create a session with the prover
let session = Session::new(socket.compat());
let (driver, mut handle) = session.split();
// Spawn the session driver to run in the background
let driver_task = tokio::spawn(driver);
// Create verifier config with Mozilla root certificates for TLS verification
let verifier_config = VerifierConfig::builder()
.protocol_config_validator(config_validator)
.root_store(RootCertStore::mozilla())
.build()
.unwrap();
.map_err(|e| eyre!("Failed to build verifier config: {}", e))?;
info!("verifier_config: {:?}", verifier_config);
let verifier = Verifier::new(verifier_config);
let verifier = handle
.new_verifier(verifier_config)
.map_err(|e| eyre!("Failed to create verifier: {}", e))?;
info!("Starting verification");
info!("Starting TLS commitment protocol");
let VerifierOutput {
server_name,
transcript,
..
} = verifier
.verify(socket.compat(), &VerifyConfig::default())
// Run the commitment protocol
let verifier = verifier
.commit()
.await
.map_err(|e| eyre!("Commitment failed: {}", e))?;
// Check the proposed configuration
let request = verifier.request();
let TlsCommitProtocolConfig::Mpc(mpc_config) = request.protocol() else {
return Err(eyre!("Only MPC protocol is supported"));
};
// Validate the proposed configuration
if mpc_config.max_sent_data() > max_sent_data {
return Err(eyre!(
"Prover requested max_sent_data {} exceeds limit {}",
mpc_config.max_sent_data(),
max_sent_data
));
}
if mpc_config.max_recv_data() > max_recv_data {
return Err(eyre!(
"Prover requested max_recv_data {} exceeds limit {}",
mpc_config.max_recv_data(),
max_recv_data
));
}
info!(
"Accepting TLS commitment with max_sent={}, max_recv={}",
mpc_config.max_sent_data(),
mpc_config.max_recv_data()
);
// Accept and run the commitment protocol
let verifier = verifier
.accept()
.await
.map_err(|e| eyre!("Accept failed: {}", e))?
.run()
.await
.map_err(|e| eyre!("Run failed: {}", e))?;
info!("TLS connection complete, starting verification");
// Verify the proof
let verifier = verifier
.verify()
.await
.map_err(|e| eyre!("Verification failed: {}", e))?;
let (
VerifierOutput {
server_name,
transcript,
..
},
verifier,
) = verifier
.accept()
.await
.map_err(|e| eyre!("Accept verification failed: {}", e))?;
// Close the verifier
verifier
.close()
.await
.map_err(|e| eyre!("Failed to close verifier: {}", e))?;
// Close the session handle
handle.close();
// Wait for the driver to complete
driver_task
.await
.map_err(|e| eyre!("Driver task failed: {}", e))?
.map_err(|e| eyre!("Session driver error: {}", e))?;
info!("verify() returned successfully - prover sent all data");
let server_name =