mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-23 05:58:05 -05:00
Compare commits
7 Commits
new-tutori
...
git_fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de9faea4c9 | ||
|
|
877527aeca | ||
|
|
52cc68937b | ||
|
|
a04b3c671a | ||
|
|
2b72884192 | ||
|
|
9b22e2af37 | ||
|
|
bbe6e23d5f |
20
.github/workflows/demo.yml
vendored
20
.github/workflows/demo.yml
vendored
@@ -20,9 +20,26 @@ env:
|
||||
should_publish: ${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.')) || github.ref == 'refs/heads/staging' }}
|
||||
|
||||
jobs:
|
||||
test_verifier:
|
||||
name: test verifier server
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Run verifier tests
|
||||
working-directory: packages/verifier
|
||||
run: cargo test
|
||||
|
||||
build_and_publish_demo_verifier_server:
|
||||
name: build and publish demo verifier server image
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_verifier
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -52,6 +69,8 @@ jobs:
|
||||
push: ${{ env.should_publish == 'true' }}
|
||||
tags: ${{ steps.meta-prover-server.outputs.tags }}
|
||||
labels: ${{ steps.meta-prover-server.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_HASH=${{ github.sha }}
|
||||
|
||||
build_and_publish_demo_frontend:
|
||||
name: build and publish demo frontend image
|
||||
@@ -88,3 +107,4 @@ jobs:
|
||||
build-args: |
|
||||
VITE_VERIFIER_HOST=demo-staging.tlsnotary.org
|
||||
VITE_SSL=true
|
||||
GIT_HASH=${{ github.sha }}
|
||||
|
||||
@@ -591,7 +591,8 @@ logger.setLevel(LogLevel.WARN);
|
||||
Docker-based demo environment for testing plugins:
|
||||
|
||||
**Files:**
|
||||
- `twitter.js`, `swissbank.js` - Example plugin files
|
||||
- `src/plugins/*.plugin.ts` - Plugin source files (TypeScript)
|
||||
- `public/plugins/*.js` - Built plugin files (generated by `build-plugins.js`)
|
||||
- `docker-compose.yml` - Docker services configuration
|
||||
- `nginx.conf` - Reverse proxy configuration
|
||||
|
||||
|
||||
819
package-lock.json
generated
819
package-lock.json
generated
@@ -4864,10 +4864,6 @@
|
||||
"resolved": "packages/plugin-sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tlsn/tutorial": {
|
||||
"resolved": "packages/tutorial",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tlsnotary/demo": {
|
||||
"resolved": "packages/demo",
|
||||
"link": true
|
||||
@@ -15714,19 +15710,6 @@
|
||||
"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"
|
||||
@@ -17558,808 +17541,6 @@
|
||||
"name": "tlsn-wasm",
|
||||
"version": "0.1.0-alpha.14",
|
||||
"license": "MIT OR Apache-2.0"
|
||||
},
|
||||
"packages/tutorial": {
|
||||
"name": "@tlsn/tutorial",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.1.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/type-utils": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
|
||||
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
|
||||
"integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
|
||||
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
|
||||
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
|
||||
"integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
|
||||
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"packages/tutorial/node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
|
||||
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.14 --no-logging",
|
||||
"demo": "npm run dev --workspace=@tlsnotary/demo",
|
||||
"tutorial": "npm run dev --workspace=@tlsn/tutorial",
|
||||
"tutorial": "serve -l 8080 packages/tutorial",
|
||||
"docker:up": "cd packages/demo && docker compose up --build -d",
|
||||
"docker:down": "cd packages/demo && docker compose down"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ FROM node:20-alpine AS builder
|
||||
# Accept build arguments with defaults
|
||||
ARG VITE_VERIFIER_HOST=localhost:7047
|
||||
ARG VITE_SSL=false
|
||||
ARG GIT_HASH=local
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -17,6 +18,7 @@ COPY . .
|
||||
# Build with environment variables
|
||||
ENV VITE_VERIFIER_HOST=${VITE_VERIFIER_HOST}
|
||||
ENV VITE_SSL=${VITE_SSL}
|
||||
ENV GIT_HASH=${GIT_HASH}
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const plugins = ['twitter', 'swissbank', 'spotify'];
|
||||
const plugins = ['twitter', 'swissbank', 'spotify', 'duolingo'];
|
||||
|
||||
// Build URLs from environment variables (matching config.ts pattern)
|
||||
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';
|
||||
|
||||
@@ -9,6 +9,7 @@ services:
|
||||
- "7047:7047"
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- GIT_HASH=${GIT_HASH:-dev}
|
||||
restart: unless-stopped
|
||||
|
||||
demo-static:
|
||||
@@ -17,6 +18,7 @@ services:
|
||||
args:
|
||||
VITE_VERIFIER_HOST: ${VITE_VERIFIER_HOST:-localhost:7047}
|
||||
VITE_SSL: ${VITE_SSL:-false}
|
||||
GIT_HASH: ${GIT_HASH:-dev}
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
|
||||
@@ -290,7 +290,7 @@ export function App() {
|
||||
>
|
||||
View source on GitHub
|
||||
</a>
|
||||
<span className="footer-version">v{__GIT_COMMIT_HASH__}</span>
|
||||
<span className="footer-version">{__GIT_COMMIT_HASH__}</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export function StatusBar({
|
||||
{!extensionOk && (
|
||||
<div>
|
||||
TLSNotary extension not detected.{' '}
|
||||
<a href="chrome://extensions/" target="_blank" rel="noopener noreferrer">
|
||||
<a href="https://chromewebstore.google.com/detail/tlsnotary/gnoglgpcamodhflknhmafmjdahcejcgg?authuser=2&hl=en" target="_blank" rel="noopener noreferrer">
|
||||
Install extension
|
||||
</a>
|
||||
{' '}then <strong>refresh this page</strong>.
|
||||
|
||||
@@ -28,4 +28,13 @@ export const plugins: Record<string, Plugin> = {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
duolingo: {
|
||||
name: 'Duolingo',
|
||||
description: 'Prove your Duolingo language learning progress and achievements',
|
||||
logo: '🦉',
|
||||
file: '/plugins/duolingo.js',
|
||||
parseResult: (json) => {
|
||||
return json.results[json.results.length - 1].value;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
/// <reference types="@tlsn/plugin-sdk/src/globals" />
|
||||
|
||||
// @ts-ignore - These will be replaced at build time by Vite's define option
|
||||
const VERIFIER_URL = VITE_VERIFIER_URL;
|
||||
// @ts-ignore
|
||||
const PROXY_URL_BASE = VITE_PROXY_URL;
|
||||
|
||||
const api = 'www.duolingo.com';
|
||||
const ui = 'https://www.duolingo.com/';
|
||||
|
||||
const config = {
|
||||
name: 'Spotify Top Artist',
|
||||
description: 'This plugin will prove your top artist on Spotify.',
|
||||
name: 'Duolingo Plugin',
|
||||
description: 'This plugin will prove your email and current streak on Duolingo.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'www.duolingo.com',
|
||||
pathname: '/2023-05-23/users/*',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://www.duolingo.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
const api = 'api.spotify.com';
|
||||
const ui = 'https://developer.spotify.com/';
|
||||
const top_artist_path = '/v1/me/top/artists?time_range=medium_term&limit=1';
|
||||
function getRelevantHeaderValues() {
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}/2023-05-23/users`));
|
||||
});
|
||||
|
||||
const authorization = header?.requestHeaders.find(header => header.name === 'Authorization')?.value;
|
||||
|
||||
const traceId = header?.requestHeaders.find(header => header.name === 'X-Amzn-Trace-Id')?.value;
|
||||
const user_id = traceId?.split('=')[1];
|
||||
|
||||
return { authorization, user_id };
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
@@ -15,41 +44,29 @@ async function onClick() {
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}`));
|
||||
});
|
||||
|
||||
// console.log('Intercepted Spotify API request header:', header);
|
||||
const { authorization, user_id } = getRelevantHeaderValues();
|
||||
|
||||
const headers = {
|
||||
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
|
||||
authorization: authorization,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
url: `https://${api}${top_artist_path}`, // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
url: `https://${api}/2023-05-23/users/${user_id}?fields=longestStreak,username`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
},
|
||||
{
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.spotify.com',
|
||||
verifierUrl: VERIFIER_URL,
|
||||
proxyUrl: PROXY_URL_BASE + api,
|
||||
maxRecvData: 2400,
|
||||
maxSentData: 600,
|
||||
maxSentData: 1200,
|
||||
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: 'items[0].name', },
|
||||
// type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'items[0].external_urls.spotify', },
|
||||
},
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'longestStreak', }, },
|
||||
]
|
||||
}
|
||||
);
|
||||
@@ -65,8 +82,8 @@ function minimizeUI() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
|
||||
// const [header] = useHeaders(headers => { return headers.filter(headers => headers.url.includes('https://api.spotify.com')) });
|
||||
const { authorization, user_id } = getRelevantHeaderValues();
|
||||
const header_has_necessary_values = authorization && user_id;
|
||||
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
@@ -84,7 +101,7 @@ function main() {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#1DB954',
|
||||
backgroundColor: '#58CC02',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
@@ -96,7 +113,7 @@ function main() {
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🎵']);
|
||||
}, ['🦉']);
|
||||
}
|
||||
|
||||
return div({
|
||||
@@ -116,7 +133,7 @@ function main() {
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -129,7 +146,7 @@ function main() {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['Spotify Top Artist']),
|
||||
}, ['Duolingo Streak']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
@@ -159,38 +176,34 @@ function main() {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: header_has_necessary_values ? '#d4edda' : '#f8d7da',
|
||||
color: header_has_necessary_values ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header_has_necessary_values ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
header_has_necessary_values ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
header_has_necessary_values ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #1DB954 0%, #1AA34A 100%)',
|
||||
background: 'linear-gradient(135deg, #58CC02 0%, #4CAF00 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
@@ -200,7 +213,7 @@ function main() {
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to Spotify to continue'])
|
||||
}, ['Please login to Duolingo to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -33,12 +33,16 @@ async function onClick() {
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes(`https://${api}`));
|
||||
});
|
||||
// Use cached authorization token from state
|
||||
const authToken = useState('authToken', null);
|
||||
|
||||
if (!authToken) {
|
||||
setState('isRequestPending', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
authorization: header.requestHeaders.find(header => header.name === 'Authorization')?.value,
|
||||
authorization: authToken,
|
||||
Host: api,
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'close',
|
||||
@@ -79,10 +83,23 @@ function minimizeUI() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [header] = useHeaders(headers => headers.filter(h => h.url.includes(`https://${api}`)));
|
||||
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
const authToken = useState('authToken', null);
|
||||
|
||||
|
||||
// Only search for auth token if not already cached
|
||||
if (!authToken) {
|
||||
const token = useHeaders(h => h.filter(x => x.url.startsWith(`https://${api}`)))
|
||||
.flatMap(h => h.requestHeaders)
|
||||
.find((h: { name: string; value?: string }) => h.name === 'Authorization')
|
||||
?.value;
|
||||
|
||||
if (token) {
|
||||
setState('authToken', token);
|
||||
console.log('Auth Token found:', token);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
openWindow(ui);
|
||||
@@ -172,16 +189,16 @@ function main() {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
backgroundColor: authToken ? '#d4edda' : '#f8d7da',
|
||||
color: authToken ? '#155724' : '#721c24',
|
||||
border: `1px solid ${authToken ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
authToken ? '✓ Api token detected' : '⚠ No API token detected'
|
||||
]),
|
||||
|
||||
header ? (
|
||||
authToken ? (
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
@@ -194,7 +211,7 @@ function main() {
|
||||
fontSize: '15px',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
opacity: isRequestPending ? '0.5' : '1',
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
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: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
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 => {
|
||||
console.log('Intercepted headers:', 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,
|
||||
},
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=swissbank.tlsnotary.org',
|
||||
// proxyUrl: 'ws://localhost:55688',
|
||||
maxRecvData: 460, // Maximum bytes to receive from server (response size limit)
|
||||
maxSentData: 180,// Maximum bytes to send to server (request size limit)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
handlers: [
|
||||
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL', },
|
||||
{ type: 'RECV', part: 'START_LINE', action: 'REVEAL', },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'account_id' }, },
|
||||
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'accounts.CHF' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:\s*"[^"]+"' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"CHF"\s*:' }, },
|
||||
// { type: 'RECV', part: 'ALL', action: 'REVEAL', params: { type: 'regex', regex: '"275_000_000"' }, },
|
||||
]
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
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);
|
||||
|
||||
// Run once on plugin load
|
||||
useEffect(() => {
|
||||
openWindow(`https://${host}${ui_path}`);
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#4CAF50',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
right: '8px',
|
||||
width: '280px',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
zIndex: '999999',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
}
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['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',
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether cookie is detected
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
hasNecessaryHeader ? (
|
||||
// Show prove button when not pending
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
@@ -1,361 +0,0 @@
|
||||
// =============================================================================
|
||||
// PLUGIN CONFIGURATION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The config object defines plugin metadata displayed to users.
|
||||
* This information appears in the plugin selection UI.
|
||||
*/
|
||||
const config = {
|
||||
name: 'X Profile Prover',
|
||||
description: 'This plugin will prove your X.com profile.',
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
host: 'api.x.com',
|
||||
pathname: '/1.1/account/settings.json',
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
},
|
||||
],
|
||||
urls: [
|
||||
'https://x.com/*',
|
||||
],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// PROOF GENERATION CALLBACK
|
||||
// =============================================================================
|
||||
/**
|
||||
* This function is triggered when the user clicks the "Prove" button.
|
||||
* It extracts authentication headers from intercepted requests and generates
|
||||
* a TLSNotary proof using the unified prove() API.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Get the intercepted X.com API request headers
|
||||
* 2. Extract authentication headers (Cookie, CSRF token, OAuth token, etc.)
|
||||
* 3. Call prove() with the request configuration and reveal handlers
|
||||
* 4. prove() internally:
|
||||
* - Creates a prover connection to the verifier
|
||||
* - Sends the HTTP request through the TLS prover
|
||||
* - Captures the TLS transcript (sent/received bytes)
|
||||
* - Parses the transcript with byte-level range tracking
|
||||
* - Applies selective reveal handlers to show only specified data
|
||||
* - Generates and returns the cryptographic proof
|
||||
* 5. Return the proof result to the caller via done()
|
||||
*/
|
||||
async function onClick() {
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
if (isRequestPending) return;
|
||||
|
||||
setState('isRequestPending', true);
|
||||
|
||||
// Step 1: Get the intercepted header from the X.com API request
|
||||
// useHeaders() provides access to all intercepted HTTP request headers
|
||||
// We filter for the specific X.com API endpoint we want to prove
|
||||
const [header] = useHeaders(headers => {
|
||||
return headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json'));
|
||||
});
|
||||
|
||||
// Step 2: Extract authentication headers from the intercepted request
|
||||
// These headers are required to authenticate with the X.com API
|
||||
const headers = {
|
||||
// Cookie: Session authentication token
|
||||
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
|
||||
|
||||
// X-CSRF-Token: Cross-Site Request Forgery protection token
|
||||
'x-csrf-token': header.requestHeaders.find(header => header.name === 'x-csrf-token')?.value,
|
||||
|
||||
// X-Client-Transaction-ID: Request tracking identifier
|
||||
'x-client-transaction-id': header.requestHeaders.find(header => header.name === 'x-client-transaction-id')?.value,
|
||||
|
||||
// Host: Target server hostname
|
||||
Host: 'api.x.com',
|
||||
|
||||
// Authorization: OAuth bearer token for API authentication
|
||||
authorization: header.requestHeaders.find(header => header.name === 'authorization')?.value,
|
||||
|
||||
// Accept-Encoding: Must be 'identity' for TLSNotary (no compression)
|
||||
// TLSNotary requires uncompressed data to verify byte-for-byte
|
||||
'Accept-Encoding': 'identity',
|
||||
|
||||
// Connection: Use 'close' to complete the connection after one request
|
||||
Connection: 'close',
|
||||
};
|
||||
|
||||
// Step 3: Generate TLS proof using the unified prove() API
|
||||
// This single function handles the entire proof generation pipeline
|
||||
const resp = await prove(
|
||||
// -------------------------------------------------------------------------
|
||||
// REQUEST OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Defines the HTTP request to be proven
|
||||
{
|
||||
url: 'https://api.x.com/1.1/account/settings.json', // Target API endpoint
|
||||
method: 'GET', // HTTP method
|
||||
headers: headers, // Authentication headers
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PROVER OPTIONS
|
||||
// -------------------------------------------------------------------------
|
||||
// Configures the TLS proof generation process
|
||||
{
|
||||
// Verifier URL: The notary server that verifies the TLS connection
|
||||
// Must be running locally or accessible at this address
|
||||
verifierUrl: 'http://localhost:7047',
|
||||
|
||||
// Proxy URL: WebSocket proxy that relays TLS data to the target server
|
||||
// The token parameter specifies which server to connect to
|
||||
proxyUrl: 'ws://localhost:7047/proxy?token=api.x.com',
|
||||
|
||||
// Maximum bytes to receive from server (response size limit)
|
||||
maxRecvData: 4000,
|
||||
|
||||
// Maximum bytes to send to server (request size limit)
|
||||
maxSentData: 2000,
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HANDLERS
|
||||
// -----------------------------------------------------------------------
|
||||
// These handlers specify which parts of the TLS transcript to reveal
|
||||
// in the proof. Unrevealed data is redacted for privacy.
|
||||
handlers: [
|
||||
// Reveal the request start line (GET /path HTTP/1.1)
|
||||
// This proves the HTTP method and path were sent
|
||||
{
|
||||
type: 'SENT', // Direction: data sent to server
|
||||
part: 'START_LINE', // Part: HTTP request line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the response start line (HTTP/1.1 200 OK)
|
||||
// This proves the server responded with status code 200
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'START_LINE', // Part: HTTP response line
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
},
|
||||
|
||||
// Reveal the 'date' header from the response
|
||||
// This proves when the server generated the response
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'HEADERS', // Part: HTTP headers
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
key: 'date', // Specific header to reveal
|
||||
},
|
||||
},
|
||||
|
||||
// Reveal the 'screen_name' field from the JSON response body
|
||||
// This proves the X.com username without revealing other profile data
|
||||
{
|
||||
type: 'RECV', // Direction: data received from server
|
||||
part: 'BODY', // Part: HTTP response body
|
||||
action: 'REVEAL', // Action: include as plaintext in proof
|
||||
params: {
|
||||
type: 'json', // Body format: JSON
|
||||
path: 'screen_name', // JSON field to reveal (top-level only)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Complete plugin execution and return the proof result
|
||||
// done() signals that the plugin has finished and passes the result back
|
||||
done(JSON.stringify(resp));
|
||||
}
|
||||
|
||||
function expandUI() {
|
||||
setState('isMinimized', false);
|
||||
}
|
||||
|
||||
function minimizeUI() {
|
||||
setState('isMinimized', true);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN UI FUNCTION
|
||||
// =============================================================================
|
||||
/**
|
||||
* The main() function is called reactively whenever plugin state changes.
|
||||
* It returns a DOM structure that is rendered as the plugin UI.
|
||||
*
|
||||
* React-like Hooks Used:
|
||||
* - useHeaders(): Subscribes to intercepted HTTP request headers
|
||||
* - useEffect(): Runs side effects when dependencies change
|
||||
*
|
||||
* UI Flow:
|
||||
* 1. Check if X.com API request headers have been intercepted
|
||||
* 2. If not intercepted yet: Show "Please login" message
|
||||
* 3. If intercepted: Show "Profile detected" with a "Prove" button
|
||||
* 4. On first render: Open X.com in a new window to trigger login
|
||||
*/
|
||||
function main() {
|
||||
// Subscribe to intercepted headers for the X.com API endpoint
|
||||
// This will reactively update whenever new headers matching the filter arrive
|
||||
const [header] = useHeaders(headers => headers.filter(header => header.url.includes('https://api.x.com/1.1/account/settings.json')));
|
||||
const isMinimized = useState('isMinimized', false);
|
||||
const isRequestPending = useState('isRequestPending', false);
|
||||
|
||||
// Run once on plugin load: Open X.com in a new window
|
||||
// The empty dependency array [] means this runs only once
|
||||
// The opened window's requests will be intercepted by the plugin
|
||||
useEffect(() => {
|
||||
openWindow('https://x.com');
|
||||
}, []);
|
||||
|
||||
// If minimized, show floating action button
|
||||
if (isMinimized) {
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#4CAF50',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
zIndex: '999999',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
},
|
||||
onclick: 'expandUI',
|
||||
}, ['🔐']);
|
||||
}
|
||||
|
||||
// Render the plugin UI overlay
|
||||
// This creates a fixed-position widget in the bottom-right corner
|
||||
return div({
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
right: '8px',
|
||||
width: '280px',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
|
||||
zIndex: '999999',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}, [
|
||||
// Header with minimize button
|
||||
div({
|
||||
style: {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
}
|
||||
}, [
|
||||
div({
|
||||
style: {
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
}
|
||||
}, ['X Profile Prover']),
|
||||
button({
|
||||
style: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer',
|
||||
padding: '0',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
onclick: 'minimizeUI',
|
||||
}, ['−'])
|
||||
]),
|
||||
|
||||
// Content area
|
||||
div({
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
}
|
||||
}, [
|
||||
// Status indicator showing whether profile is detected
|
||||
div({
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: header ? '#d4edda' : '#f8d7da',
|
||||
color: header ? '#155724' : '#721c24',
|
||||
border: `1px solid ${header ? '#c3e6cb' : '#f5c6cb'}`,
|
||||
fontWeight: '500',
|
||||
},
|
||||
}, [
|
||||
header ? '✓ Profile detected' : '⚠ No profile detected'
|
||||
]),
|
||||
|
||||
// Conditional UI based on whether we have intercepted the headers
|
||||
header ? (
|
||||
// Show prove button when not pending
|
||||
button({
|
||||
style: {
|
||||
width: '100%',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
opacity: isRequestPending ? 0.5 : 1,
|
||||
cursor: isRequestPending ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
onclick: 'onClick',
|
||||
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
|
||||
) : (
|
||||
// Show login message
|
||||
div({
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ffeaa7',
|
||||
}
|
||||
}, ['Please login to x.com to continue'])
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PLUGIN EXPORTS
|
||||
// =============================================================================
|
||||
/**
|
||||
* All plugins must export an object with these properties:
|
||||
* - main: The reactive UI rendering function
|
||||
* - onClick: Click handler callback for buttons
|
||||
* - config: Plugin metadata
|
||||
*/
|
||||
export default {
|
||||
main,
|
||||
onClick,
|
||||
expandUI,
|
||||
minimizeUI,
|
||||
config,
|
||||
};
|
||||
@@ -1,19 +1,12 @@
|
||||
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';
|
||||
}
|
||||
};
|
||||
// Get git commit hash from GIT_HASH env var (set by CI/Docker) or fallback to 'local'
|
||||
const gitHash = process.env.GIT_HASH || 'local';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__GIT_COMMIT_HASH__: JSON.stringify(getGitCommitHash()),
|
||||
__GIT_COMMIT_HASH__: JSON.stringify(gitHash),
|
||||
},
|
||||
plugins: [react()],
|
||||
build: {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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
6
packages/tutorial/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
public/plugins/*.js
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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
|
||||
@@ -1,236 +0,0 @@
|
||||
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`);
|
||||
@@ -1,29 +0,0 @@
|
||||
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,13 +1,650 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
<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>
|
||||
@@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB |
@@ -1,67 +0,0 @@
|
||||
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;
|
||||
@@ -1,76 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
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" />;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1,159 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,293 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,167 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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.',
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
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';
|
||||
},
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,238 +0,0 @@
|
||||
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
|
||||
];
|
||||
@@ -1,16 +0,0 @@
|
||||
/** @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: [],
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"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" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "build-plugins.js"]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -34,6 +34,10 @@ FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Accept build argument for git hash and set as environment variable
|
||||
ARG GIT_HASH=local
|
||||
ENV GIT_HASH=${GIT_HASH}
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn main() {
|
||||
// Build router with routes
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/info", get(info_handler))
|
||||
.route("/session", get(session_ws_handler))
|
||||
.route("/verifier", get(verifier_ws_handler))
|
||||
.route("/proxy", get(proxy_ws_handler))
|
||||
@@ -75,6 +76,7 @@ async fn main() {
|
||||
|
||||
info!("Server listening on http://{}", addr);
|
||||
info!("Health endpoint: http://{}/health", addr);
|
||||
info!("Info endpoint: http://{}/info", addr);
|
||||
info!("Session WebSocket endpoint: ws://{}/session", addr);
|
||||
info!(
|
||||
"Verifier WebSocket endpoint: ws://{}/verifier?sessionId=<id>",
|
||||
@@ -352,6 +354,28 @@ async fn health_handler() -> impl IntoResponse {
|
||||
"ok"
|
||||
}
|
||||
|
||||
/// Info response structure
|
||||
#[derive(Debug, Serialize)]
|
||||
struct InfoResponse {
|
||||
/// Package version from Cargo.toml
|
||||
version: &'static str,
|
||||
/// Git commit hash (from GIT_HASH env var, set by CI)
|
||||
git_hash: String,
|
||||
/// TLSNotary library version
|
||||
tlsn_version: &'static str,
|
||||
}
|
||||
|
||||
/// Info endpoint handler - returns server information as JSON
|
||||
pub(crate) async fn info_handler() -> impl IntoResponse {
|
||||
let git_hash = std::env::var("GIT_HASH").unwrap_or_else(|_| "dev".to_string());
|
||||
|
||||
axum::Json(InfoResponse {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
git_hash,
|
||||
tlsn_version: "0.1.0-alpha.14",
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket session handler for extension
|
||||
pub(crate) async fn session_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
|
||||
@@ -28,9 +28,11 @@ use tracing::info;
|
||||
use ws_stream_tungstenite::WsStream;
|
||||
|
||||
use tlsn::{
|
||||
config::ProtocolConfig,
|
||||
connection::ServerName,
|
||||
prover::{ProveConfig, Prover, ProverConfig},
|
||||
config::{
|
||||
prove::ProveConfig, prover::ProverConfig, tls::TlsClientConfig, tls_commit::TlsCommitConfig,
|
||||
},
|
||||
prover::Prover,
|
||||
Session,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -156,11 +158,11 @@ async fn webhook_handler(
|
||||
// ============================================================================
|
||||
|
||||
async fn start_verifier_server(webhook_port: u16, verifier_port: u16) -> JoinHandle<()> {
|
||||
// Create config with webhook for swapi.dev
|
||||
// Create config with webhook for raw.githubusercontent.com
|
||||
let config_yaml = format!(
|
||||
r#"
|
||||
webhooks:
|
||||
"swapi.dev":
|
||||
"raw.githubusercontent.com":
|
||||
url: "http://127.0.0.1:{}"
|
||||
headers: {{}}
|
||||
"#,
|
||||
@@ -176,14 +178,9 @@ webhooks:
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", axum::routing::get(|| async { "ok" }))
|
||||
.route(
|
||||
"/session",
|
||||
axum::routing::get(crate::session_ws_handler),
|
||||
)
|
||||
.route(
|
||||
"/verifier",
|
||||
axum::routing::get(crate::verifier_ws_handler),
|
||||
)
|
||||
.route("/info", axum::routing::get(crate::info_handler))
|
||||
.route("/session", axum::routing::get(crate::session_ws_handler))
|
||||
.route("/verifier", axum::routing::get(crate::verifier_ws_handler))
|
||||
.route("/proxy", axum::routing::get(crate::proxy_ws_handler))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(app_state);
|
||||
@@ -385,15 +382,23 @@ async fn connect_wss(
|
||||
|
||||
/// Helper function that performs MPC-TLS and HTTP request with a given proxy stream
|
||||
async fn run_prover_with_stream<S>(
|
||||
prover: tlsn::prover::Prover<tlsn::prover::state::Setup>,
|
||||
prover: Prover,
|
||||
tls_commit_config: TlsCommitConfig,
|
||||
tls_client_config: TlsClientConfig,
|
||||
proxy_stream: S,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error + Send + Sync>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
// 5. Pass proxy connection into the prover for TLS
|
||||
// 5. Start the TLS commitment protocol
|
||||
let prover = prover
|
||||
.commit(tls_commit_config)
|
||||
.await
|
||||
.map_err(|e| format!("Commitment failed: {}", e))?;
|
||||
|
||||
// 6. Pass proxy connection into the prover for TLS
|
||||
let (mpc_tls_connection, prover_fut) = prover
|
||||
.connect(proxy_stream)
|
||||
.connect(tls_client_config, proxy_stream)
|
||||
.await
|
||||
.map_err(|e| format!("TLS connect failed: {}", e))?;
|
||||
|
||||
@@ -405,18 +410,19 @@ where
|
||||
// Spawn the prover task
|
||||
let prover_task = tokio::spawn(prover_fut);
|
||||
|
||||
// 6. HTTP handshake
|
||||
let (mut request_sender, connection) = hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
// 7. HTTP handshake
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(mpc_tls_connection)
|
||||
.await
|
||||
.map_err(|e| format!("HTTP handshake failed: {}", e))?;
|
||||
|
||||
tokio::spawn(connection);
|
||||
|
||||
// 7. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /api/films/1/");
|
||||
// 8. Send HTTP GET request
|
||||
info!("[Prover] Sending GET /tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json");
|
||||
let request = Request::builder()
|
||||
.uri("/api/films/1/")
|
||||
.header("Host", "swapi.dev")
|
||||
.uri("/tlsnotary/tlsn/refs/heads/main/crates/server-fixture/server/src/data/1kb.json")
|
||||
.header("Host", "raw.githubusercontent.com")
|
||||
.header("Accept", "application/json")
|
||||
.header("Connection", "close")
|
||||
.method("GET")
|
||||
@@ -431,7 +437,7 @@ where
|
||||
info!("[Prover] Response status: {}", response.status());
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// 8. Wait for prover task to complete
|
||||
// 9. Wait for prover task to complete
|
||||
let mut prover = prover_task
|
||||
.await
|
||||
.map_err(|e| format!("Prover task panicked: {}", e))?
|
||||
@@ -446,24 +452,26 @@ where
|
||||
recv.len()
|
||||
);
|
||||
|
||||
// 9. Build reveal configuration (reveal everything)
|
||||
let mut builder = ProveConfig::builder(prover.transcript());
|
||||
builder.server_identity();
|
||||
builder
|
||||
// 10. Build proof configuration (reveal everything including server identity)
|
||||
let mut prove_config = ProveConfig::builder(prover.transcript());
|
||||
prove_config.server_identity();
|
||||
prove_config
|
||||
.reveal_sent(&(0..sent.len()))
|
||||
.map_err(|e| format!("reveal_sent failed: {}", e))?;
|
||||
builder
|
||||
prove_config
|
||||
.reveal_recv(&(0..recv.len()))
|
||||
.map_err(|e| format!("reveal_recv failed: {}", e))?;
|
||||
let prove_config = prove_config
|
||||
.build()
|
||||
.map_err(|e| format!("build proof failed: {}", e))?;
|
||||
|
||||
let config = builder.build().unwrap();
|
||||
|
||||
// 10. Send proof to verifier
|
||||
// 11. Send proof to verifier
|
||||
info!("[Prover] Sending proof to verifier");
|
||||
prover
|
||||
.prove(&config)
|
||||
.prove(&prove_config)
|
||||
.await
|
||||
.map_err(|e| format!("prove failed: {}", e))?;
|
||||
|
||||
prover
|
||||
.close()
|
||||
.await
|
||||
@@ -491,50 +499,134 @@ async fn run_prover(
|
||||
// WsStream implements tokio::io::AsyncRead/AsyncWrite when inner implements futures_io traits
|
||||
let verifier_stream = WsStream::new(verifier_ws);
|
||||
|
||||
// 2. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.server_name(ServerName::Dns("swapi.dev".try_into().unwrap()))
|
||||
.protocol_config(
|
||||
ProtocolConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
// 2. Create session with verifier stream
|
||||
let session = Session::new(verifier_stream);
|
||||
let (driver, mut handle) = session.split();
|
||||
|
||||
// Spawn the session driver in the background
|
||||
let driver_task = tokio::spawn(driver);
|
||||
|
||||
// 3. Create TLS commit config for MPC protocol
|
||||
use tlsn::config::tls_commit::{mpc::MpcTlsConfig, TlsCommitProtocolConfig};
|
||||
let mpc_config = MpcTlsConfig::builder()
|
||||
.max_sent_data(max_sent_data)
|
||||
.max_recv_data(max_recv_data)
|
||||
.build()
|
||||
.unwrap();
|
||||
.map_err(|e| format!("Failed to build MPC TLS config: {}", e))?;
|
||||
|
||||
let tls_commit_config = TlsCommitConfig::builder()
|
||||
.protocol(TlsCommitProtocolConfig::Mpc(mpc_config))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS commit config: {}", e))?;
|
||||
|
||||
// 4. Create prover config
|
||||
let prover_config = ProverConfig::builder()
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build prover config: {}", e))?;
|
||||
|
||||
info!("[Prover] Setting up MPC-TLS with verifier");
|
||||
|
||||
// 3. Create prover and perform setup with verifier
|
||||
// tlsn expects futures_io traits, so we don't need compat() - WsStream already provides them
|
||||
let prover = Prover::new(prover_config)
|
||||
.setup(verifier_stream)
|
||||
.await
|
||||
.map_err(|e| format!("Prover setup failed: {}", e))?;
|
||||
// 5. Create prover via handle
|
||||
let prover = handle
|
||||
.new_prover(prover_config)
|
||||
.map_err(|e| format!("Failed to create prover: {}", e))?;
|
||||
|
||||
// 6. Create TLS client config with server name and root certs
|
||||
use tlsn::{connection::ServerName, webpki::RootCertStore};
|
||||
let tls_client_config = TlsClientConfig::builder()
|
||||
.server_name(ServerName::Dns(
|
||||
"raw.githubusercontent.com".try_into().unwrap(),
|
||||
))
|
||||
.root_store(RootCertStore::mozilla())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build TLS client config: {}", e))?;
|
||||
|
||||
info!("[Prover] Connecting to proxy at {}", proxy_url);
|
||||
|
||||
// 4. Connect to proxy WebSocket (ws:// or wss://)
|
||||
if proxy_url.starts_with("wss://") {
|
||||
// 7. Connect to proxy WebSocket (ws:// or wss://) and run prover
|
||||
let result = if proxy_url.starts_with("wss://") {
|
||||
let proxy_ws = connect_wss(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (wss)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
} else {
|
||||
let proxy_ws = connect_ws(&proxy_url).await?;
|
||||
info!("[Prover] Connected to proxy (ws)");
|
||||
let proxy_stream = WsStream::new(proxy_ws);
|
||||
run_prover_with_stream(prover, proxy_stream).await
|
||||
}
|
||||
run_prover_with_stream(prover, tls_commit_config, tls_client_config, proxy_stream).await
|
||||
};
|
||||
|
||||
// 8. Close the session handle
|
||||
handle.close();
|
||||
|
||||
// 9. Wait for the driver to complete
|
||||
driver_task
|
||||
.await
|
||||
.map_err(|e| format!("Driver task failed: {}", e))?
|
||||
.map_err(|e| format!("Session driver error: {}", e))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Test
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Test the /health endpoint
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_swapi() {
|
||||
async fn health() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 1, VERIFIER_PORT + 1).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/health", VERIFIER_PORT + 1))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.text().await.unwrap(), "ok");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
/// Test the /info endpoint returns expected JSON structure
|
||||
#[tokio::test]
|
||||
async fn info() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.try_init();
|
||||
|
||||
let verifier_handle = start_verifier_server(WEBHOOK_PORT + 2, VERIFIER_PORT + 2).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.get(format!("http://127.0.0.1:{}/info", VERIFIER_PORT + 2))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let info: Value = resp.json().await.expect("Failed to parse JSON");
|
||||
|
||||
// Verify required fields exist
|
||||
info.get("version").expect("Missing version field");
|
||||
info.get("git_hash").expect("Missing git_hash field");
|
||||
info.get("tlsn_version")
|
||||
.expect("Missing tlsn_version field");
|
||||
|
||||
verifier_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webhook_integration_with_github() {
|
||||
// Initialize tracing for debugging
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
@@ -573,7 +665,10 @@ async fn test_webhook_integration_with_swapi() {
|
||||
"ws://127.0.0.1:{}/verifier?sessionId={}",
|
||||
VERIFIER_PORT, session_id
|
||||
);
|
||||
let proxy_url = format!("ws://127.0.0.1:{}/proxy?token=swapi.dev", VERIFIER_PORT);
|
||||
let proxy_url = format!(
|
||||
"ws://127.0.0.1:{}/proxy?token=raw.githubusercontent.com",
|
||||
VERIFIER_PORT
|
||||
);
|
||||
|
||||
let prover_handle = tokio::spawn(async move {
|
||||
run_prover(verifier_ws_url, proxy_url, MAX_SENT_DATA, MAX_RECV_DATA).await
|
||||
@@ -628,11 +723,11 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// 8. Verify results contain expected data
|
||||
assert!(!results.is_empty(), "Should have handler results");
|
||||
|
||||
// Check that response contains Star Wars data
|
||||
// Check that response contains expected JSON data
|
||||
let recv_str = String::from_utf8_lossy(&recv_transcript);
|
||||
assert!(
|
||||
recv_str.contains("A New Hope") || recv_str.contains("Star Wars"),
|
||||
"Response should contain Star Wars film data: {}",
|
||||
recv_str.contains("software engineer") || recv_str.contains("Anytown"),
|
||||
"Response should contain expected JSON data: {}",
|
||||
&recv_str[..recv_str.len().min(500)]
|
||||
);
|
||||
|
||||
@@ -651,8 +746,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
|
||||
// Verify webhook payload structure
|
||||
assert_eq!(
|
||||
payload["server_name"], "swapi.dev",
|
||||
"server_name should be swapi.dev"
|
||||
payload["server_name"], "raw.githubusercontent.com",
|
||||
"server_name should be raw.githubusercontent.com"
|
||||
);
|
||||
assert!(payload["results"].is_array(), "results should be an array");
|
||||
assert!(
|
||||
@@ -691,8 +786,8 @@ async fn test_webhook_integration_with_swapi() {
|
||||
// Verify transcript contains expected content
|
||||
let webhook_recv = payload["transcript"]["recv"].as_str().unwrap();
|
||||
assert!(
|
||||
webhook_recv.contains("A New Hope") || webhook_recv.contains("title"),
|
||||
"Webhook transcript should contain Star Wars film data"
|
||||
webhook_recv.contains("software engineer") || webhook_recv.contains("Anytown"),
|
||||
"Webhook transcript should contain expected JSON data"
|
||||
);
|
||||
|
||||
info!("All assertions passed!");
|
||||
|
||||
Reference in New Issue
Block a user