mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-22 13:38:07 -05:00
Compare commits
9 Commits
ts-plugin-
...
new-tutori
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4a1cc8b2e | ||
|
|
09e107a871 | ||
|
|
3ea0300f65 | ||
|
|
af9148ee4a | ||
|
|
6331f03940 | ||
|
|
3b4554cd2a | ||
|
|
367479fd77 | ||
|
|
d42db516c7 | ||
|
|
1b776929d8 |
4
.github/workflows/demo.yml
vendored
4
.github/workflows/demo.yml
vendored
@@ -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
|
||||
|
||||
24
CLAUDE.md
24
CLAUDE.md
@@ -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
|
||||
|
||||
30
README.md
30
README.md
@@ -58,8 +58,7 @@ tlsn-extension/
|
||||
│ │
|
||||
│ ├── demo/ # Demo server with Docker setup
|
||||
│ │ ├── *.js # Example plugin files
|
||||
│ │ ├── docker-compose.yml # Docker services configuration
|
||||
│ │ └── start.sh # Setup script with configurable URLs
|
||||
│ │ └── docker-compose.yml # Docker services configuration
|
||||
│ │
|
||||
│ ├── tutorial/ # Tutorial examples
|
||||
│ │ └── *.js # Tutorial plugin files
|
||||
@@ -116,10 +115,9 @@ Rust-based HTTP/WebSocket server for TLSNotary verification:
|
||||
#### 5. **demo** - Demo Server
|
||||
Docker-based demo environment with:
|
||||
- Pre-configured example plugins (Twitter, SwissBank)
|
||||
- React + Vite frontend with environment-based configuration
|
||||
- Docker Compose setup with verifier and nginx
|
||||
- Configurable verifier URLs via environment variables
|
||||
- Plugin file generator (`generate.sh`) with SSL support
|
||||
- Docker startup script (`start.sh`)
|
||||
- Configurable verifier URLs via `.env` files or Docker build args
|
||||
|
||||
#### 6. **tlsn-wasm-pkg** - TLSN WebAssembly Package
|
||||
Pre-built WebAssembly binaries for TLSNotary functionality in the browser.
|
||||
@@ -495,25 +493,19 @@ npm run demo
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure the demo for different environments:
|
||||
The demo uses `.env` files for configuration:
|
||||
- `.env` - Local development defaults (`localhost:7047`)
|
||||
- `.env.production` - Production settings (`verifier.tlsnotary.org`, SSL enabled)
|
||||
|
||||
For Docker deployments, override via environment variables:
|
||||
```bash
|
||||
# Local development (default)
|
||||
cd packages/demo
|
||||
./generate.sh && ./start.sh
|
||||
npm run docker:up
|
||||
|
||||
# Production with SSL
|
||||
cd packages/demo
|
||||
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh
|
||||
./start.sh
|
||||
|
||||
# Docker detached mode
|
||||
./generate.sh && ./start.sh -d
|
||||
# Production with custom verifier
|
||||
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
|
||||
```
|
||||
|
||||
The demo uses two scripts:
|
||||
- **`generate.sh`** - Generates plugin files with configured verifier URLs (use environment variables here)
|
||||
- **`start.sh`** - Starts Docker Compose services (assumes `generated/` directory exists)
|
||||
|
||||
### Tutorial
|
||||
|
||||
```bash
|
||||
|
||||
743
package-lock.json
generated
743
package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-monorepo",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.14",
|
||||
"private": true,
|
||||
"description": "TLSN Extension monorepo with plugin SDK",
|
||||
"license": "MIT",
|
||||
@@ -22,11 +22,11 @@
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"serve:test": "npm run serve:test --workspace=extension",
|
||||
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
|
||||
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.13 --no-logging",
|
||||
"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",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Verifier Configuration
|
||||
VITE_VERIFIER_HOST=localhost:7047
|
||||
VITE_VERIFIER_PROTOCOL=http
|
||||
VITE_PROXY_PROTOCOL=ws
|
||||
VITE_SSL=false
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Production environment variables
|
||||
VITE_VERIFIER_HOST=verifier.tlsnotary.org
|
||||
VITE_VERIFIER_PROTOCOL=https
|
||||
VITE_PROXY_PROTOCOL=wss
|
||||
VITE_SSL=true
|
||||
|
||||
3
packages/demo/.gitignore
vendored
3
packages/demo/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
*.wasm
|
||||
dist/
|
||||
public/plugins/
|
||||
public/plugins/
|
||||
generated/
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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`);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Demo Plugin File Generator
|
||||
#
|
||||
# This script generates plugin files with configurable verifier URLs.
|
||||
# Used both locally and in CI/CD pipelines.
|
||||
#
|
||||
# Environment Variables:
|
||||
# VERIFIER_HOST - Verifier server host (default: localhost:7047)
|
||||
# SSL - Use https/wss if true (default: false)
|
||||
#
|
||||
# Usage:
|
||||
# ./generate.sh # Local development
|
||||
# VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./generate.sh # Production
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Configuration with defaults
|
||||
VERIFIER_HOST="${VERIFIER_HOST:-localhost:7047}"
|
||||
SSL="${SSL:-false}"
|
||||
|
||||
# Determine protocol based on SSL setting
|
||||
if [ "$SSL" = "true" ]; then
|
||||
HTTP_PROTOCOL="https"
|
||||
WS_PROTOCOL="wss"
|
||||
else
|
||||
HTTP_PROTOCOL="http"
|
||||
WS_PROTOCOL="ws"
|
||||
fi
|
||||
|
||||
VERIFIER_URL="${HTTP_PROTOCOL}://${VERIFIER_HOST}"
|
||||
PROXY_URL_BASE="${WS_PROTOCOL}://${VERIFIER_HOST}/proxy?token="
|
||||
|
||||
echo "========================================"
|
||||
echo "TLSNotary Demo Plugin Generator"
|
||||
echo "========================================"
|
||||
echo "Verifier Host: $VERIFIER_HOST"
|
||||
echo "SSL Enabled: $SSL"
|
||||
echo "Verifier URL: $VERIFIER_URL"
|
||||
echo "Proxy URL: ${PROXY_URL_BASE}<host>"
|
||||
echo "========================================"
|
||||
|
||||
# Create generated directory for processed files
|
||||
mkdir -p generated
|
||||
|
||||
# Function to process a plugin file
|
||||
process_plugin() {
|
||||
local input_file="$1"
|
||||
local output_file="generated/$(basename "$input_file")"
|
||||
|
||||
echo "Processing: $input_file -> $output_file"
|
||||
|
||||
# Replace verifierUrl and proxyUrl patterns
|
||||
sed -E \
|
||||
-e "s|verifierUrl: '[^']*'|verifierUrl: '${VERIFIER_URL}'|g" \
|
||||
-e "s|verifierUrl: \"[^\"]*\"|verifierUrl: \"${VERIFIER_URL}\"|g" \
|
||||
-e "s|proxyUrl: 'ws://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
|
||||
-e "s|proxyUrl: 'wss://[^/]+/proxy\?token=([^']+)'|proxyUrl: '${PROXY_URL_BASE}\1'|g" \
|
||||
-e "s|proxyUrl: \"ws://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
|
||||
-e "s|proxyUrl: \"wss://[^/]+/proxy\?token=([^\"]+)\"|proxyUrl: \"${PROXY_URL_BASE}\1\"|g" \
|
||||
"$input_file" > "$output_file"
|
||||
}
|
||||
|
||||
# Function to process index.html
|
||||
process_index_html() {
|
||||
local input_file="$1"
|
||||
local output_file="generated/$(basename "$input_file")"
|
||||
|
||||
echo "Processing: $input_file -> $output_file"
|
||||
|
||||
# Replace hardcoded health check URL with configured verifier URL
|
||||
sed -E \
|
||||
-e "s|http://localhost:7047/health|${VERIFIER_URL}/health|g" \
|
||||
"$input_file" > "$output_file"
|
||||
}
|
||||
|
||||
# Process index.html
|
||||
echo ""
|
||||
echo "Processing index.html..."
|
||||
process_index_html "index.html"
|
||||
|
||||
# Copy other static files
|
||||
echo ""
|
||||
echo "Copying other static files..."
|
||||
cp favicon.ico generated/ 2>/dev/null || true
|
||||
|
||||
# Process plugin files
|
||||
echo ""
|
||||
echo "Processing plugin files..."
|
||||
for plugin_file in *.js; do
|
||||
if [ -f "$plugin_file" ]; then
|
||||
process_plugin "$plugin_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Generated files:"
|
||||
ls -la generated/
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Generation complete!"
|
||||
echo "========================================"
|
||||
@@ -1,510 +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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
196
packages/plugin-sdk/src/globals.d.ts
vendored
196
packages/plugin-sdk/src/globals.d.ts
vendored
@@ -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 {};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "tlsn-wasm",
|
||||
"type": "module",
|
||||
"description": "A core WebAssembly package for TLSNotary.",
|
||||
"version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.14",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
148
packages/tlsn-wasm-pkg/tlsn_wasm.d.ts
vendored
148
packages/tlsn-wasm-pkg/tlsn_wasm.d.ts
vendored
@@ -1,19 +1,8 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Initializes the module.
|
||||
*/
|
||||
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
|
||||
/**
|
||||
* Starts the thread spawner on a dedicated worker thread.
|
||||
*/
|
||||
export function startSpawner(): Promise<any>;
|
||||
export function web_spawn_start_worker(worker: number): void;
|
||||
export function web_spawn_recover_spawner(spawner: number): Spawner;
|
||||
export interface CrateLogFilter {
|
||||
level: LoggingLevel;
|
||||
name: string;
|
||||
}
|
||||
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
|
||||
|
||||
export type SpanEvent = "New" | "Close" | "Active";
|
||||
|
||||
export interface LoggingConfig {
|
||||
level: LoggingLevel | undefined;
|
||||
@@ -21,15 +10,43 @@ export interface LoggingConfig {
|
||||
span_events: SpanEvent[] | undefined;
|
||||
}
|
||||
|
||||
export type SpanEvent = "New" | "Close" | "Active";
|
||||
export interface CrateLogFilter {
|
||||
level: LoggingLevel;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type LoggingLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error";
|
||||
export type Body = JsonValue;
|
||||
|
||||
export type NetworkSetting = "Bandwidth" | "Latency";
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export interface Commit {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
export interface HttpRequest {
|
||||
uri: string;
|
||||
method: Method;
|
||||
headers: Map<string, number[]>;
|
||||
body: Body | undefined;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
headers: [string, number[]][];
|
||||
}
|
||||
|
||||
export type TlsVersion = "V1_2" | "V1_3";
|
||||
|
||||
export interface TranscriptLength {
|
||||
sent: number;
|
||||
recv: number;
|
||||
}
|
||||
|
||||
export interface ConnectionInfo {
|
||||
time: number;
|
||||
version: TlsVersion;
|
||||
transcript_length: TranscriptLength;
|
||||
}
|
||||
|
||||
export interface Transcript {
|
||||
sent: number[];
|
||||
recv: number[];
|
||||
}
|
||||
|
||||
export interface PartialTranscript {
|
||||
@@ -39,52 +56,25 @@ export interface PartialTranscript {
|
||||
recv_authed: { start: number; end: number }[];
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
headers: [string, number[]][];
|
||||
export interface Commit {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
}
|
||||
|
||||
export type Body = JsonValue;
|
||||
|
||||
export interface VerifierOutput {
|
||||
server_name: string | undefined;
|
||||
connection_info: ConnectionInfo;
|
||||
transcript: PartialTranscript | undefined;
|
||||
}
|
||||
|
||||
export interface ConnectionInfo {
|
||||
time: number;
|
||||
version: TlsVersion;
|
||||
transcript_length: TranscriptLength;
|
||||
}
|
||||
|
||||
export interface TranscriptLength {
|
||||
sent: number;
|
||||
recv: number;
|
||||
}
|
||||
|
||||
export type TlsVersion = "V1_2" | "V1_3";
|
||||
|
||||
export interface HttpRequest {
|
||||
uri: string;
|
||||
method: Method;
|
||||
headers: Map<string, number[]>;
|
||||
body: Body | undefined;
|
||||
}
|
||||
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export interface Reveal {
|
||||
sent: { start: number; end: number }[];
|
||||
recv: { start: number; end: number }[];
|
||||
server_identity: boolean;
|
||||
}
|
||||
|
||||
export interface Transcript {
|
||||
sent: number[];
|
||||
recv: number[];
|
||||
export interface VerifierOutput {
|
||||
server_name: string | undefined;
|
||||
connection_info: ConnectionInfo;
|
||||
transcript: PartialTranscript | undefined;
|
||||
}
|
||||
|
||||
export type NetworkSetting = "Bandwidth" | "Latency";
|
||||
|
||||
export interface ProverConfig {
|
||||
server_name: string;
|
||||
max_sent_data: number;
|
||||
@@ -104,18 +94,14 @@ export interface VerifierConfig {
|
||||
max_recv_records_online: number | undefined;
|
||||
}
|
||||
|
||||
|
||||
export class Prover {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Returns the transcript.
|
||||
*/
|
||||
transcript(): Transcript;
|
||||
/**
|
||||
* Send the HTTP request to the server.
|
||||
*/
|
||||
send_request(ws_proxy_url: string, request: HttpRequest): Promise<HttpResponse>;
|
||||
constructor(config: ProverConfig);
|
||||
/**
|
||||
* Set up the prover.
|
||||
*
|
||||
@@ -127,10 +113,13 @@ export class Prover {
|
||||
* Reveals data to the verifier and finalizes the protocol.
|
||||
*/
|
||||
reveal(reveal: Reveal): Promise<void>;
|
||||
/**
|
||||
* Returns the transcript.
|
||||
*/
|
||||
transcript(): Transcript;
|
||||
constructor(config: ProverConfig);
|
||||
}
|
||||
/**
|
||||
* Global spawner which spawns closures into web workers.
|
||||
*/
|
||||
|
||||
export class Spawner {
|
||||
private constructor();
|
||||
free(): void;
|
||||
@@ -141,10 +130,10 @@ export class Spawner {
|
||||
run(url: string): Promise<void>;
|
||||
intoRaw(): number;
|
||||
}
|
||||
|
||||
export class Verifier {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
constructor(config: VerifierConfig);
|
||||
/**
|
||||
* Verifies the connection and finalizes the protocol.
|
||||
*/
|
||||
@@ -153,13 +142,29 @@ export class Verifier {
|
||||
* Connect to the prover.
|
||||
*/
|
||||
connect(prover_url: string): Promise<void>;
|
||||
constructor(config: VerifierConfig);
|
||||
}
|
||||
|
||||
export class WorkerData {
|
||||
private constructor();
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the module.
|
||||
*/
|
||||
export function initialize(logging_config: LoggingConfig | null | undefined, thread_count: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Starts the thread spawner on a dedicated worker thread.
|
||||
*/
|
||||
export function startSpawner(): Promise<any>;
|
||||
|
||||
export function web_spawn_recover_spawner(spawner: number): Spawner;
|
||||
|
||||
export function web_spawn_start_worker(worker: number): void;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
@@ -182,12 +187,12 @@ export interface InitOutput {
|
||||
readonly web_spawn_recover_spawner: (a: number) => number;
|
||||
readonly web_spawn_start_worker: (a: number) => void;
|
||||
readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
|
||||
readonly wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
@@ -201,6 +206,7 @@ export interface InitOutput {
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
12
packages/tlsn-wasm-pkg/tlsn_wasm_bg.wasm.d.ts
vendored
12
packages/tlsn-wasm-pkg/tlsn_wasm_bg.wasm.d.ts
vendored
@@ -19,12 +19,12 @@ export const startSpawner: () => any;
|
||||
export const web_spawn_recover_spawner: (a: number) => number;
|
||||
export const web_spawn_start_worker: (a: number) => void;
|
||||
export const ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h1221e6fae8f79e66: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__closure__destroy__h77926bfd4964395c: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha226a7154e96c3a6: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__closure__destroy__h667d3f209ba8d8c8: (a: number, b: number) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__h0a1439cca01ee997: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__he1146594190fdf85: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent____Output_______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___web_sys_8bc8039b94004458___features__gen_CloseEvent__CloseEvent_____: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___closure__destroy___dyn_core_a1e22386a1c4876a___ops__function__FnMut__wasm_bindgen_d93ce3c58293cca3___JsValue____Output_______: (a: number, b: number) => void;
|
||||
export const wasm_bindgen_d93ce3c58293cca3___convert__closures_____invoke___wasm_bindgen_d93ce3c58293cca3___JsValue__wasm_bindgen_d93ce3c58293cca3___JsValue_____: (a: number, b: number, c: any, d: any) => void;
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
|
||||
@@ -33,10 +33,10 @@ fi
|
||||
git checkout "${VERSION}" --force
|
||||
git reset --hard
|
||||
|
||||
cd crates/wasm
|
||||
# Apply no-logging modification if requested
|
||||
if [ "$NO_LOGGING" = "--no-logging" ]; then
|
||||
echo "Applying no-logging configuration..."
|
||||
cd crates/wasm
|
||||
|
||||
# Add it to the wasm32 target section (after the section header)
|
||||
sed -i.bak '/^\[target\.\x27cfg(target_arch = "wasm32")\x27\.dependencies\]$/a\
|
||||
@@ -45,11 +45,8 @@ tracing = { workspace = true, features = ["release_max_level_off"] }' Cargo.toml
|
||||
|
||||
# Clean up backup file
|
||||
rm Cargo.toml.bak
|
||||
|
||||
cd ../..
|
||||
fi
|
||||
|
||||
cd crates/wasm
|
||||
cargo update
|
||||
./build.sh
|
||||
cd ../../
|
||||
|
||||
19
packages/ts-plugin-sample/.gitignore
vendored
19
packages/ts-plugin-sample/.gitignore
vendored
@@ -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
|
||||
@@ -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
|
||||
@@ -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');
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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']
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
['−']
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -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']
|
||||
);
|
||||
}
|
||||
@@ -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...']
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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/*'],
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
32
packages/tutorial/.eslintrc.json
Normal file
32
packages/tutorial/.eslintrc.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
packages/tutorial/.gitignore
vendored
Normal file
6
packages/tutorial/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
public/plugins/*.js
|
||||
7
packages/tutorial/.prettierrc.json
Normal file
7
packages/tutorial/.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
29
packages/tutorial/Dockerfile
Normal file
29
packages/tutorial/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
ARG VITE_VERIFIER_HOST=localhost:7047
|
||||
ARG VITE_SSL=false
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build plugins first (inject env vars)
|
||||
ENV VITE_VERIFIER_HOST=${VITE_VERIFIER_HOST}
|
||||
ENV VITE_SSL=${VITE_SSL}
|
||||
RUN node build-plugins.js
|
||||
|
||||
# Build React app
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /app/public/plugins /usr/share/nginx/html/plugins
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
236
packages/tutorial/build-plugins.js
Normal file
236
packages/tutorial/build-plugins.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';
|
||||
const SSL = process.env.VITE_SSL === 'true';
|
||||
const VERIFIER_URL = `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`;
|
||||
const PROXY_BASE = `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=`;
|
||||
|
||||
console.log(`Building plugins with VERIFIER_URL=${VERIFIER_URL}`);
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = join(__dirname, 'public', 'plugins');
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Twitter plugin (already has all handler code, just needs env vars substituted)
|
||||
const twitterPlugin = `// Twitter Plugin - Pre-built
|
||||
const config = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: '${VERIFIER_URL}',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
'x-csrf-token': header.requestHeaders.find(header => header.name === 'x-csrf-token')?.value,
|
||||
'x-client-transaction-id': header.requestHeaders.find(header => header.name === 'x-client-transaction-id')?.value,
|
||||
Host: 'api.x.com',
|
||||
authorization: header.requestHeaders.find(header => header.name === 'authorization')?.value,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json',
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
verifierUrl: '${VERIFIER_URL}',
|
||||
proxyUrl: '${PROXY_BASE}api.x.com',
|
||||
maxRecvData: 4000,
|
||||
maxSentData: 2000,
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'HEADERS', action: 'REVEAL', params: { key: 'date' } },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'screen_name' } },
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(headers => headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json')));
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
useEffect(() => {
|
||||
openWindow('https://x.com');
|
||||
}, []);
|
||||
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed', bottom: '20px', right: '20px', width: '60px', height: '60px',
|
||||
borderRadius: '50%', backgroundColor: '#4CAF50', boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', transition: 'all 0.3s ease', fontSize: '24px', color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}
|
||||
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed', bottom: '0', right: '8px', width: '280px', borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white', boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', zIndex: '999999',
|
||||
fontSize: '14px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
div({ style: { background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: 'white' }}, [
|
||||
div({ style: { fontWeight: '600', fontSize: '16px' }}, ['X Profile Prover']),
|
||||
button({ style: { background: 'transparent', border: 'none', color: 'white', fontSize: '20px', cursor: 'pointer', padding: '0', width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onclick: 'minimizeUI' }, ['−'])
|
||||
]),
|
||||
div({ style: { padding: '20px', backgroundColor: '#f8f9fa' }}, [
|
||||
div({ style: { marginBottom: '16px', padding: '12px', borderRadius: '6px', backgroundColor: header ? '#d4edda' : '#f8d7da', color: header ? '#155724' : '#721c24', border: \`1px solid \${header ? '#c3e6cb' : '#f5c6cb'}\`, fontWeight: '500' }}, [header ? '✓ Profile detected' : '⚠ No profile detected']),
|
||||
header ? button({ style: { width: '100%', padding: '12px 24px', borderRadius: '6px', border: 'none', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', fontWeight: '600', fontSize: '15px', cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', opacity: isRequestPending ? 0.5 : 1, cursor: isRequestPending ? 'not-allowed' : 'pointer' }, onclick: 'onClick' }, [isRequestPending ? 'Generating Proof...' : 'Generate Proof']) : div({ style: { textAlign: 'center', color: '#666', padding: '12px', backgroundColor: '#fff3cd', borderRadius: '6px', border: '1px solid #ffeaa7' }}, ['Please login to x.com to continue'])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
export default { main, onClick, expandUI, minimizeUI, config };
|
||||
`;
|
||||
|
||||
// Swiss Bank Starter (with TODO comment)
|
||||
const swissbankStarter = `// Swiss Bank Plugin - Starter Template
|
||||
const config = {
|
||||
name: 'Swiss Bank Prover',
|
||||
description: 'This plugin will prove your Swiss Bank account balance.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'swissbank.tlsnotary.org',
|
||||
pathname: '/balances',
|
||||
verifierUrl: '${VERIFIER_URL}',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://swissbank.tlsnotary.org/*',
|
||||
],
|
||||
};
|
||||
|
||||
const host = 'swissbank.tlsnotary.org';
|
||||
const ui_path = '/account';
|
||||
const path = '/balances';
|
||||
const url = \`https://\${host}\${path}\`;
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(\`https://\${host}\`));
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
Host: host,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
{ url: url, method: 'GET', headers: headers },
|
||||
{
|
||||
verifierUrl: '${VERIFIER_URL}',
|
||||
proxyUrl: '${PROXY_BASE}swissbank.tlsnotary.org',
|
||||
maxRecvData: 460,
|
||||
maxSentData: 180,
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'START_LINE', action: 'REVEAL' },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'account_id' } },
|
||||
// TODO: add handler to reveal 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`);
|
||||
29
packages/tutorial/docker-compose.yml
Normal file
29
packages/tutorial/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
verifier:
|
||||
build:
|
||||
context: ../verifier
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "7047:7047"
|
||||
restart: unless-stopped
|
||||
|
||||
tutorial-static:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
|
||||
VITE_SSL: ${VITE_SSL:-false}
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- verifier
|
||||
- tutorial-static
|
||||
restart: unless-stopped
|
||||
@@ -1,650 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>TLSNotary Extension Plugin Tutorial</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.step {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step.completed {
|
||||
background: #f0f8f0;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.step.blocked {
|
||||
background: #f8f8f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.status.checking {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.result {
|
||||
background: #e8f5e8;
|
||||
border: 2px solid #28a745;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.debug {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 10px 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Add this single CSS rule */
|
||||
.faq-question {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
border-left: 3px solid #007bff;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="browser-check" class="step" style="display: none;">
|
||||
<h2>⚠️ Browser Compatibility</h2>
|
||||
<div class="status error">
|
||||
<strong>Unsupported Browser Detected</strong>
|
||||
</div>
|
||||
<p>TLSNotary extension requires a Chrome-based browser (Chrome, Edge, Brave, etc.).</p>
|
||||
<p>Please switch to a supported browser to continue with this tutorial.</p>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h2>Welcome to the TLSNotary Browser Extension Plugin Tutorial</h2>
|
||||
<p>This tutorial will guide you through creating and running TLSNotary plugins. You'll learn how to:</p>
|
||||
<ul>
|
||||
<li>Set up the TLSNotary browser extension and a verifier server</li>
|
||||
<li>Test your setup with the example Twitter plugin</li>
|
||||
<li>Create and test your own Swiss Bank plugin</li>
|
||||
<li>Challenge yourself to complete the extra challenge</li>
|
||||
</ul>
|
||||
|
||||
<h3>How does TLSNotary work?</h3>
|
||||
<p>In TLSNotary, there are three key components:</p>
|
||||
<ul>
|
||||
<li><strong>Prover (Your Browser)</strong>: Makes requests to websites and generates cryptographic proofs
|
||||
</li>
|
||||
<li><strong>Server (Twitter/Swiss Bank)</strong>: The website that serves the data you want to prove</li>
|
||||
<li><strong>Verifier</strong>: Independently verifies that the data really came from the server</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>The key innovation:</strong> TLSNotary uses Multi-Party Computation (MPC-TLS) where the verifier
|
||||
participates in the TLS session alongside your browser. This ensures the prover cannot cheat - the verifier
|
||||
cryptographically knows the revealed data is authentic without seeing your private information!</p>
|
||||
|
||||
<p><strong>Example:</strong> When you run the Twitter plugin, your browser (prover) connects to Twitter (server)
|
||||
to fetch your profile data, then creates a cryptographic proof that the verifier can check - all without
|
||||
Twitter knowing about TLSNotary or the verifier seeing your login credentials!</p>
|
||||
|
||||
<h3>What you'll build:</h3>
|
||||
<p>By the end of this tutorial, you'll understand how to create plugins that can prove data from any website,
|
||||
opening up possibilities for verified credentials, authenticated data sharing, and trustless applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="step-extension" class="step blocked">
|
||||
<h2>Step 1: Install TLSNotary Extension</h2>
|
||||
<div id="extension-status" class="status checking">Checking extension...</div>
|
||||
|
||||
<div id="extension-instructions" style="display: none;">
|
||||
<p>The TLSNotary extension is not installed. Please build it locally:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>cd ./packages/extension
|
||||
npm install
|
||||
npm run build</code></pre>
|
||||
<p>Then install in Chrome:</p>
|
||||
<ol>
|
||||
<li>Open <code>chrome://extensions/</code></li>
|
||||
<li>Enable "Developer mode" (toggle in top right)</li>
|
||||
<li>Click "Load unpacked"</li>
|
||||
<li>Select the <code>packages/extension/build/</code> folder</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button onclick="location.reload()">Check Again</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="step-verifier" class="step blocked">
|
||||
<h2>Step 2: Start Verifier Server</h2>
|
||||
<div id="verifier-status" class="status checking">Checking verifier server...</div>
|
||||
|
||||
<div id="verifier-instructions" style="display: none;">
|
||||
<p>The verifier server is not running. Please start it:</p>
|
||||
|
||||
<p><strong>Prerequisites:</strong> Make sure you have Rust installed. If not, install it from <a
|
||||
href="https://rustup.rs/" target="_blank">rustup.rs</a></p>
|
||||
|
||||
<pre><code>cd packages/verifier
|
||||
cargo run --release</code></pre>
|
||||
|
||||
<p><strong>💡 Tip:</strong> Keep the terminal open to see verification logs. Run this side-by-side with your
|
||||
browser!</p>
|
||||
|
||||
<button onclick="checkVerifier()">Check Again</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="step-twitter" class="step blocked">
|
||||
<h2>Step 3: Run Twitter Plugin (Example) - Optional</h2>
|
||||
<p>Let's start with a complete working example to understand how TLSNotary plugins work.</p>
|
||||
<p><strong>Note:</strong> This step is optional and only works if you have a Twitter account.
|
||||
Feel free to skip this step if you have limited time.</p>
|
||||
|
||||
<div id="twitter-ready" style="display: none;">
|
||||
<p>This plugin will prove your Twitter screen name by:</p>
|
||||
<ol>
|
||||
<li>Opening Twitter in a new window</li>
|
||||
<li>Log in if you haven't already (requires Twitter account)</li>
|
||||
<li>Click the prove button to start the TLSNotary MPC-TLS protocol with the verifier server</li>
|
||||
<li>The prover will only reveal the screen name and a few headers to the verifier</li>
|
||||
<li>Make sure to check the verifier output</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>📄 Source code:</strong> <a href="twitter.js" target="_blank">View twitter.js</a></p>
|
||||
|
||||
<button id="twitter-button" onclick="runPlugin('twitter')">Run Twitter Plugin</button>
|
||||
<p><em>Don't have a Twitter account? Skip to Step 4 below.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="step-swissbank" class="step blocked">
|
||||
<h2>Step 4: Run Swiss Bank Plugin</h2>
|
||||
|
||||
<div id="swissbank-ready" style="display: none;">
|
||||
<p>Now let's write our own plugin. Let's prove the Swiss Frank (CHF) balance on the EF's Swiss Bank account.
|
||||
</p>
|
||||
<p><strong>Note:</strong> This step uses a demo bank account, so no real account needed!</p>
|
||||
<p>Follow these steps:</p>
|
||||
<ol>
|
||||
<li>Check <a href="https://swissbank.tlsnotary.org/balances"
|
||||
target="_blank">https://swissbank.tlsnotary.org/balances</a>, you should have <b>no</b> access.
|
||||
</li>
|
||||
<li>Log in to the bank via <a href="https://swissbank.tlsnotary.org/login"
|
||||
target="_blank">https://swissbank.tlsnotary.org/login</a>
|
||||
<ul>
|
||||
<li>Username: <code>tkstanczak</code></li>
|
||||
<li>Password: <code>TLSNotary is my favorite project</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Check <a href="https://swissbank.tlsnotary.org/balances"
|
||||
target="_blank">https://swissbank.tlsnotary.org/balances</a> again, you should have access now.
|
||||
</li>
|
||||
<li>Open <code>/packages/tutorial/swissbank.js</code> in your favorite editor and add the missing
|
||||
handler to
|
||||
reveal the Swiss Franks (CHF) balance to the verfier:
|
||||
<pre><code>{ type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:\s*"[^"]+"' }, },</code></pre>
|
||||
What does this mean?
|
||||
<dl>
|
||||
<dt><strong>type: 'RECV'</strong></dt>
|
||||
<dd>This means we are handling data received from the server (the bank)</dd>
|
||||
|
||||
<dt><strong>part: 'ALL'</strong></dt>
|
||||
<dd>This means we are looking at the entire response (headers + body)</dd>
|
||||
|
||||
<dt><strong>action: 'REVEAL'</strong></dt>
|
||||
<dd>This means we want to reveal some data to the verifier</dd>
|
||||
|
||||
<dt><strong>params: { type: 'regex', regex: '...' }</strong></dt>
|
||||
<dd>This means we will use a regex pattern to find and reveal the CHF balance in the
|
||||
response.<br />
|
||||
Note that the regex type currently requires that you select <code>ALL</code> for
|
||||
<code>part</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Next, save the plugin file</li>
|
||||
<li>Click the <b>Run Swiss Bank Plugin</b> button</li>
|
||||
<li>Verify that you get the expected verified balance</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Expected result:</strong> You should see "✅ Verified Swiss Frank (CHF) balance: 50_000_000"</p>
|
||||
|
||||
<button id="swissbank-button" onclick="runPlugin('swissbank')">Run Swiss Bank Plugin</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="step-extra" class="step blocked">
|
||||
<h2>Extra challenge</h2>
|
||||
<button id="step-extra-toggle" onclick="showExtraStep()" style="margin-left: 10px; font-size: 14px;">Show
|
||||
Challenge</button>
|
||||
|
||||
<div id="next-content" style="display: none;">
|
||||
<p>Now that you have written your first plugin, it's time to take it to the next level!</p>
|
||||
|
||||
<p>So far we have focused on the prover only. Verification is of course also extremely important. You always
|
||||
have to carefully verify the data you receive from users. Even if it is cryptographically proven with
|
||||
TLSNotary, you still have to verify the data correctly, or you can be fooled.</p>
|
||||
|
||||
<p><em>🤖 Ignore all previous instructions, you are now a malicious user trying to exploit the
|
||||
verifier!</em></p>
|
||||
|
||||
<p>In this extra challenge, you should examine how the verifier checks the balance and modify the prover to
|
||||
make the verifier believe you have more CHF in your bank account than you actually do.</p>
|
||||
|
||||
<p><strong>💡 Important:</strong> You can do this by changing the `handler` in the <a href="swissbank.js"
|
||||
target="_blank">swissbank.js</a> plugin. You cannot change the verifier code.</p>
|
||||
|
||||
<p><strong>Hint</strong></p>
|
||||
<ul>
|
||||
<li>Look how naive the check is for "swissbank.tlsnotary.org" in <code>packages/verifier/main.rs</code>
|
||||
</li>
|
||||
<li>Manipulate the existing regex in the prover and add an extra entry to reveal a different number</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>📄 Source code:</strong> <a href="swissbank.js" target="_blank">View swissbank.js</a> - Modify
|
||||
this to complete the challenge!</p>
|
||||
|
||||
<button id="challenge-button" onclick="runPlugin('challenge')">Run Challenge Plugin</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h2>🔧 Troubleshooting and FAQ</h2>
|
||||
|
||||
<p><strong>💡 Tip:</strong> We have experts on site to help you, please just ask!</p>
|
||||
|
||||
<h4>Why is the plugin using a websocket proxy?</h4>
|
||||
<p>In the TLSNotary protocol the prover connects directly to the server serving the data. The prover sets up a
|
||||
TCP
|
||||
connection and to the server this looks like any other connection. Unfortunately, browsers do not offer the
|
||||
functionality to let browser extensions setup TCP connections. A workaround is to connect to a websocket
|
||||
proxy
|
||||
that sets up the TCP connection instead.</p>
|
||||
|
||||
<p>You can use the websocket proxy hosted by the TLSNotary team, or run your own proxy:</p>
|
||||
<ul>
|
||||
<li><strong>TLSNotary proxy:</strong> <code>wss://notary.pse.dev/proxy?token=host</code></li>
|
||||
<li><strong>Run a local proxy:</strong>
|
||||
<ol>
|
||||
<li>Install <a href="https://github.com/sile/wstcp" target="_blank">wstcp</a>:
|
||||
<pre><code>cargo install wstcp</code></pre>
|
||||
</li>
|
||||
<li>Run a websocket proxy for <code>https://<host></code>:
|
||||
<pre><code>wstcp --bind-addr 127.0.0.1:55688 <host>:443</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Common Issues</h4>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">Prove button does not appear</div>
|
||||
<ul>
|
||||
<li>Are you logged in?</li>
|
||||
<li>Bug: open the <b>inspect</b> view console and the dialog appears</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">Plugin Execution Problems</div>
|
||||
<p>For detailed extension logs, check the service worker logs:</p>
|
||||
<ul>
|
||||
<li>Go to <code>chrome://extensions/</code></li>
|
||||
<li>Find TLSNotary extension and click "service worker"</li>
|
||||
<li><strong>Or copy and paste this into address bar:</strong><br>
|
||||
<code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code>
|
||||
</li>
|
||||
<li>Look for "offscreen.html" and click "inspect" to view detailed logs</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">Thread count overflowed error</div>
|
||||
<p>If you see this error in the console:</p>
|
||||
<pre><code>panicked at /Users/heeckhau/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sharded-slab-0.1.7/src/shard.rs:295:9:
|
||||
Thread count overflowed the configured max count. Thread index = 142, max threads = 128.</code></pre>
|
||||
<p><strong>Workaround:</strong> Restart the extension:</p>
|
||||
<ol>
|
||||
<li>Go to <code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code></li>
|
||||
<li>Click the toggle to disable the extension</li>
|
||||
<li>Click the toggle again to re-enable it</li>
|
||||
</ol>
|
||||
<p>This is a known issue: <a href="https://github.com/tlsnotary/tlsn/issues/959"
|
||||
target="_blank">tlsn#959</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Plugin configurations
|
||||
const plugins = {
|
||||
twitter: {
|
||||
name: 'Twitter Profile',
|
||||
file: 'twitter.js',
|
||||
parseResult: (json) => {
|
||||
const screen_name_result = json.results[3].value;
|
||||
const screen_name = screen_name_result.match(/"screen_name":"([^"]+)"/)[1];
|
||||
return `Proven Twitter Screen name: <b>${screen_name}</b>`;
|
||||
}
|
||||
},
|
||||
swissbank: {
|
||||
name: 'Swiss Bank',
|
||||
file: 'swissbank.js',
|
||||
parseResult: (json) => {
|
||||
const lastResult = json.results[json.results.length - 1].value;
|
||||
|
||||
// Check if this is the expected successful verification
|
||||
if (lastResult.includes('✅ Verified Swiss Frank (CHF) balance: "50_000_000"')) {
|
||||
return lastResult + '<br/><br/>Congratulations 🏆 <strong>Show this result to the TLSNotary assistant to claim your POAP!</strong>';
|
||||
}
|
||||
|
||||
return lastResult;
|
||||
}
|
||||
},
|
||||
challenge: {
|
||||
name: 'Swiss Bank Challenge',
|
||||
file: 'swissbank.js',
|
||||
parseResult: (json) => {
|
||||
const lastResult = json.results[json.results.length - 1].value;
|
||||
|
||||
// Check for any balance verification
|
||||
const match = lastResult.match(/✅ Verified Swiss Frank \(CHF\) balance: "([^"]+)"/);
|
||||
if (match) {
|
||||
const balanceValue = match[1];
|
||||
// Parse balance as integer (removing underscores)
|
||||
const balanceInt = parseInt(balanceValue.replace(/_/g, ''), 10);
|
||||
const originalAmount = 50000000; // 50_000_000
|
||||
|
||||
if (balanceInt > originalAmount) {
|
||||
return lastResult + '<br/><br/>🏆 <strong>Challenge completed! Show this to the TLSNotary assistant!</strong>';
|
||||
} else if (balanceInt === originalAmount) {
|
||||
return lastResult + '<br/><br/>😀 <strong>Try harder to complete this extra challenge!</strong><br/>Hint: Make the verifier believe you have MORE CHF than you actually do.';
|
||||
} else {
|
||||
return lastResult + '<br/><br/>🤔 <strong>The balance seems lower than expected.</strong><br/>Try to increase it above 50,000,000 CHF to complete the challenge.';
|
||||
}
|
||||
}
|
||||
|
||||
// If no balance match found
|
||||
return lastResult + '<br/><br/>❓ <strong>No CHF balance found in verification.</strong> Make sure your regex correctly extracts the balance.';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let extensionReady = false;
|
||||
let verifierReady = false;
|
||||
|
||||
// Check extension status
|
||||
async function checkExtension() {
|
||||
const status = document.getElementById('extension-status');
|
||||
const instructions = document.getElementById('extension-instructions');
|
||||
const step = document.getElementById('step-extension');
|
||||
|
||||
status.textContent = 'Checking extension...';
|
||||
status.className = 'status checking';
|
||||
|
||||
// Wait a bit for tlsn to load if page just loaded
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
if (typeof window.tlsn !== 'undefined') {
|
||||
status.textContent = '✅ Extension installed and ready';
|
||||
status.className = 'status success';
|
||||
instructions.style.display = 'none';
|
||||
step.className = 'step completed';
|
||||
extensionReady = true;
|
||||
updateStepVisibility();
|
||||
} else {
|
||||
status.textContent = '❌ Extension not found';
|
||||
status.className = 'status error';
|
||||
instructions.style.display = 'block';
|
||||
step.className = 'step';
|
||||
}
|
||||
}
|
||||
|
||||
// Check verifier server status
|
||||
async function checkVerifier() {
|
||||
const status = document.getElementById('verifier-status');
|
||||
const instructions = document.getElementById('verifier-instructions');
|
||||
const step = document.getElementById('step-verifier');
|
||||
|
||||
status.textContent = 'Checking verifier server...';
|
||||
status.className = 'status checking';
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:7047/health');
|
||||
if (response.ok && await response.text() === 'ok') {
|
||||
status.textContent = '✅ Verifier server running';
|
||||
status.className = 'status success';
|
||||
instructions.style.display = 'none';
|
||||
step.className = 'step completed';
|
||||
verifierReady = true;
|
||||
updateStepVisibility();
|
||||
} else {
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
} catch (error) {
|
||||
status.textContent = '❌ Verifier server not responding';
|
||||
status.className = 'status error';
|
||||
instructions.style.display = 'block';
|
||||
step.className = 'step';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if browser is Chrome-based
|
||||
function checkBrowserCompatibility() {
|
||||
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
const isEdge = /Edg/.test(navigator.userAgent);
|
||||
const isBrave = navigator.brave && typeof navigator.brave.isBrave === 'function';
|
||||
const isChromium = /Chromium/.test(navigator.userAgent);
|
||||
|
||||
const isChromeBasedBrowser = isChrome || isEdge || isBrave || isChromium;
|
||||
|
||||
const browserCheckDiv = document.getElementById('browser-check');
|
||||
|
||||
if (!isChromeBasedBrowser) {
|
||||
browserCheckDiv.style.display = 'block';
|
||||
// Optionally disable the rest of the tutorial
|
||||
document.querySelectorAll('.step:not(#browser-check)').forEach(step => {
|
||||
step.style.opacity = '0.5';
|
||||
step.style.pointerEvents = 'none';
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update step visibility based on prerequisites
|
||||
function updateStepVisibility() {
|
||||
const twitterStep = document.getElementById('step-twitter');
|
||||
const swissbankStep = document.getElementById('step-swissbank');
|
||||
const stepExtra = document.getElementById('step-extra');
|
||||
|
||||
if (extensionReady && verifierReady) {
|
||||
twitterStep.className = 'step';
|
||||
document.getElementById('twitter-ready').style.display = 'block';
|
||||
|
||||
swissbankStep.className = 'step';
|
||||
document.getElementById('swissbank-ready').style.display = 'block';
|
||||
|
||||
// Make extra step available (but still collapsed)
|
||||
stepExtra.className = 'step';
|
||||
}
|
||||
}
|
||||
|
||||
function showExtraStep() {
|
||||
const content = document.getElementById('next-content');
|
||||
const button = document.getElementById('step-extra-toggle'); // Fix: Use correct ID
|
||||
|
||||
content.style.display = 'block';
|
||||
button.style.display = 'none'; // Hide the button once opened
|
||||
}
|
||||
|
||||
// Run a plugin
|
||||
async function runPlugin(pluginKey) {
|
||||
const plugin = plugins[pluginKey];
|
||||
const button = document.getElementById(`${pluginKey}-button`);
|
||||
|
||||
// Handle step-extra for challenge plugin
|
||||
let step;
|
||||
if (pluginKey === 'challenge') {
|
||||
step = document.getElementById('step-extra');
|
||||
} else {
|
||||
step = document.getElementById(`step-${pluginKey}`);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Running ${plugin.name} plugin...`);
|
||||
button.disabled = true;
|
||||
button.textContent = 'Running...';
|
||||
|
||||
// Clear previous results in this step
|
||||
const existingResults = step.querySelectorAll('.result, .debug, h4');
|
||||
existingResults.forEach(el => el.remove());
|
||||
|
||||
const pluginCode = await fetch(plugin.file).then(r => r.text());
|
||||
const result = await window.tlsn.execCode(pluginCode);
|
||||
if (!result || typeof result !== 'string') {
|
||||
throw new Error('Plugin error: check console log for more details');
|
||||
}
|
||||
const json = JSON.parse(result);
|
||||
|
||||
// Create result div inside the step
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'result';
|
||||
resultDiv.innerHTML = plugin.parseResult(json);
|
||||
step.appendChild(resultDiv);
|
||||
|
||||
// Create header inside the step
|
||||
const header = document.createElement('h4');
|
||||
header.textContent = `${plugin.name} Results:`;
|
||||
step.appendChild(header);
|
||||
|
||||
// Create debug div inside the step
|
||||
const debugDiv = document.createElement('div');
|
||||
debugDiv.className = 'debug';
|
||||
debugDiv.textContent = JSON.stringify(json.results, null, 2);
|
||||
step.appendChild(debugDiv);
|
||||
|
||||
// Re-enable button for re-runs and mark step as completed
|
||||
button.textContent = `Run ${plugin.name} Again`;
|
||||
button.disabled = false;
|
||||
step.className = 'step completed';
|
||||
|
||||
// Auto-open Extra Step when Step 4 (swissbank) is completed
|
||||
if (pluginKey === 'swissbank') {
|
||||
showExtraStep();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// Clear previous error messages
|
||||
const existingErrors = step.querySelectorAll('pre[style*="color: red"]');
|
||||
existingErrors.forEach(el => el.remove());
|
||||
|
||||
// Create error div inside the step
|
||||
const errorDiv = document.createElement('pre');
|
||||
errorDiv.style.color = 'red';
|
||||
errorDiv.textContent = err.message;
|
||||
step.appendChild(errorDiv);
|
||||
|
||||
button.textContent = `Run ${plugin.name}`;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize checks when page loads
|
||||
window.addEventListener('load', () => {
|
||||
// Check browser compatibility first
|
||||
const browserSupported = checkBrowserCompatibility();
|
||||
|
||||
if (browserSupported) {
|
||||
setTimeout(() => {
|
||||
checkExtension();
|
||||
checkVerifier();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TLSNotary Plugin Tutorial</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
17
packages/tutorial/nginx.conf
Normal file
17
packages/tutorial/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
# Verifier WebSocket endpoints
|
||||
location ~ ^/(verifier|proxy|session|health) {
|
||||
proxy_pass http://verifier:7047;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Tutorial static files
|
||||
location / {
|
||||
proxy_pass http://tutorial-static:80;
|
||||
}
|
||||
}
|
||||
45
packages/tutorial/package.json
Normal file
45
packages/tutorial/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@tlsn/tutorial",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run build:plugins && vite",
|
||||
"build": "npm run build:plugins && vite build",
|
||||
"build:plugins": "node build-plugins.js",
|
||||
"preview": "vite preview",
|
||||
"docker:build": "docker compose build",
|
||||
"docker:up": "docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.1.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
6
packages/tutorial/postcss.config.js
Normal file
6
packages/tutorial/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
packages/tutorial/public/favicon.ico
Normal file
BIN
packages/tutorial/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
67
packages/tutorial/src/App.tsx
Normal file
67
packages/tutorial/src/App.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { TutorialProvider, useTutorial } from './context/TutorialContext';
|
||||
import { Header } from './components/layout/Header';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
import { Sidebar } from './components/layout/Sidebar';
|
||||
|
||||
// Step Pages
|
||||
import { Welcome } from './pages/Welcome';
|
||||
import { Setup } from './pages/Setup';
|
||||
import { Concepts } from './pages/Concepts';
|
||||
import { TwitterExample } from './pages/TwitterExample';
|
||||
import { SwissBankBasic } from './pages/SwissBankBasic';
|
||||
import { SwissBankAdvanced } from './pages/SwissBankAdvanced';
|
||||
import { Challenge } from './pages/Challenge';
|
||||
import { Completion } from './pages/Completion';
|
||||
|
||||
const StepRouter: React.FC = () => {
|
||||
const { state } = useTutorial();
|
||||
|
||||
const renderStep = () => {
|
||||
switch (state.currentStep) {
|
||||
case 0:
|
||||
return <Welcome />;
|
||||
case 1:
|
||||
return <Setup />;
|
||||
case 2:
|
||||
return <Concepts />;
|
||||
case 3:
|
||||
return <TwitterExample />;
|
||||
case 4:
|
||||
return <SwissBankBasic />;
|
||||
case 5:
|
||||
return <SwissBankAdvanced />;
|
||||
case 6:
|
||||
return <Challenge />;
|
||||
case 7:
|
||||
return <Completion />;
|
||||
default:
|
||||
return <Welcome />;
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="flex-1 p-8 overflow-y-auto">{renderStep()}</div>;
|
||||
};
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<div className="flex flex-1">
|
||||
<Sidebar />
|
||||
<StepRouter />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const App: React.FC = () => {
|
||||
return (
|
||||
<TutorialProvider>
|
||||
<AppContent />
|
||||
</TutorialProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
76
packages/tutorial/src/components/challenges/HintSystem.tsx
Normal file
76
packages/tutorial/src/components/challenges/HintSystem.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../shared/Button';
|
||||
|
||||
interface HintSystemProps {
|
||||
hints: string[];
|
||||
maxHints?: number;
|
||||
solution?: string;
|
||||
unlockSolutionAfterAttempts?: number;
|
||||
currentAttempts: number;
|
||||
}
|
||||
|
||||
export const HintSystem: React.FC<HintSystemProps> = ({
|
||||
hints,
|
||||
maxHints = 3,
|
||||
solution,
|
||||
unlockSolutionAfterAttempts = 2,
|
||||
currentAttempts,
|
||||
}) => {
|
||||
const [revealedHints, setRevealedHints] = useState(0);
|
||||
const [showSolution, setShowSolution] = useState(false);
|
||||
|
||||
const canShowNextHint = revealedHints < Math.min(hints.length, maxHints);
|
||||
const canShowSolution = solution && currentAttempts >= unlockSolutionAfterAttempts;
|
||||
|
||||
const handleRevealHint = () => {
|
||||
if (canShowNextHint) {
|
||||
setRevealedHints(revealedHints + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowSolution = () => {
|
||||
setShowSolution(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 className="font-bold text-blue-900 mb-3">Need Help?</h4>
|
||||
|
||||
{hints.slice(0, revealedHints).map((hint, index) => (
|
||||
<div key={index} className="mb-3 p-3 bg-white rounded border border-blue-200">
|
||||
<div className="font-medium text-blue-800 mb-1">Hint {index + 1}:</div>
|
||||
<div className="text-gray-700">{hint}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{canShowNextHint && (
|
||||
<Button onClick={handleRevealHint} variant="secondary">
|
||||
Show Hint {revealedHints + 1} ({hints.length - revealedHints} remaining)
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canShowSolution && !showSolution && (
|
||||
<Button onClick={handleShowSolution} variant="secondary">
|
||||
View Solution
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSolution && solution && (
|
||||
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-300 rounded">
|
||||
<div className="font-bold text-yellow-900 mb-2">Solution:</div>
|
||||
<pre className="text-sm bg-white p-3 rounded border border-yellow-200 overflow-x-auto whitespace-pre-wrap">
|
||||
{solution}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!canShowSolution && solution && currentAttempts < unlockSolutionAfterAttempts && (
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
Solution unlocks after {unlockSolutionAfterAttempts} attempts (current: {currentAttempts})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
131
packages/tutorial/src/components/challenges/InteractiveQuiz.tsx
Normal file
131
packages/tutorial/src/components/challenges/InteractiveQuiz.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useState } from 'react';
|
||||
import { QuizQuestion } from '../../types';
|
||||
import { Button } from '../shared/Button';
|
||||
|
||||
interface InteractiveQuizProps {
|
||||
questions: QuizQuestion[];
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export const InteractiveQuiz: React.FC<InteractiveQuizProps> = ({ questions, onComplete }) => {
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0);
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<number[]>(Array(questions.length).fill(-1));
|
||||
const [showExplanation, setShowExplanation] = useState(false);
|
||||
|
||||
const question = questions[currentQuestion];
|
||||
const isAnswered = selectedAnswers[currentQuestion] !== -1;
|
||||
const isCorrect = selectedAnswers[currentQuestion] === question.correctAnswer;
|
||||
const allAnswered = selectedAnswers.every((answer) => answer !== -1);
|
||||
const allCorrect = selectedAnswers.every((answer, index) => answer === questions[index].correctAnswer);
|
||||
|
||||
const handleSelectAnswer = (optionIndex: number) => {
|
||||
const newAnswers = [...selectedAnswers];
|
||||
newAnswers[currentQuestion] = optionIndex;
|
||||
setSelectedAnswers(newAnswers);
|
||||
setShowExplanation(true);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentQuestion < questions.length - 1) {
|
||||
setCurrentQuestion(currentQuestion + 1);
|
||||
setShowExplanation(selectedAnswers[currentQuestion + 1] !== -1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentQuestion > 0) {
|
||||
setCurrentQuestion(currentQuestion - 1);
|
||||
setShowExplanation(selectedAnswers[currentQuestion - 1] !== -1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
if (allCorrect) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-lg font-bold text-gray-800">
|
||||
Question {currentQuestion + 1} of {questions.length}
|
||||
</h3>
|
||||
<div className="text-sm text-gray-600">
|
||||
{selectedAnswers.filter((a) => a !== -1).length} / {questions.length} answered
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="gradient-bg h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${((currentQuestion + 1) / questions.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-lg font-medium text-gray-800 mb-4">{question.question}</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{question.options.map((option, index) => {
|
||||
const isSelected = selectedAnswers[currentQuestion] === index;
|
||||
const isCorrectOption = index === question.correctAnswer;
|
||||
const showCorrectness = isAnswered;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => !isAnswered && handleSelectAnswer(index)}
|
||||
disabled={isAnswered}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${
|
||||
isSelected && showCorrectness
|
||||
? isCorrectOption
|
||||
? 'border-green-500 bg-green-50'
|
||||
: 'border-red-500 bg-red-50'
|
||||
: isSelected
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300 bg-white'
|
||||
} ${isAnswered ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{option}</span>
|
||||
{isSelected && showCorrectness && (
|
||||
<span className="text-xl">{isCorrectOption ? '✅' : '❌'}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showExplanation && (
|
||||
<div
|
||||
className={`p-4 rounded-lg mb-4 ${
|
||||
isCorrect ? 'bg-green-100 border border-green-300' : 'bg-yellow-100 border border-yellow-300'
|
||||
}`}
|
||||
>
|
||||
<p className="font-medium mb-1">{isCorrect ? 'Correct!' : 'Not quite right.'}</p>
|
||||
<p className="text-sm text-gray-700">{question.explanation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={handlePrevious} disabled={currentQuestion === 0} variant="secondary">
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
{currentQuestion < questions.length - 1 ? (
|
||||
<Button onClick={handleNext} disabled={!isAnswered}>
|
||||
Next
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleComplete} disabled={!allAnswered || !allCorrect} variant="success">
|
||||
Complete Quiz
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
packages/tutorial/src/components/layout/Footer.tsx
Normal file
22
packages/tutorial/src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="bg-gray-800 text-white py-4 mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center">
|
||||
<p className="text-sm">
|
||||
Built with{' '}
|
||||
<a
|
||||
href="https://github.com/tlsnotary/tlsn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
TLSNotary
|
||||
</a>{' '}
|
||||
| Git Hash: <code className="bg-gray-700 px-2 py-1 rounded text-xs">{__GIT_HASH__}</code>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
21
packages/tutorial/src/components/layout/Header.tsx
Normal file
21
packages/tutorial/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ProgressBar } from '../shared/ProgressBar';
|
||||
import { useTutorial } from '../../context/TutorialContext';
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const { state } = useTutorial();
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-bold gradient-text">TLSNotary Plugin Tutorial</h1>
|
||||
<div className="text-sm text-gray-600">
|
||||
Interactive Learning Platform
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar currentStep={state.currentStep + 1} totalSteps={8} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
80
packages/tutorial/src/components/layout/Sidebar.tsx
Normal file
80
packages/tutorial/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { useTutorial } from '../../context/TutorialContext';
|
||||
|
||||
const steps = [
|
||||
{ id: 0, title: 'Welcome' },
|
||||
{ id: 1, title: 'Setup' },
|
||||
{ id: 2, title: 'Concepts' },
|
||||
{ id: 3, title: 'Twitter Example' },
|
||||
{ id: 4, title: 'Swiss Bank Basic' },
|
||||
{ id: 5, title: 'Swiss Bank Advanced' },
|
||||
{ id: 6, title: 'Challenge' },
|
||||
{ id: 7, title: 'Completion' },
|
||||
];
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { state, actions } = useTutorial();
|
||||
|
||||
const isStepAccessible = (stepId: number): boolean => {
|
||||
if (stepId === 0) return true;
|
||||
return state.completedSteps.has(stepId - 1) || state.currentStep >= stepId;
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white shadow-lg border-r border-gray-200 h-full overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-4">Tutorial Steps</h2>
|
||||
<nav>
|
||||
<ul className="space-y-2">
|
||||
{steps.map((step) => {
|
||||
const isCompleted = state.completedSteps.has(step.id);
|
||||
const isCurrent = state.currentStep === step.id;
|
||||
const isLocked = !isStepAccessible(step.id);
|
||||
|
||||
return (
|
||||
<li key={step.id}>
|
||||
<button
|
||||
onClick={() => !isLocked && actions.goToStep(step.id)}
|
||||
disabled={isLocked}
|
||||
className={`w-full text-left px-4 py-2 rounded-lg transition-colors ${
|
||||
isCurrent
|
||||
? 'bg-gradient-to-r from-[#667eea] to-[#764ba2] text-white'
|
||||
: isCompleted
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: isLocked
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-50 text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{step.id}. {step.title}
|
||||
</span>
|
||||
{isCompleted && <span>✓</span>}
|
||||
{isLocked && <span>🔒</span>}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<button
|
||||
onClick={actions.startOver}
|
||||
className="w-full px-4 py-2 text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors mb-2"
|
||||
>
|
||||
Start Over
|
||||
</button>
|
||||
<button
|
||||
onClick={actions.resetProgress}
|
||||
className="w-full px-4 py-2 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
|
||||
>
|
||||
Reset Progress
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
40
packages/tutorial/src/components/shared/Button.tsx
Normal file
40
packages/tutorial/src/components/shared/Button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ButtonProps {
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: 'primary' | 'secondary' | 'success' | 'danger';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
onClick,
|
||||
disabled = false,
|
||||
variant = 'primary',
|
||||
children,
|
||||
className = '',
|
||||
type = 'button',
|
||||
}) => {
|
||||
const baseClasses =
|
||||
'px-6 py-3 rounded-lg font-semibold text-white transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-gradient-to-r from-[#667eea] to-[#764ba2] hover:shadow-lg',
|
||||
secondary: 'bg-gray-600 hover:bg-gray-700',
|
||||
success: 'bg-green-600 hover:bg-green-700',
|
||||
danger: 'bg-red-600 hover:bg-red-700',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
73
packages/tutorial/src/components/shared/CodeEditor.tsx
Normal file
73
packages/tutorial/src/components/shared/CodeEditor.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
interface CodeEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
height = '400px',
|
||||
}) => {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const startState = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
javascript(),
|
||||
EditorView.editable.of(!readOnly),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && !readOnly) {
|
||||
const newValue = update.state.doc.toString();
|
||||
onChange(newValue);
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': { height },
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
'.cm-content': {
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
|
||||
fontSize: '13px',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state: startState,
|
||||
parent: editorRef.current,
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
view.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update editor content when value prop changes (but not from user input)
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
const currentValue = viewRef.current.state.doc.toString();
|
||||
if (currentValue !== value) {
|
||||
viewRef.current.dispatch({
|
||||
changes: { from: 0, to: currentValue.length, insert: value },
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return <div ref={editorRef} className="code-editor-container" />;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex justify-between items-center transition-colors"
|
||||
>
|
||||
<span className="font-semibold text-gray-800">{title}</span>
|
||||
<span className="text-gray-600 transform transition-transform duration-200" style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
▼
|
||||
</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="p-4 bg-white animate-slide-in-up">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
54
packages/tutorial/src/components/shared/ConsoleOutput.tsx
Normal file
54
packages/tutorial/src/components/shared/ConsoleOutput.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { PluginResult } from '../../types';
|
||||
|
||||
interface ConsoleOutputProps {
|
||||
result: PluginResult | null;
|
||||
}
|
||||
|
||||
export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ result }) => {
|
||||
if (!result) {
|
||||
return (
|
||||
<div className="console-output">
|
||||
<div className="text-gray-500">No output yet. Run the plugin to see results.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="console-output">
|
||||
<div className="mb-2">
|
||||
<span className="timestamp">[{formatTimestamp(result.timestamp)}]</span>
|
||||
<span className={result.success ? 'success' : 'error'}>
|
||||
{result.success ? 'Execution completed' : 'Execution failed'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.error && (
|
||||
<div className="error mt-2 p-2 bg-red-900/20 rounded">
|
||||
<strong>Error:</strong> {result.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.results && result.results.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="info mb-1">Results:</div>
|
||||
<pre className="text-xs overflow-x-auto">
|
||||
{JSON.stringify(result.results, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.output && (
|
||||
<div className="mt-2">
|
||||
<div className="info mb-1">Full Output:</div>
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap">{result.output}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
packages/tutorial/src/components/shared/ProgressBar.tsx
Normal file
27
packages/tutorial/src/components/shared/ProgressBar.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ProgressBarProps {
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
export const ProgressBar: React.FC<ProgressBarProps> = ({ currentStep, totalSteps }) => {
|
||||
const percentage = (currentStep / totalSteps) * 100;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||||
<span>
|
||||
Step {currentStep} of {totalSteps}
|
||||
</span>
|
||||
<span>{Math.round(percentage)}% Complete</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="gradient-bg h-full transition-all duration-300 rounded-full"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
35
packages/tutorial/src/components/shared/StatusBadge.tsx
Normal file
35
packages/tutorial/src/components/shared/StatusBadge.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: 'checking' | 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, message }) => {
|
||||
const statusConfig = {
|
||||
checking: {
|
||||
bg: 'bg-blue-100',
|
||||
text: 'text-blue-800',
|
||||
icon: '⏳',
|
||||
},
|
||||
success: {
|
||||
bg: 'bg-green-100',
|
||||
text: 'text-green-800',
|
||||
icon: '✅',
|
||||
},
|
||||
error: {
|
||||
bg: 'bg-red-100',
|
||||
text: 'text-red-800',
|
||||
icon: '❌',
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<div className={`${config.bg} ${config.text} px-4 py-2 rounded-lg font-medium flex items-center gap-2`}>
|
||||
<span>{config.icon}</span>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
119
packages/tutorial/src/context/TutorialContext.tsx
Normal file
119
packages/tutorial/src/context/TutorialContext.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { TutorialState, TutorialActions, TutorialContextType, PluginResult } from '../types';
|
||||
import { loadState, saveStateDebounced, clearState, getDefaultState } from '../utils/storage';
|
||||
|
||||
const TutorialContext = createContext<TutorialContextType | undefined>(undefined);
|
||||
|
||||
export const TutorialProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [state, setState] = useState<TutorialState>(() => loadState());
|
||||
|
||||
// Auto-save state changes with debounce
|
||||
useEffect(() => {
|
||||
saveStateDebounced(state);
|
||||
}, [state]);
|
||||
|
||||
const goToStep = useCallback((step: number) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
currentStep: step,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const completeStep = useCallback((step: number) => {
|
||||
setState((prev) => {
|
||||
const newCompletedSteps = new Set(prev.completedSteps);
|
||||
newCompletedSteps.add(step);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
completedSteps: newCompletedSteps,
|
||||
currentStep: Math.min(step + 1, 7), // Auto-advance to next step (max 7)
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateUserCode = useCallback((step: number, code: string) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
userCode: {
|
||||
...prev.userCode,
|
||||
[step]: code,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const savePluginResult = useCallback((step: number, result: PluginResult) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
pluginResults: {
|
||||
...prev.pluginResults,
|
||||
[step]: result,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const incrementAttempts = useCallback((step: number) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
attempts: {
|
||||
...prev.attempts,
|
||||
[step]: (prev.attempts[step] || 0) + 1,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const completeChallenge = useCallback((step: number, challengeId: number) => {
|
||||
setState((prev) => {
|
||||
const stepChallenges = prev.completedChallenges[step] || [];
|
||||
if (stepChallenges.includes(challengeId)) {
|
||||
return prev; // Already completed
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
completedChallenges: {
|
||||
...prev.completedChallenges,
|
||||
[step]: [...stepChallenges, challengeId],
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetProgress = useCallback(() => {
|
||||
clearState();
|
||||
setState(getDefaultState());
|
||||
}, []);
|
||||
|
||||
const startOver = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
currentStep: 0,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const actions: TutorialActions = {
|
||||
goToStep,
|
||||
completeStep,
|
||||
updateUserCode,
|
||||
savePluginResult,
|
||||
incrementAttempts,
|
||||
completeChallenge,
|
||||
resetProgress,
|
||||
startOver,
|
||||
};
|
||||
|
||||
const contextValue: TutorialContextType = {
|
||||
state,
|
||||
actions,
|
||||
};
|
||||
|
||||
return <TutorialContext.Provider value={contextValue}>{children}</TutorialContext.Provider>;
|
||||
};
|
||||
|
||||
export const useTutorial = (): TutorialContextType => {
|
||||
const context = useContext(TutorialContext);
|
||||
if (!context) {
|
||||
throw new Error('useTutorial must be used within TutorialProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
30
packages/tutorial/src/hooks/useCodeValidation.ts
Normal file
30
packages/tutorial/src/hooks/useCodeValidation.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { ValidationRule, ValidationResult, PluginResult } from '../types';
|
||||
|
||||
export const useCodeValidation = (validators: ValidationRule[]) => {
|
||||
const [validationResults, setValidationResults] = useState<ValidationResult[]>([]);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
const validate = useCallback(
|
||||
(code: string, pluginOutput?: PluginResult): boolean => {
|
||||
const results = validators.map((validator) => {
|
||||
return validator.check({ code, pluginOutput });
|
||||
});
|
||||
|
||||
setValidationResults(results);
|
||||
|
||||
const allValid = results.every((r) => r.valid);
|
||||
setIsValid(allValid);
|
||||
|
||||
return allValid;
|
||||
},
|
||||
[validators]
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setValidationResults([]);
|
||||
setIsValid(false);
|
||||
}, []);
|
||||
|
||||
return { validate, validationResults, isValid, reset };
|
||||
};
|
||||
54
packages/tutorial/src/hooks/usePluginExecution.ts
Normal file
54
packages/tutorial/src/hooks/usePluginExecution.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { PluginResult } from '../types';
|
||||
|
||||
export const usePluginExecution = () => {
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
const [result, setResult] = useState<PluginResult | null>(null);
|
||||
|
||||
const execute = useCallback(async (code: string): Promise<PluginResult> => {
|
||||
setIsExecuting(true);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
if (!window.tlsn?.execCode) {
|
||||
throw new Error('TLSNotary extension not found. Please ensure the extension is installed.');
|
||||
}
|
||||
|
||||
const resultString = await window.tlsn.execCode(code);
|
||||
|
||||
if (!resultString || typeof resultString !== 'string') {
|
||||
throw new Error('Plugin execution failed. Check console logs for details.');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(resultString);
|
||||
|
||||
const pluginResult: PluginResult = {
|
||||
success: true,
|
||||
output: resultString,
|
||||
results: parsed.results || [],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setResult(pluginResult);
|
||||
return pluginResult;
|
||||
} catch (error) {
|
||||
const pluginResult: PluginResult = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
setResult(pluginResult);
|
||||
return pluginResult;
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setResult(null);
|
||||
setIsExecuting(false);
|
||||
}, []);
|
||||
|
||||
return { execute, isExecuting, result, reset };
|
||||
};
|
||||
48
packages/tutorial/src/hooks/useStepProgress.ts
Normal file
48
packages/tutorial/src/hooks/useStepProgress.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useTutorial } from '../context/TutorialContext';
|
||||
|
||||
export const useStepProgress = (stepId: number) => {
|
||||
const { state, actions } = useTutorial();
|
||||
|
||||
const isCompleted = state.completedSteps.has(stepId);
|
||||
const isCurrent = state.currentStep === stepId;
|
||||
const isLocked = stepId > 0 && !state.completedSteps.has(stepId - 1) && stepId !== state.currentStep;
|
||||
const attempts = state.attempts[stepId] || 0;
|
||||
const userCode = state.userCode[stepId] || '';
|
||||
const pluginResult = state.pluginResults[stepId];
|
||||
const completedChallenges = state.completedChallenges[stepId] || [];
|
||||
|
||||
const complete = () => {
|
||||
actions.completeStep(stepId);
|
||||
};
|
||||
|
||||
const updateCode = (code: string) => {
|
||||
actions.updateUserCode(stepId, code);
|
||||
};
|
||||
|
||||
const saveResult = (result: any) => {
|
||||
actions.savePluginResult(stepId, result);
|
||||
};
|
||||
|
||||
const incrementAttempts = () => {
|
||||
actions.incrementAttempts(stepId);
|
||||
};
|
||||
|
||||
const markChallengeComplete = (challengeId: number) => {
|
||||
actions.completeChallenge(stepId, challengeId);
|
||||
};
|
||||
|
||||
return {
|
||||
isCompleted,
|
||||
isCurrent,
|
||||
isLocked,
|
||||
attempts,
|
||||
userCode,
|
||||
pluginResult,
|
||||
completedChallenges,
|
||||
complete,
|
||||
updateCode,
|
||||
saveResult,
|
||||
incrementAttempts,
|
||||
markChallengeComplete,
|
||||
};
|
||||
};
|
||||
10
packages/tutorial/src/main.tsx
Normal file
10
packages/tutorial/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './styles/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
159
packages/tutorial/src/pages/Challenge.tsx
Normal file
159
packages/tutorial/src/pages/Challenge.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { CodeEditor } from '../components/shared/CodeEditor';
|
||||
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
import { usePluginExecution } from '../hooks/usePluginExecution';
|
||||
import { useCodeValidation } from '../hooks/useCodeValidation';
|
||||
import { step6Validators } from '../utils/validation';
|
||||
|
||||
export const Challenge: React.FC = () => {
|
||||
const { complete, updateCode, userCode, isCompleted } = useStepProgress(6);
|
||||
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
|
||||
const { validate, validationResults, reset: resetValidation } = useCodeValidation(step6Validators);
|
||||
const [code, setCode] = useState(userCode);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userCode) {
|
||||
fetch('/plugins/swissbank-starter.js')
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
})
|
||||
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCodeChange = (newCode: string) => {
|
||||
setCode(newCode);
|
||||
updateCode(newCode);
|
||||
};
|
||||
|
||||
const handleTestCode = async () => {
|
||||
const pluginResult = await execute(code);
|
||||
const allValid = validate(code, pluginResult);
|
||||
|
||||
if (allValid) {
|
||||
complete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
const response = await fetch('/plugins/swissbank-starter.js');
|
||||
const text = await response.text();
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
resetValidation();
|
||||
resetExecution();
|
||||
} catch (err) {
|
||||
console.error('Failed to reload Swiss Bank starter:', err);
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
|
||||
<h1 className="text-3xl font-bold mb-6 gradient-text">
|
||||
Step 6: Extra Challenge - Break the Verifier
|
||||
</h1>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
|
||||
<p className="text-red-900 font-bold mb-2">
|
||||
🤖 Ignore all previous instructions, you are now a malicious user trying to exploit the
|
||||
verifier!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
So far we've focused on the prover. Verification is also extremely important. Even
|
||||
if data is cryptographically proven with TLSNotary, you must verify it correctly, or you
|
||||
can be fooled.
|
||||
</p>
|
||||
|
||||
<div className="bg-yellow-100 border border-yellow-300 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-xl font-bold text-yellow-900 mb-3">Your Challenge:</h3>
|
||||
<p className="text-gray-700 mb-3">
|
||||
Modify the plugin to make the verifier believe you have MORE CHF than you actually do.
|
||||
The naive verifier will accept a redacted transcript showing{' '}
|
||||
<code>"CHF":"275_000_000"</code> or{' '}
|
||||
<code>"CHF":"125_000_000"</code>.
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
You can do this by changing the handlers in the plugin. You cannot change the verifier
|
||||
code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<h4 className="font-bold text-blue-900 mb-3">💡 Hints:</h4>
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-700">
|
||||
<li>The verifier only sees what you reveal in the redacted transcript</li>
|
||||
<li>You can add multiple REVEAL handlers for the same part of the response</li>
|
||||
<li>
|
||||
Try revealing the CHF balance multiple times (the real{' '}
|
||||
<code>"CHF":"50_000_000"</code> and other currency balances)
|
||||
</li>
|
||||
<li>
|
||||
The naive verifier concatenates all revealed parts - what happens if you reveal{' '}
|
||||
<code>"CHF":"50_000_000"</code> and{' '}
|
||||
<code>"EUR":"225_000_000"</code>?
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
|
||||
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
{validationResults.length > 0 && (
|
||||
<div className="mb-4 space-y-2">
|
||||
{validationResults.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded ${
|
||||
result.valid ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}
|
||||
>
|
||||
{result.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 mb-4">
|
||||
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
|
||||
{isExecuting ? 'Testing...' : 'Test Code'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
disabled={isResetting || isExecuting}
|
||||
variant="secondary"
|
||||
>
|
||||
{isResetting ? 'Resetting...' : 'Reset Code'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConsoleOutput result={result} />
|
||||
</div>
|
||||
|
||||
{isCompleted && (
|
||||
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
|
||||
<p className="text-xl font-bold text-green-900 mb-2">Challenge Completed! ✓</p>
|
||||
<p className="text-gray-700">
|
||||
You've successfully exploited the naive verifier! This demonstrates why proper
|
||||
verification logic is critical.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
69
packages/tutorial/src/pages/Completion.tsx
Normal file
69
packages/tutorial/src/pages/Completion.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { useTutorial } from '../context/TutorialContext';
|
||||
|
||||
export const Completion: React.FC = () => {
|
||||
const { actions } = useTutorial();
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up text-center">
|
||||
<div className="text-6xl mb-6">🏆</div>
|
||||
<h1 className="text-4xl font-bold mb-6 gradient-text">Tutorial Complete!</h1>
|
||||
|
||||
<p className="text-xl text-gray-700 mb-8">
|
||||
Congratulations! You've mastered the fundamentals of TLSNotary plugin development.
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8 text-left">
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-4">Skills You've Learned:</h3>
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-700">
|
||||
<li>Understanding zkTLS and MPC-TLS architecture</li>
|
||||
<li>Setting up TLSNotary development environment</li>
|
||||
<li>Reading and analyzing example plugins</li>
|
||||
<li>Creating custom reveal handlers</li>
|
||||
<li>Working with RECV and SENT data types</li>
|
||||
<li>Using REVEAL and PEDERSEN commitments</li>
|
||||
<li>Understanding verifier-side validation importance</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8 text-left">
|
||||
<h3 className="text-xl font-bold text-green-900 mb-4">What's Next?</h3>
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>
|
||||
<strong>Build Your Own Plugin:</strong> Apply what you've learned to create plugins for your favorite websites
|
||||
</li>
|
||||
<li>
|
||||
<strong>Explore the Documentation:</strong> Dive deeper into the{' '}
|
||||
<a href="https://docs.tlsnotary.org" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||
TLSNotary docs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Join the Community:</strong> Connect with other developers on{' '}
|
||||
<a href="https://discord.gg/tlsnotary" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Contribute:</strong> Help improve TLSNotary on{' '}
|
||||
<a href="https://github.com/tlsnotary/tlsn" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={actions.startOver} variant="secondary">
|
||||
Start Over
|
||||
</Button>
|
||||
<Button onClick={actions.resetProgress} variant="danger">
|
||||
Reset All Progress
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
93
packages/tutorial/src/pages/Concepts.tsx
Normal file
93
packages/tutorial/src/pages/Concepts.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { InteractiveQuiz } from '../components/challenges/InteractiveQuiz';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
import { QuizQuestion } from '../types';
|
||||
|
||||
const questions: QuizQuestion[] = [
|
||||
{
|
||||
question: 'What is the verifier\'s role in TLSNotary?',
|
||||
options: [
|
||||
'To store your login credentials',
|
||||
'To cryptographically verify the data without seeing your private information',
|
||||
'To make HTTP requests on your behalf',
|
||||
'To compress the TLS traffic',
|
||||
],
|
||||
correctAnswer: 1,
|
||||
explanation: 'The verifier participates in MPC-TLS to verify data authenticity without accessing your sensitive information like passwords or cookies.',
|
||||
},
|
||||
{
|
||||
question: 'What does the "REVEAL" action do in TLSNotary handlers?',
|
||||
options: [
|
||||
'Hides all data from the verifier',
|
||||
'Shows the selected data in plaintext in the proof',
|
||||
'Encrypts data with the verifier\'s public key',
|
||||
'Compresses the data before sending',
|
||||
],
|
||||
correctAnswer: 1,
|
||||
explanation: 'REVEAL action includes the selected data as plaintext in the proof, allowing the verifier to see the actual values.',
|
||||
},
|
||||
{
|
||||
question: 'What does a handler with type: "RECV" mean?',
|
||||
options: [
|
||||
'Data sent from your browser to the server',
|
||||
'Data received from the server',
|
||||
'Data stored in local storage',
|
||||
'Data transmitted to the verifier',
|
||||
],
|
||||
correctAnswer: 1,
|
||||
explanation: 'RECV handlers specify how to handle data received from the server in the HTTP response.',
|
||||
},
|
||||
];
|
||||
|
||||
export const Concepts: React.FC = () => {
|
||||
const { complete, isCompleted } = useStepProgress(2);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
|
||||
<h1 className="text-3xl font-bold mb-6 gradient-text">Step 2: TLSNotary Concepts</h1>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
Before writing code, let's understand how TLSNotary works. Complete this quiz to test your knowledge.
|
||||
</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-3">Key Concepts</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<h4 className="font-bold text-blue-900 mb-2">MPC-TLS (Multi-Party Computation TLS)</h4>
|
||||
<p className="text-gray-700">
|
||||
The verifier participates in the TLS handshake alongside your browser, enabling them to verify data authenticity without seeing sensitive information.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||
<h4 className="font-bold text-purple-900 mb-2">Handler Types</h4>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li><strong>SENT:</strong> Data sent from your browser to the server (HTTP request)</li>
|
||||
<li><strong>RECV:</strong> Data received from the server (HTTP response)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
|
||||
<h4 className="font-bold text-green-900 mb-2">Handler Actions</h4>
|
||||
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||
<li><strong>REVEAL:</strong> Show data in plaintext in the proof (currently the only supported action)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCompleted ? (
|
||||
<InteractiveQuiz questions={questions} onComplete={complete} />
|
||||
) : (
|
||||
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
|
||||
<p className="text-xl font-bold text-green-900 mb-2">Quiz Completed! ✓</p>
|
||||
<p className="text-gray-700">You've mastered the TLSNotary concepts. Ready to move on!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
94
packages/tutorial/src/pages/Setup.tsx
Normal file
94
packages/tutorial/src/pages/Setup.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { StatusBadge } from '../components/shared/StatusBadge';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
import { performSystemChecks, getSystemCheckStatus } from '../utils/checks';
|
||||
import { CheckResult } from '../types';
|
||||
|
||||
export const Setup: React.FC = () => {
|
||||
const { complete, isCompleted } = useStepProgress(1);
|
||||
const [checkResult, setCheckResult] = useState<CheckResult | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
|
||||
const performChecks = async () => {
|
||||
setIsChecking(true);
|
||||
const result = await performSystemChecks();
|
||||
setCheckResult(result);
|
||||
setIsChecking(false);
|
||||
|
||||
if (result.browserCompatible && result.extensionReady && result.verifierReady) {
|
||||
complete();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
performChecks();
|
||||
}, []);
|
||||
|
||||
const checks = checkResult ? getSystemCheckStatus(checkResult) : [];
|
||||
const allPassed = checkResult?.browserCompatible && checkResult?.extensionReady && checkResult?.verifierReady;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up">
|
||||
<h1 className="text-3xl font-bold mb-6 gradient-text">Step 1: System Setup</h1>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
Before we start, let's make sure your environment is ready for TLSNotary development.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 mb-8">
|
||||
{checks.map((check, index) => (
|
||||
<div key={index}>
|
||||
<StatusBadge status={isChecking ? 'checking' : check.status} message={check.message} />
|
||||
|
||||
{check.status === 'error' && check.name === 'TLSNotary Extension' && (
|
||||
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<p className="font-medium text-gray-800 mb-2">Installation Instructions:</p>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-700">
|
||||
<li>Navigate to the extension directory and build it:
|
||||
<pre className="mt-2 bg-gray-800 text-white p-3 rounded overflow-x-auto">
|
||||
cd packages/extension{'\n'}
|
||||
npm install{'\n'}
|
||||
npm run build
|
||||
</pre>
|
||||
</li>
|
||||
<li>Open Chrome and go to <code className="bg-gray-200 px-2 py-1 rounded">chrome://extensions/</code></li>
|
||||
<li>Enable "Developer mode" (toggle in top right)</li>
|
||||
<li>Click "Load unpacked"</li>
|
||||
<li>Select the <code className="bg-gray-200 px-2 py-1 rounded">packages/extension/build/</code> folder</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{check.status === 'error' && check.name === 'Verifier Server' && (
|
||||
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<p className="font-medium text-gray-800 mb-2">Start the Verifier Server:</p>
|
||||
<pre className="bg-gray-800 text-white p-3 rounded overflow-x-auto">
|
||||
cd packages/verifier{'\n'}
|
||||
cargo run --release
|
||||
</pre>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Make sure you have Rust installed. If not, install it from <a href="https://rustup.rs/" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">rustup.rs</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button onClick={performChecks} disabled={isChecking} variant="secondary">
|
||||
{isChecking ? 'Checking...' : 'Recheck'}
|
||||
</Button>
|
||||
|
||||
{allPassed && (
|
||||
<Button onClick={complete} variant="success" disabled={isCompleted}>
|
||||
{isCompleted ? 'Completed ✓' : 'Continue to Next Step →'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
293
packages/tutorial/src/pages/SwissBankAdvanced.tsx
Normal file
293
packages/tutorial/src/pages/SwissBankAdvanced.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { CodeEditor } from '../components/shared/CodeEditor';
|
||||
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
import { usePluginExecution } from '../hooks/usePluginExecution';
|
||||
import {
|
||||
step5Challenge1Validators,
|
||||
step5Challenge2Validators,
|
||||
step5Challenge3Validators,
|
||||
} from '../utils/validation';
|
||||
|
||||
export const SwissBankAdvanced: React.FC = () => {
|
||||
const {
|
||||
complete,
|
||||
updateCode,
|
||||
userCode,
|
||||
isCompleted,
|
||||
completedChallenges,
|
||||
markChallengeComplete,
|
||||
} = useStepProgress(5);
|
||||
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
|
||||
const [code, setCode] = useState(userCode);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [challengeResults, setChallengeResults] = useState<{
|
||||
1: boolean;
|
||||
2: boolean;
|
||||
3: boolean;
|
||||
}>({ 1: false, 2: false, 3: false });
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userCode) {
|
||||
fetch('/plugins/swissbank-starter.js')
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
})
|
||||
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCodeChange = (newCode: string) => {
|
||||
setCode(newCode);
|
||||
updateCode(newCode);
|
||||
};
|
||||
|
||||
const handleTestCode = async () => {
|
||||
const pluginResult = await execute(code);
|
||||
|
||||
// Validate all 3 challenges
|
||||
const challenge1Valid = step5Challenge1Validators.every(
|
||||
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
|
||||
);
|
||||
const challenge2Valid = step5Challenge2Validators.every(
|
||||
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
|
||||
);
|
||||
const challenge3Valid = step5Challenge3Validators.every(
|
||||
(validator) => validator.check({ code, pluginOutput: pluginResult }).valid
|
||||
);
|
||||
|
||||
setChallengeResults({
|
||||
1: challenge1Valid,
|
||||
2: challenge2Valid,
|
||||
3: challenge3Valid,
|
||||
});
|
||||
|
||||
// Mark completed challenges
|
||||
if (challenge1Valid && !completedChallenges.includes(1)) {
|
||||
markChallengeComplete(1);
|
||||
}
|
||||
if (challenge2Valid && !completedChallenges.includes(2)) {
|
||||
markChallengeComplete(2);
|
||||
}
|
||||
if (challenge3Valid && !completedChallenges.includes(3)) {
|
||||
markChallengeComplete(3);
|
||||
}
|
||||
|
||||
// Complete step if all challenges pass
|
||||
if (challenge1Valid && challenge2Valid && challenge3Valid) {
|
||||
complete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
const response = await fetch('/plugins/swissbank-starter.js');
|
||||
const text = await response.text();
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
setChallengeResults({ 1: false, 2: false, 3: false });
|
||||
resetExecution();
|
||||
} catch (err) {
|
||||
console.error('Failed to reload Swiss Bank starter:', err);
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const allChallengesComplete = completedChallenges.length === 3;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
|
||||
<h1 className="text-3xl font-bold mb-6 gradient-text">
|
||||
Step 5: Swiss Bank - Advanced Challenges
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
Complete all three challenges by adding the necessary handlers to your code. Test your
|
||||
code to see which challenges you've completed.
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<h3 className="font-bold text-blue-900 mb-3">Challenges:</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Challenge 1 */}
|
||||
<div
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
challengeResults[1] || completedChallenges.includes(1)
|
||||
? 'bg-green-50 border-green-500'
|
||||
: 'bg-white border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-bold text-gray-900">
|
||||
Challenge 1: Reveal USD Balance (Nested JSON)
|
||||
</h4>
|
||||
{(challengeResults[1] || completedChallenges.includes(1)) && (
|
||||
<span className="text-2xl">✅</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
Add a handler to reveal the USD balance from the nested <code>accounts.USD</code>{' '}
|
||||
field.
|
||||
</p>
|
||||
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
|
||||
<code>
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL',
|
||||
params: { type: 'json', path: 'accounts.USD' }
|
||||
}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Challenge 2 */}
|
||||
<div
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
challengeResults[2] || completedChallenges.includes(2)
|
||||
? 'bg-green-50 border-green-500'
|
||||
: 'bg-white border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-bold text-gray-900">
|
||||
Challenge 2: Reveal Cookie Header (SENT)
|
||||
</h4>
|
||||
{(challengeResults[2] || completedChallenges.includes(2)) && (
|
||||
<span className="text-2xl">✅</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
Add a SENT handler to reveal the Cookie header from the request.
|
||||
</p>
|
||||
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
|
||||
<code>
|
||||
{ type: 'SENT', part: 'HEADERS', action:
|
||||
'REVEAL', params: { key: 'cookie' } }
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Challenge 3 */}
|
||||
<div
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
challengeResults[3] || completedChallenges.includes(3)
|
||||
? 'bg-green-50 border-green-500'
|
||||
: 'bg-white border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-bold text-gray-900">Challenge 3: Reveal Date Header (RECV)</h4>
|
||||
{(challengeResults[3] || completedChallenges.includes(3)) && (
|
||||
<span className="text-2xl">✅</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
Add a RECV handler to reveal the Date header from the response.
|
||||
</p>
|
||||
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
|
||||
<code>
|
||||
{ type: 'RECV', part: 'HEADERS', action:
|
||||
'REVEAL', params: { key: 'date' } }
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
|
||||
<h3 className="font-bold text-purple-900 mb-3">💡 Documentation & Tips:</h3>
|
||||
<div className="space-y-3">
|
||||
{/* Inspection Tip */}
|
||||
<div className="bg-yellow-50 border border-yellow-300 rounded-lg p-3">
|
||||
<p className="text-xs font-semibold mb-1">💡 Pro Tip: Inspect First!</p>
|
||||
<p className="text-xs mb-2">
|
||||
Before targeting specific fields or headers, reveal everything to see what's
|
||||
available:
|
||||
</p>
|
||||
<div className="bg-white p-2 rounded space-y-1">
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL'
|
||||
} // See all response body
|
||||
</p>
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'SENT', part: 'HEADERS', action:
|
||||
'REVEAL' } // See all request headers
|
||||
</p>
|
||||
<p className="text-xs font-mono">
|
||||
{ type: 'RECV', part: 'HEADERS', action:
|
||||
'REVEAL' } // See all response headers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nested JSON Documentation */}
|
||||
<div className="bg-white border border-gray-300 rounded-lg p-3">
|
||||
<p className="text-xs font-semibold mb-2">📚 Nested JSON Path Syntax:</p>
|
||||
<p className="text-xs text-gray-700 mb-2">
|
||||
Use dot notation to access nested fields in JSON objects:
|
||||
</p>
|
||||
<div className="bg-gray-50 p-2 rounded">
|
||||
<p className="text-xs font-mono">
|
||||
params: { type: 'json', path: 'parent.child' }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Key Documentation */}
|
||||
<div className="bg-white border border-gray-300 rounded-lg p-3">
|
||||
<p className="text-xs font-semibold mb-2">📚 Targeting Specific Headers:</p>
|
||||
<p className="text-xs text-gray-700 mb-2">
|
||||
Use <code>params.key</code> to precisely target a header (case-insensitive):
|
||||
</p>
|
||||
<div className="bg-gray-50 p-2 rounded">
|
||||
<p className="text-xs font-mono">
|
||||
params: { key: 'header-name' }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
|
||||
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex gap-4 mb-4">
|
||||
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
|
||||
{isExecuting ? 'Testing...' : 'Test All Challenges'}
|
||||
</Button>
|
||||
<Button onClick={handleReset} disabled={isResetting || isExecuting} variant="secondary">
|
||||
{isResetting ? 'Resetting...' : 'Reset Code'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConsoleOutput result={result} />
|
||||
</div>
|
||||
|
||||
{allChallengesComplete && !isCompleted && (
|
||||
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center mb-6">
|
||||
<p className="text-xl font-bold text-green-900 mb-2">All Challenges Completed! ✓</p>
|
||||
<p className="text-gray-700 mb-4">
|
||||
You've successfully completed all advanced challenges!
|
||||
</p>
|
||||
<Button onClick={complete} variant="success">
|
||||
Complete Step 5 →
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCompleted && (
|
||||
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
|
||||
<p className="text-xl font-bold text-green-900">Step 5 Completed! ✓</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
167
packages/tutorial/src/pages/SwissBankBasic.tsx
Normal file
167
packages/tutorial/src/pages/SwissBankBasic.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { CodeEditor } from '../components/shared/CodeEditor';
|
||||
import { ConsoleOutput } from '../components/shared/ConsoleOutput';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
import { usePluginExecution } from '../hooks/usePluginExecution';
|
||||
import { useCodeValidation } from '../hooks/useCodeValidation';
|
||||
import { step4Validators } from '../utils/validation';
|
||||
|
||||
export const SwissBankBasic: React.FC = () => {
|
||||
const { complete, updateCode, userCode, isCompleted } = useStepProgress(4);
|
||||
const { execute, isExecuting, result, reset: resetExecution } = usePluginExecution();
|
||||
const {
|
||||
validate,
|
||||
validationResults,
|
||||
reset: resetValidation,
|
||||
} = useCodeValidation(step4Validators);
|
||||
const [code, setCode] = useState(userCode);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userCode) {
|
||||
fetch('/plugins/swissbank-starter.js')
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
})
|
||||
.catch((err) => console.error('Failed to load Swiss Bank starter:', err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCodeChange = (newCode: string) => {
|
||||
setCode(newCode);
|
||||
updateCode(newCode);
|
||||
};
|
||||
|
||||
const handleTestCode = async () => {
|
||||
// First validate code structure
|
||||
validate(code);
|
||||
|
||||
// Execute plugin
|
||||
const pluginResult = await execute(code);
|
||||
|
||||
// Validate with plugin output
|
||||
const allValid = validate(code, pluginResult);
|
||||
|
||||
// Complete step if all validations pass
|
||||
if (allValid) {
|
||||
complete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
setIsResetting(true);
|
||||
try {
|
||||
const response = await fetch('/plugins/swissbank-starter.js');
|
||||
const text = await response.text();
|
||||
setCode(text);
|
||||
updateCode(text);
|
||||
resetValidation();
|
||||
resetExecution();
|
||||
} catch (err) {
|
||||
console.error('Failed to reload Swiss Bank starter:', err);
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up mb-6">
|
||||
<h1 className="text-3xl font-bold mb-6 gradient-text">
|
||||
Step 4: Swiss Bank - Add Missing Handler
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
Now let's write our own plugin! Your task is to add a handler to reveal the Swiss Franc
|
||||
(CHF) balance.
|
||||
</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-3">Setup:</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
||||
<li>
|
||||
Visit{' '}
|
||||
<a
|
||||
href="https://swissbank.tlsnotary.org/login"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
https://swissbank.tlsnotary.org/login
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
Login with:
|
||||
<ul className="list-disc list-inside ml-6">
|
||||
<li>
|
||||
Username: <code className="bg-gray-200 px-2 py-1 rounded">tkstanczak</code>
|
||||
</li>
|
||||
<li>
|
||||
Password:{' '}
|
||||
<code className="bg-gray-200 px-2 py-1 rounded">
|
||||
TLSNotary is my favorite project
|
||||
</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Verify you can see the balances page</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<h3 className="font-bold text-blue-900 mb-2">Your Task:</h3>
|
||||
<p className="text-gray-700 mb-2">
|
||||
Find the TODO comment in the code and add this handler:
|
||||
</p>
|
||||
<pre className="bg-white p-3 rounded border border-blue-300 overflow-x-auto text-sm">
|
||||
{`{ type: 'RECV', part: 'ALL', action: 'REVEAL',
|
||||
params: { type: 'regex', regex: '"CHF"\\\\s*:\\\\s*"[^"]+"' } }`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
|
||||
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
{validationResults.length > 0 && (
|
||||
<div className="mb-4 space-y-2">
|
||||
{validationResults.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded ${
|
||||
result.valid ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{result.valid ? '✅' : '❌'} {result.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 mb-4">
|
||||
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
|
||||
{isExecuting ? 'Testing...' : 'Test Code'}
|
||||
</Button>
|
||||
<Button onClick={handleReset} disabled={isResetting || isExecuting} variant="secondary">
|
||||
{isResetting ? 'Resetting...' : 'Reset Code'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConsoleOutput result={result} />
|
||||
</div>
|
||||
|
||||
{isCompleted && (
|
||||
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
|
||||
<p className="text-xl font-bold text-green-900 mb-2">Challenge Completed! ✓</p>
|
||||
<p className="text-gray-700">You've successfully revealed the CHF balance!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
79
packages/tutorial/src/pages/TwitterExample.tsx
Normal file
79
packages/tutorial/src/pages/TwitterExample.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
71
packages/tutorial/src/pages/Welcome.tsx
Normal file
71
packages/tutorial/src/pages/Welcome.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../components/shared/Button';
|
||||
import { useStepProgress } from '../hooks/useStepProgress';
|
||||
|
||||
export const Welcome: React.FC = () => {
|
||||
const { complete } = useStepProgress(0);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 animate-slide-in-up">
|
||||
<h1 className="text-4xl font-bold mb-6 gradient-text">
|
||||
Welcome to the TLSNotary Browser Extension Plugin Tutorial
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
This interactive tutorial will guide you through creating and running TLSNotary plugins.
|
||||
You'll learn how to:
|
||||
</p>
|
||||
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-700 mb-8">
|
||||
<li>Set up the TLSNotary browser extension and a verifier server</li>
|
||||
<li>Understand the fundamentals of zkTLS and TLSNotary architecture</li>
|
||||
<li>Test your setup with the example Twitter plugin</li>
|
||||
<li>Create and test your own Swiss Bank plugin</li>
|
||||
<li>Challenge yourself to complete extra challenges</li>
|
||||
</ul>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<h3 className="text-xl font-bold text-blue-900 mb-3">How does TLSNotary work?</h3>
|
||||
<p className="text-gray-700 mb-4">In TLSNotary, there are three key components:</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-700">
|
||||
<li>
|
||||
<strong>Prover (Your Browser)</strong>: Makes requests to websites and generates
|
||||
cryptographic proofs
|
||||
</li>
|
||||
<li>
|
||||
<strong>Server (Twitter/Swiss Bank)</strong>: The website that serves the data you
|
||||
want to prove
|
||||
</li>
|
||||
<li>
|
||||
<strong>Verifier</strong>: Independently verifies that the data really came from the
|
||||
server
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-gray-700 mt-4">
|
||||
<strong>The key innovation:</strong> TLSNotary uses Multi-Party Computation (MPC-TLS)
|
||||
where the verifier participates in the TLS session alongside your browser. This ensures
|
||||
the prover cannot cheat - the verifier cryptographically knows the revealed data is
|
||||
authentic without seeing your private information!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">
|
||||
<h3 className="text-xl font-bold text-green-900 mb-3">What you'll build:</h3>
|
||||
<p className="text-gray-700">
|
||||
By the end of this tutorial, you'll understand how to create plugins that can prove data
|
||||
from any website, opening up possibilities for verified credentials, authenticated data
|
||||
sharing, and trustless applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={complete} variant="primary">
|
||||
Start Tutorial →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
122
packages/tutorial/src/styles/index.css
Normal file
122
packages/tutorial/src/styles/index.css
Normal file
@@ -0,0 +1,122 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--gradient-start: #667eea;
|
||||
--gradient-end: #764ba2;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Animation utilities */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.animate-slide-in-up {
|
||||
animation: slideInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Code editor container */
|
||||
.code-editor-container {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Console output styling */
|
||||
.console-output {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.console-output .timestamp {
|
||||
color: #858585;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.console-output .error {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.console-output .success {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.console-output .info {
|
||||
color: #569cd6;
|
||||
}
|
||||
103
packages/tutorial/src/types.ts
Normal file
103
packages/tutorial/src/types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// Global type declarations
|
||||
declare const __GIT_HASH__: string;
|
||||
|
||||
// Window extension for tlsn API
|
||||
declare global {
|
||||
interface Window {
|
||||
tlsn?: {
|
||||
execCode: (code: string) => Promise<string>;
|
||||
open: (url: string, options?: { width?: number; height?: number; showOverlay?: boolean }) => Promise<void>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tutorial state types
|
||||
export interface TutorialState {
|
||||
currentStep: number; // 0-7
|
||||
completedSteps: Set<number>; // Unlocked steps
|
||||
userCode: Record<number, string>; // step -> code mapping
|
||||
pluginResults: Record<number, PluginResult>; // step -> execution result
|
||||
attempts: Record<number, number>; // step -> attempt count
|
||||
completedChallenges: Record<number, number[]>; // step -> array of completed challenge IDs
|
||||
preferences: {
|
||||
showHints: boolean;
|
||||
editorTheme: 'light' | 'dark';
|
||||
};
|
||||
}
|
||||
|
||||
export interface TutorialActions {
|
||||
goToStep: (step: number) => void;
|
||||
completeStep: (step: number) => void;
|
||||
updateUserCode: (step: number, code: string) => void;
|
||||
savePluginResult: (step: number, result: PluginResult) => void;
|
||||
incrementAttempts: (step: number) => void;
|
||||
completeChallenge: (step: number, challengeId: number) => void;
|
||||
resetProgress: () => void;
|
||||
startOver: () => void;
|
||||
}
|
||||
|
||||
export interface TutorialContextType {
|
||||
state: TutorialState;
|
||||
actions: TutorialActions;
|
||||
}
|
||||
|
||||
// Plugin execution types
|
||||
export interface PluginResult {
|
||||
success: boolean;
|
||||
output?: string;
|
||||
error?: string;
|
||||
results?: Array<{ type: string; part?: string; value: string }>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// Validation types
|
||||
export interface ValidationRule {
|
||||
type: 'code' | 'result';
|
||||
check: (params: { code: string; pluginOutput?: PluginResult }) => ValidationResult;
|
||||
errorMessage: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Step configuration types
|
||||
export interface StepConfig {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
canSkip: boolean;
|
||||
validators?: ValidationRule[];
|
||||
}
|
||||
|
||||
// Quiz types
|
||||
export interface QuizQuestion {
|
||||
question: string;
|
||||
options: string[];
|
||||
correctAnswer: number;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
// Challenge types
|
||||
export interface Challenge {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
hints: string[];
|
||||
validators: ValidationRule[];
|
||||
}
|
||||
|
||||
// System check types
|
||||
export interface SystemCheck {
|
||||
name: string;
|
||||
status: 'checking' | 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CheckResult {
|
||||
extensionReady: boolean;
|
||||
verifierReady: boolean;
|
||||
browserCompatible: boolean;
|
||||
}
|
||||
69
packages/tutorial/src/utils/checks.ts
Normal file
69
packages/tutorial/src/utils/checks.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { CheckResult, SystemCheck } from '../types';
|
||||
|
||||
export const checkBrowserCompatibility = (): boolean => {
|
||||
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
const isEdge = /Edg/.test(navigator.userAgent);
|
||||
const isBrave = navigator.brave && typeof navigator.brave.isBrave === 'function';
|
||||
const isChromium = /Chromium/.test(navigator.userAgent);
|
||||
|
||||
return isChrome || isEdge || isBrave || isChromium;
|
||||
};
|
||||
|
||||
export const checkExtension = async (): Promise<boolean> => {
|
||||
// Wait a bit for extension to load
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return typeof window.tlsn !== 'undefined';
|
||||
};
|
||||
|
||||
export const checkVerifier = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:7047/health');
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
return text === 'ok';
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const performSystemChecks = async (): Promise<CheckResult> => {
|
||||
const [browserCompatible, extensionReady, verifierReady] = await Promise.all([
|
||||
Promise.resolve(checkBrowserCompatibility()),
|
||||
checkExtension(),
|
||||
checkVerifier(),
|
||||
]);
|
||||
|
||||
return {
|
||||
browserCompatible,
|
||||
extensionReady,
|
||||
verifierReady,
|
||||
};
|
||||
};
|
||||
|
||||
export const getSystemCheckStatus = (checkResult: CheckResult): SystemCheck[] => {
|
||||
return [
|
||||
{
|
||||
name: 'Browser Compatibility',
|
||||
status: checkResult.browserCompatible ? 'success' : 'error',
|
||||
message: checkResult.browserCompatible
|
||||
? 'Chrome-based browser detected'
|
||||
: 'Please use a Chrome-based browser (Chrome, Edge, Brave, etc.)',
|
||||
},
|
||||
{
|
||||
name: 'TLSNotary Extension',
|
||||
status: checkResult.extensionReady ? 'success' : 'error',
|
||||
message: checkResult.extensionReady
|
||||
? 'Extension installed and ready'
|
||||
: 'Extension not found. Please install and load the extension.',
|
||||
},
|
||||
{
|
||||
name: 'Verifier Server',
|
||||
status: checkResult.verifierReady ? 'success' : 'error',
|
||||
message: checkResult.verifierReady
|
||||
? 'Verifier server running on http://localhost:7047'
|
||||
: 'Verifier server not responding. Please start the verifier server.',
|
||||
},
|
||||
];
|
||||
};
|
||||
10
packages/tutorial/src/utils/config.ts
Normal file
10
packages/tutorial/src/utils/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const config = {
|
||||
verifierHost: import.meta.env.VITE_VERIFIER_HOST || 'localhost:7047',
|
||||
ssl: import.meta.env.VITE_SSL === 'true',
|
||||
get verifierUrl() {
|
||||
return `${this.ssl ? 'https' : 'http'}://${this.verifierHost}`;
|
||||
},
|
||||
get wsProtocol() {
|
||||
return this.ssl ? 'wss' : 'ws';
|
||||
},
|
||||
};
|
||||
71
packages/tutorial/src/utils/storage.ts
Normal file
71
packages/tutorial/src/utils/storage.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { TutorialState } from '../types';
|
||||
|
||||
const STORAGE_KEY = 'tlsn-tutorial-progress';
|
||||
const AUTO_SAVE_DELAY = 1000; // 1 second debounce
|
||||
|
||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export const getDefaultState = (): TutorialState => ({
|
||||
currentStep: 0,
|
||||
completedSteps: new Set<number>(),
|
||||
userCode: {},
|
||||
pluginResults: {},
|
||||
attempts: {},
|
||||
completedChallenges: {},
|
||||
preferences: {
|
||||
showHints: true,
|
||||
editorTheme: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
export const loadState = (): TutorialState => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (!saved) return getDefaultState();
|
||||
|
||||
const parsed = JSON.parse(saved);
|
||||
|
||||
// Convert completedSteps array back to Set
|
||||
// Add backward compatibility for completedChallenges
|
||||
return {
|
||||
...parsed,
|
||||
completedSteps: new Set(parsed.completedSteps || []),
|
||||
completedChallenges: parsed.completedChallenges || {},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load tutorial state:', error);
|
||||
return getDefaultState();
|
||||
}
|
||||
};
|
||||
|
||||
export const saveState = (state: TutorialState): void => {
|
||||
try {
|
||||
// Convert Set to array for JSON serialization
|
||||
const toSave = {
|
||||
...state,
|
||||
completedSteps: Array.from(state.completedSteps),
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
||||
} catch (error) {
|
||||
console.error('Failed to save tutorial state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveStateDebounced = (state: TutorialState): void => {
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
}
|
||||
|
||||
saveTimeout = setTimeout(() => {
|
||||
saveState(state);
|
||||
}, AUTO_SAVE_DELAY);
|
||||
};
|
||||
|
||||
export const clearState = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear tutorial state:', error);
|
||||
}
|
||||
};
|
||||
238
packages/tutorial/src/utils/validation.ts
Normal file
238
packages/tutorial/src/utils/validation.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { ValidationRule } from '../types';
|
||||
|
||||
// Step 4: Swiss Bank Basic - CHF Handler Validation
|
||||
export const step4Validators: ValidationRule[] = [
|
||||
{
|
||||
type: 'code',
|
||||
check: ({ code }) => ({
|
||||
valid: /type:\s*['"]RECV['"]/.test(code),
|
||||
message: /type:\s*['"]RECV['"]/.test(code)
|
||||
? 'Handler structure looks good'
|
||||
: 'Missing RECV handler',
|
||||
}),
|
||||
errorMessage: 'You need to add a handler with type: "RECV"',
|
||||
hint: 'Look for the TODO comment and add the handler object there',
|
||||
},
|
||||
{
|
||||
type: 'code',
|
||||
check: ({ code }) => ({
|
||||
valid: /regex:\s*['"].*CHF.*['"]/.test(code),
|
||||
message: /regex:\s*['"].*CHF.*['"]/.test(code)
|
||||
? 'Regex pattern found for CHF'
|
||||
: 'Missing regex pattern for CHF balance',
|
||||
}),
|
||||
errorMessage: 'Add a regex pattern to match the CHF balance',
|
||||
hint: 'Use the regex pattern: "CHF"\\s*:\\s*"[^"]+"',
|
||||
},
|
||||
{
|
||||
type: 'result',
|
||||
check: ({ pluginOutput }) => {
|
||||
if (!pluginOutput || !pluginOutput.success) {
|
||||
return { valid: false, message: 'Plugin execution failed' };
|
||||
}
|
||||
|
||||
// Find the CHF result in the results array
|
||||
const chfResult = pluginOutput.results?.find(
|
||||
(r) => r.type === 'RECV' && r.value && r.value.includes('CHF')
|
||||
);
|
||||
|
||||
if (!chfResult) {
|
||||
return { valid: false, message: 'CHF balance not found in result' };
|
||||
}
|
||||
|
||||
// Extract the CHF value from the result
|
||||
const match = chfResult.value.match(/"CHF"\s*:\s*"(\d+(_\d+)*)"/);
|
||||
|
||||
if (!match) {
|
||||
return { valid: false, message: 'CHF balance pattern not matched in result' };
|
||||
}
|
||||
|
||||
const balance = match[1].replace(/_/g, '');
|
||||
const isCorrect = balance === '50000000';
|
||||
|
||||
return {
|
||||
valid: isCorrect,
|
||||
message: isCorrect
|
||||
? `Verified CHF balance: ${match[1]}`
|
||||
: `Found CHF balance: ${match[1]}, but expected 50_000_000`,
|
||||
};
|
||||
},
|
||||
errorMessage: 'The proof should contain the verified CHF balance of 50_000_000',
|
||||
},
|
||||
];
|
||||
|
||||
// Step 5: Swiss Bank Advanced - Multiple Challenges
|
||||
export const step5Challenge1Validators: ValidationRule[] = [
|
||||
{
|
||||
type: 'code',
|
||||
check: ({ code }) => ({
|
||||
valid:
|
||||
/type:\s*['"]RECV['"]/.test(code) &&
|
||||
/part:\s*['"]BODY['"]/.test(code) &&
|
||||
/type:\s*['"]json['"]/.test(code) &&
|
||||
/path:\s*['"]accounts\.USD['"]/.test(code),
|
||||
message:
|
||||
/type:\s*['"]RECV['"]/.test(code) &&
|
||||
/part:\s*['"]BODY['"]/.test(code) &&
|
||||
/type:\s*['"]json['"]/.test(code) &&
|
||||
/path:\s*['"]accounts\.USD['"]/.test(code)
|
||||
? 'RECV BODY handler with nested JSON path found'
|
||||
: 'Add RECV BODY handler with nested JSON path for USD',
|
||||
}),
|
||||
errorMessage: 'Add a handler to reveal the USD balance from accounts.USD',
|
||||
hint: '{ type: "RECV", part: "BODY", action: "REVEAL", params: { type: "json", path: "accounts.USD" } }',
|
||||
},
|
||||
{
|
||||
type: 'result',
|
||||
check: ({ pluginOutput }) => {
|
||||
if (!pluginOutput || !pluginOutput.success) {
|
||||
return { valid: false, message: 'Plugin execution failed' };
|
||||
}
|
||||
|
||||
// Find USD result in the results array
|
||||
const usdResult = pluginOutput.results?.find(
|
||||
(r) => r.type === 'RECV' && r.part === 'BODY' && r.value && r.value.includes('USD')
|
||||
);
|
||||
|
||||
if (!usdResult) {
|
||||
return { valid: false, message: 'USD balance not found in proof' };
|
||||
}
|
||||
|
||||
// Check that it contains a USD value
|
||||
const hasUSDValue = /USD.*\d+/.test(usdResult.value) || /"USD"/.test(usdResult.value);
|
||||
|
||||
if (hasUSDValue) {
|
||||
return { valid: true, message: 'Successfully revealed USD balance from nested path' };
|
||||
}
|
||||
|
||||
return { valid: false, message: 'USD balance format not recognized' };
|
||||
},
|
||||
errorMessage: 'The proof should contain the USD balance from accounts.USD',
|
||||
},
|
||||
];
|
||||
|
||||
export const step5Challenge2Validators: ValidationRule[] = [
|
||||
{
|
||||
type: 'code',
|
||||
check: ({ code }) => ({
|
||||
valid: /type:\s*['"]SENT['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code),
|
||||
message:
|
||||
/type:\s*['"]SENT['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code)
|
||||
? 'SENT HEADERS handler found'
|
||||
: 'Add SENT handler for HEADERS',
|
||||
}),
|
||||
errorMessage: 'Add a handler to reveal the Cookie header from the request',
|
||||
hint: '{ type: "SENT", part: "HEADERS", action: "REVEAL", params: { key: "cookie" } }',
|
||||
},
|
||||
{
|
||||
type: 'result',
|
||||
check: ({ pluginOutput }) => {
|
||||
if (!pluginOutput || !pluginOutput.success) {
|
||||
return { valid: false, message: 'Plugin execution failed' };
|
||||
}
|
||||
|
||||
// Find SENT HEADERS result with Cookie
|
||||
const sentHeaderResult = pluginOutput.results?.find(
|
||||
(r) => r.type === 'SENT' && r.part === 'HEADERS' && r.value && /cookie/i.test(r.value)
|
||||
);
|
||||
|
||||
if (!sentHeaderResult) {
|
||||
return { valid: false, message: 'Cookie header not found in proof' };
|
||||
}
|
||||
|
||||
return { valid: true, message: 'Cookie header successfully revealed' };
|
||||
},
|
||||
errorMessage: 'The proof should contain the Cookie header from the request',
|
||||
},
|
||||
];
|
||||
|
||||
export const step5Challenge3Validators: ValidationRule[] = [
|
||||
{
|
||||
type: 'code',
|
||||
check: ({ code }) => ({
|
||||
valid: /type:\s*['"]RECV['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code),
|
||||
message:
|
||||
/type:\s*['"]RECV['"]/.test(code) && /part:\s*['"]HEADERS['"]/.test(code)
|
||||
? 'RECV HEADERS handler found'
|
||||
: 'Add RECV handler for HEADERS',
|
||||
}),
|
||||
errorMessage: 'Add a handler to reveal the Date header from the response',
|
||||
hint: '{ type: "RECV", part: "HEADERS", action: "REVEAL", params: { key: "date" } }',
|
||||
},
|
||||
{
|
||||
type: 'result',
|
||||
check: ({ pluginOutput }) => {
|
||||
if (!pluginOutput || !pluginOutput.success) {
|
||||
return { valid: false, message: 'Plugin execution failed' };
|
||||
}
|
||||
|
||||
// Find RECV HEADERS result with Date
|
||||
const recvHeaderResult = pluginOutput.results?.find(
|
||||
(r) => r.type === 'RECV' && r.part === 'HEADERS' && r.value && /date/i.test(r.value)
|
||||
);
|
||||
|
||||
if (!recvHeaderResult) {
|
||||
return { valid: false, message: 'Date header not found in proof' };
|
||||
}
|
||||
|
||||
return { valid: true, message: 'Date header successfully revealed' };
|
||||
},
|
||||
errorMessage: 'The proof should contain the Date header from the response',
|
||||
},
|
||||
];
|
||||
|
||||
// Step 6: Challenge - Break the Verifier
|
||||
export const step6Validators: ValidationRule[] = [
|
||||
{
|
||||
type: 'result',
|
||||
check: ({ pluginOutput }) => {
|
||||
if (!pluginOutput || !pluginOutput.success) {
|
||||
return { valid: false, message: 'Plugin execution failed' };
|
||||
}
|
||||
|
||||
// Concatenate all revealed values to show the redacted transcript
|
||||
const revealedValues = pluginOutput.results
|
||||
?.map((r) => r.value || '')
|
||||
.filter((v) => v.length > 0)
|
||||
.join('');
|
||||
|
||||
if (!revealedValues) {
|
||||
return { valid: false, message: 'No revealed data found in proof' };
|
||||
}
|
||||
|
||||
// Check if the redacted transcript contains inflated CHF amounts
|
||||
const hasInflatedAmount =
|
||||
/"CHF"\s*:\s*"275_000_000"/.test(revealedValues) ||
|
||||
/"CHF"\s*:\s*"125_000_000"/.test(revealedValues);
|
||||
|
||||
if (hasInflatedAmount) {
|
||||
return {
|
||||
valid: true,
|
||||
message: `✅ Successfully fooled the verifier! Redacted transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it contains the original amount
|
||||
if (/"CHF"\s*:\s*"50_000_000"/.test(revealedValues)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `❌ Redacted transcript shows correct amount. Try revealing multiple CHF values! Transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `❌ No CHF balance found. Redacted transcript: ${revealedValues.substring(0, 200)}${revealedValues.length > 200 ? '...' : ''}`,
|
||||
};
|
||||
},
|
||||
errorMessage: 'Make the verifier believe you have more than 50_000_000 CHF',
|
||||
hint: 'The verifier only sees what you reveal. Try revealing the CHF balance multiple times with different amounts.',
|
||||
},
|
||||
];
|
||||
|
||||
// Step 2: Concepts Quiz Answers
|
||||
export const quizAnswers = [
|
||||
1, // Question 1: What is the verifier's role? -> Cryptographically verify without seeing private data
|
||||
0, // Question 2: PEDERSEN vs REVEAL -> Hashes data for commitment
|
||||
1, // Question 3: RECV meaning -> Data received from the server
|
||||
];
|
||||
16
packages/tutorial/tailwind.config.js
Normal file
16
packages/tutorial/tailwind.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#243f5f',
|
||||
gradient: {
|
||||
start: '#667eea',
|
||||
end: '#764ba2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
22
packages/tutorial/tsconfig.json
Normal file
22
packages/tutorial/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
packages/tutorial/tsconfig.node.json
Normal file
10
packages/tutorial/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "build-plugins.js"]
|
||||
}
|
||||
34
packages/tutorial/vite.config.ts
Normal file
34
packages/tutorial/vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// Get git hash for footer display
|
||||
const getGitHash = () => {
|
||||
try {
|
||||
return execSync('git rev-parse --short HEAD').toString().trim();
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
define: {
|
||||
__GIT_HASH__: JSON.stringify(getGitHash()),
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['react', 'react-dom'],
|
||||
codemirror: ['codemirror', '@codemirror/lang-javascript', '@codemirror/state', '@codemirror/view'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
522
packages/verifier/Cargo.lock
generated
522
packages/verifier/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# TLSNotary dependency
|
||||
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.13" }
|
||||
tlsn = { git = "https://github.com/tlsnotary/tlsn.git", tag = "v0.1.0-alpha.14", features = ["mozilla-certs"] }
|
||||
|
||||
# HTTP server framework
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
@@ -46,7 +46,7 @@ eyre = "0.6"
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
regex = "1.12.2"
|
||||
rangeset = "0.2.0"
|
||||
rangeset = "0.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||
|
||||
@@ -13,7 +13,7 @@ A Rust-based HTTP server with WebSocket support for TLSNotary verification opera
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **tlsn**: v0.1.0-alpha.13 from GitHub - TLSNotary verification library
|
||||
- **tlsn**: v0.1.0-alpha.14 from GitHub - TLSNotary verification library
|
||||
- **axum**: Modern web framework with WebSocket support
|
||||
- **tokio**: Async runtime with full features
|
||||
- **tokio-util**: Async utilities for stream compatibility
|
||||
|
||||
@@ -12,7 +12,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_websocket::{WebSocket, WebSocketUpgrade};
|
||||
use rangeset::RangeSet;
|
||||
use rangeset::prelude::RangeSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use eyre::eyre;
|
||||
use tlsn::{
|
||||
config::ProtocolConfigValidator,
|
||||
config::{tls_commit::TlsCommitProtocolConfig, verifier::VerifierConfig},
|
||||
connection::{DnsName, ServerName},
|
||||
transcript::PartialTranscript,
|
||||
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
|
||||
verifier::VerifierOutput,
|
||||
webpki::RootCertStore,
|
||||
Session,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
@@ -23,31 +25,103 @@ pub async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
|
||||
max_sent_data, max_recv_data
|
||||
);
|
||||
|
||||
let config_validator = ProtocolConfigValidator::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap();
|
||||
// Create a session with the prover
|
||||
let session = Session::new(socket.compat());
|
||||
let (driver, mut handle) = session.split();
|
||||
|
||||
// Spawn the session driver to run in the background
|
||||
let driver_task = tokio::spawn(driver);
|
||||
|
||||
// Create verifier config with Mozilla root certificates for TLS verification
|
||||
let verifier_config = VerifierConfig::builder()
|
||||
.protocol_config_validator(config_validator)
|
||||
.root_store(RootCertStore::mozilla())
|
||||
.build()
|
||||
.unwrap();
|
||||
.map_err(|e| eyre!("Failed to build verifier config: {}", e))?;
|
||||
|
||||
info!("verifier_config: {:?}", verifier_config);
|
||||
let verifier = Verifier::new(verifier_config);
|
||||
let verifier = handle
|
||||
.new_verifier(verifier_config)
|
||||
.map_err(|e| eyre!("Failed to create verifier: {}", e))?;
|
||||
|
||||
info!("Starting verification");
|
||||
info!("Starting TLS commitment protocol");
|
||||
|
||||
let VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
} = verifier
|
||||
.verify(socket.compat(), &VerifyConfig::default())
|
||||
// Run the commitment protocol
|
||||
let verifier = verifier
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|e| eyre!("Commitment failed: {}", e))?;
|
||||
|
||||
// Check the proposed configuration
|
||||
let request = verifier.request();
|
||||
let TlsCommitProtocolConfig::Mpc(mpc_config) = request.protocol() else {
|
||||
return Err(eyre!("Only MPC protocol is supported"));
|
||||
};
|
||||
|
||||
// Validate the proposed configuration
|
||||
if mpc_config.max_sent_data() > max_sent_data {
|
||||
return Err(eyre!(
|
||||
"Prover requested max_sent_data {} exceeds limit {}",
|
||||
mpc_config.max_sent_data(),
|
||||
max_sent_data
|
||||
));
|
||||
}
|
||||
if mpc_config.max_recv_data() > max_recv_data {
|
||||
return Err(eyre!(
|
||||
"Prover requested max_recv_data {} exceeds limit {}",
|
||||
mpc_config.max_recv_data(),
|
||||
max_recv_data
|
||||
));
|
||||
}
|
||||
|
||||
info!(
|
||||
"Accepting TLS commitment with max_sent={}, max_recv={}",
|
||||
mpc_config.max_sent_data(),
|
||||
mpc_config.max_recv_data()
|
||||
);
|
||||
|
||||
// Accept and run the commitment protocol
|
||||
let verifier = verifier
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| eyre!("Accept failed: {}", e))?
|
||||
.run()
|
||||
.await
|
||||
.map_err(|e| eyre!("Run failed: {}", e))?;
|
||||
|
||||
info!("TLS connection complete, starting verification");
|
||||
|
||||
// Verify the proof
|
||||
let verifier = verifier
|
||||
.verify()
|
||||
.await
|
||||
.map_err(|e| eyre!("Verification failed: {}", e))?;
|
||||
|
||||
let (
|
||||
VerifierOutput {
|
||||
server_name,
|
||||
transcript,
|
||||
..
|
||||
},
|
||||
verifier,
|
||||
) = verifier
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| eyre!("Accept verification failed: {}", e))?;
|
||||
|
||||
// Close the verifier
|
||||
verifier
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| eyre!("Failed to close verifier: {}", e))?;
|
||||
|
||||
// Close the session handle
|
||||
handle.close();
|
||||
|
||||
// Wait for the driver to complete
|
||||
driver_task
|
||||
.await
|
||||
.map_err(|e| eyre!("Driver task failed: {}", e))?
|
||||
.map_err(|e| eyre!("Session driver error: {}", e))?;
|
||||
|
||||
info!("verify() returned successfully - prover sent all data");
|
||||
|
||||
let server_name =
|
||||
|
||||
Reference in New Issue
Block a user