Compare commits

..

5 Commits

Author SHA1 Message Date
Hendrik Eeckhaut
571631d78c working on discord plugin 2026-01-21 09:58:15 +01:00
Hendrik Eeckhaut
99a466db02 wip 2026-01-21 09:58:15 +01:00
Hendrik Eeckhaut
2b72884192 Duolingo streak plugin (#215) 2026-01-21 09:57:51 +01:00
Hendrik Eeckhaut
9b22e2af37 feat: add /info endpoint to verifier (#226) 2026-01-21 09:56:15 +01:00
Hendrik Eeckhaut
bbe6e23d5f fix: fix tests for alpha.14 + make sure the tests run in CI 2026-01-21 09:56:15 +01:00
59 changed files with 1339 additions and 4423 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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
}
}
}
}
}

View File

@@ -24,7 +24,7 @@
"clean": "rm -rf packages/*/node_modules packages/*/dist packages/*/build node_modules",
"build:wasm": "sh packages/tlsn-wasm/build.sh v0.1.0-alpha.14 --no-logging",
"demo": "npm run dev --workspace=@tlsnotary/demo",
"tutorial": "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"
},

View File

@@ -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', 'discord_dm', 'discord_profile'];
// Build URLs from environment variables (matching config.ts pattern)
const VERIFIER_HOST = process.env.VITE_VERIFIER_HOST || 'localhost:7047';

View File

@@ -28,4 +28,31 @@ 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;
},
},
// discord_dm: {
// name: 'Discord DM',
// description: 'Prove your Discord direct messages',
// logo: '💬',
// file: '/plugins/discord_dm.js',
// parseResult: (json) => {
// return json.results[json.results.length - 1].value;
// },
// },
discord_profile: {
name: 'Discord Profile',
description: 'Prove your Discord profile information',
logo: '💬',
file: '/plugins/discord_profile.js',
parseResult: (json) => {
return json.results[json.results.length - 1].value;
},
},
};

View File

@@ -0,0 +1,342 @@
/// <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 = 'discord.com';
const ui = 'https://discord.com/channels/@me';
const config = {
name: 'Discord DM Plugin',
description: 'This plugin will prove your Discord direct messages.',
requests: [
{
method: 'GET',
host: 'discord.com',
pathname: '/api/v9/users/@me/channels',
verifierUrl: VERIFIER_URL,
},
{
method: 'GET',
host: 'discord.com',
pathname: '/api/v9/channels/*/messages',
verifierUrl: VERIFIER_URL,
},
],
urls: [
'https://discord.com/*',
],
};
function getRelevantHeaderValues() {
const [header] = useHeaders(headers => {
return headers.filter(header =>
header.url.includes(`https://${api}/api/v9/users/@me`) ||
header.url.includes(`https://${api}/api/v9/channels`)
);
});
const authorization = header?.requestHeaders.find(header => header.name === 'authorization')?.value;
return { authorization };
}
async function fetchDMs() {
const { authorization } = getRelevantHeaderValues();
if (!authorization) return [];
try {
const headers = {
authorization: authorization,
Host: api,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const response = await fetch(`https://${api}/api/v9/users/@me/channels`, {
method: 'GET',
headers: headers,
});
const channels = await response.json();
// Filter only DM channels (type 1)
return channels.filter((channel: any) => channel.type === 1).map((channel: any) => ({
id: channel.id,
name: channel.recipients?.[0]?.username || 'Unknown User',
avatar: channel.recipients?.[0]?.avatar,
}));
} catch (error) {
console.error('Error fetching DMs:', error);
return [];
}
}
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
const selectedDMId = useState('selectedDMId', '');
if (isRequestPending || !selectedDMId) return;
setState('isRequestPending', true);
const { authorization } = getRelevantHeaderValues();
const headers = {
authorization: authorization,
Host: api,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: `https://${api}/api/v9/channels/${selectedDMId}/messages?limit=50`,
method: 'GET',
headers: headers,
},
{
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + api,
maxRecvData: 8000,
maxSentData: 2000,
handlers: [
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].content' } },
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].author.username' } },
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: '[*].timestamp' } },
]
}
);
setState('isRequestPending', false);
done(JSON.stringify(resp));
}
function selectDM(dmId: string) {
setState('selectedDMId', dmId);
}
function expandUI() {
setState('isMinimized', false);
}
function minimizeUI() {
setState('isMinimized', true);
}
function main() {
const { authorization } = getRelevantHeaderValues();
const header_has_necessary_values = !!authorization;
const isMinimized = useState('isMinimized', false);
const isRequestPending = useState('isRequestPending', false);
const selectedDMId = useState('selectedDMId', '');
const dmList = useState('dmList', []);
useEffect(() => {
openWindow(ui);
}, []);
useEffect(() => {
if (header_has_necessary_values && dmList.length === 0) {
fetchDMs().then(dms => setState('dmList', dms));
}
}, [header_has_necessary_values]);
if (isMinimized) {
return div({
style: {
position: 'fixed',
bottom: '20px',
right: '20px',
width: '60px',
height: '60px',
borderRadius: '50%',
backgroundColor: '#5865F2',
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: '320px',
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, #5865F2 0%, #4752C4 100%)',
padding: '12px 16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: 'white',
}
}, [
div({
style: {
fontWeight: '600',
fontSize: '16px',
}
}, ['Discord DM Proof']),
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',
}
}, [
// Step 1: Login Status
div({
style: {
marginBottom: '16px',
padding: '12px',
borderRadius: '6px',
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_has_necessary_values ? '✓ Discord token detected' : '⚠ No Discord token detected'
]),
// Step 2: DM Selection
header_has_necessary_values && dmList.length > 0 ? (
div({
style: {
marginBottom: '16px',
}
}, [
div({
style: {
marginBottom: '8px',
fontWeight: '600',
color: '#333',
}
}, ['Select a DM:']),
div({
style: {
maxHeight: '200px',
overflowY: 'auto',
border: '1px solid #ddd',
borderRadius: '6px',
backgroundColor: 'white',
}
}, dmList.map((dm: any) =>
div({
style: {
padding: '10px 12px',
cursor: 'pointer',
borderBottom: '1px solid #f0f0f0',
backgroundColor: selectedDMId === dm.id ? '#e3f2fd' : 'transparent',
transition: 'background-color 0.2s',
},
onclick: () => selectDM(dm.id),
}, [
div({
style: {
fontWeight: selectedDMId === dm.id ? '600' : '400',
color: '#333',
}
}, [dm.name])
])
))
])
) : null,
// Step 3: Notarize Button
header_has_necessary_values && selectedDMId ? (
button({
style: {
width: '100%',
padding: '12px 24px',
borderRadius: '6px',
border: 'none',
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
color: 'white',
fontWeight: '600',
fontSize: '15px',
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,
},
onclick: 'onClick',
}, [isRequestPending ? 'Generating Proof...' : 'Generate Proof'])
) : header_has_necessary_values && dmList.length === 0 ? (
div({
style: {
textAlign: 'center',
color: '#666',
padding: '12px',
backgroundColor: '#fff3cd',
borderRadius: '6px',
border: '1px solid #ffeaa7',
}
}, ['Loading DMs...'])
) : !header_has_necessary_values ? (
div({
style: {
textAlign: 'center',
color: '#666',
padding: '12px',
backgroundColor: '#fff3cd',
borderRadius: '6px',
border: '1px solid #ffeaa7',
}
}, ['Please login to Discord to continue'])
) : null
])
]);
}
export default {
main,
onClick,
expandUI,
minimizeUI,
fetchDMs,
selectDM,
config,
};

View File

@@ -1,24 +1,40 @@
/// <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 = 'discord.com';
const ui = `https://${api}/channels/@me`;
const config = {
name: 'Swiss Bank Prover',
description: 'This plugin will prove your Swiss Bank account balance.',
name: 'Discord Profile Plugin',
description: 'This plugin will prove your Discord username and ID.',
requests: [
{
method: 'GET',
host: 'swissbank.tlsnotary.org',
pathname: '/balances',
verifierUrl: 'http://localhost:7047',
host: api,
pathname: '/api/v9/users/@me',
verifierUrl: VERIFIER_URL,
},
],
urls: [
'https://swissbank.tlsnotary.org/*',
`https://${api}/*`,
],
};
const host = 'swissbank.tlsnotary.org';
const ui_path = '/account';
const path = '/balances';
const url = `https://${host}${path}`;
function getRelevantHeaderValues() {
const [header] = useHeaders(headers => {
// console.log('All captured headers:', headers);
// Find the first header that contains an 'authorization' request header, regardless of URL
return [headers.find(h =>
h.requestHeaders.some(rh => rh.name === 'Authorization')
)];
});
const authorization = header?.requestHeaders.find(h => h.name === 'Authorization')?.value;
return { authorization };
}
async function onClick() {
const isRequestPending = useState('isRequestPending', false);
@@ -26,52 +42,35 @@ async function onClick() {
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 { authorization } = getRelevantHeaderValues();
const headers = {
'cookie': header.requestHeaders.find(header => header.name === 'Cookie')?.value,
Host: host,
authorization: authorization,
Host: api,
'Accept-Encoding': 'identity',
Connection: 'close',
};
const resp = await prove(
{
url: url,
url: `https://${api}/api/v9/users/@me`,
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.
verifierUrl: VERIFIER_URL,
proxyUrl: PROXY_URL_BASE + api,
maxRecvData: 2000,
maxSentData: 1000,
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"' }, },
{ type: 'SENT', part: 'START_LINE', action: 'REVEAL' },
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'username' } },
{ type: 'RECV', part: 'BODY', action: 'REVEAL', params: { type: 'json', path: 'id' } },
]
}
);
// 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));
}
@@ -82,23 +81,21 @@ function expandUI() {
function minimizeUI() {
setState('isMinimized', true);
}
function main() {
const [header] = useHeaders(
headers => headers
.filter(header => header.url.includes(`https://${host}${ui_path}`))
);
const { authorization } = getRelevantHeaderValues();
console.log('🚀🚀🚀🚀🚀🚀🚀 Authorization Header:', authorization);
const header_has_necessary_values = !!authorization;
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}`);
openWindow(ui);
}, []);
// If minimized, show floating action button
if (isMinimized) {
return div({
style: {
@@ -108,7 +105,7 @@ function main() {
width: '60px',
height: '60px',
borderRadius: '50%',
backgroundColor: '#4CAF50',
backgroundColor: '#5865F2',
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
zIndex: '999999',
display: 'flex',
@@ -120,17 +117,15 @@ function main() {
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',
width: '320px',
borderRadius: '8px 8px 0 0',
backgroundColor: 'white',
boxShadow: '0 -2px 10px rgba(0,0,0,0.1)',
@@ -140,10 +135,9 @@ function main() {
overflow: 'hidden',
},
}, [
// Header with minimize button
div({
style: {
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 100%)',
padding: '12px 16px',
display: 'flex',
justifyContent: 'space-between',
@@ -156,7 +150,7 @@ function main() {
fontWeight: '600',
fontSize: '16px',
}
}, ['Swiss Bank Prover']),
}, ['Discord Profile Proof']),
button({
style: {
background: 'transparent',
@@ -175,51 +169,45 @@ function main() {
}, [''])
]),
// 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'}`,
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',
},
}, [
hasNecessaryHeader ? '✓ Cookie detected' : '⚠ No Cookie detected'
header_has_necessary_values ? '✓ Discord token detected' : '⚠ Please login to Discord'
]),
// Conditional UI based on whether we have intercepted the headers
hasNecessaryHeader ? (
// 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, #667eea 0%, #764ba2 100%)',
background: 'linear-gradient(135deg, #5865F2 0%, #4752C4 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',
@@ -229,7 +217,7 @@ function main() {
borderRadius: '6px',
border: '1px solid #ffeaa7',
}
}, ['Please login to continue'])
}, ['Please login to Discord to continue'])
)
])
]);
@@ -241,4 +229,4 @@ export default {
expandUI,
minimizeUI,
config,
};
};

View File

@@ -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'])
)
])
]);

View File

@@ -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,
};

View File

@@ -158,7 +158,7 @@ function makeUseHeaders(
// Validate that filterFn returned an array
if (result === undefined) {
throw new Error(`useHeaders: filter function returned undefined. expect an erray`);
throw new Error(`useHeaders: filter function returned undefined. expect an array`);
}
if (!Array.isArray(result)) {
throw new Error(`useHeaders: filter function must return an array, got ${typeof result}. `);

View File

@@ -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"
}
}
}

View File

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

View File

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

View File

@@ -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

View File

@@ -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`);

View File

@@ -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

View File

@@ -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://&lt;host&gt;</code>:
<pre><code>wstcp --bind-addr 127.0.0.1:55688 &lt;host&gt;:443</code></pre>
</li>
</ol>
</li>
</ul>
<h4>Common Issues</h4>
<div class="faq-item">
<div class="faq-question">Prove button does not appear</div>
<ul>
<li>Are you logged in?</li>
<li>Bug: open the <b>inspect</b> view console and the dialog appears</li>
</ul>
</div>
<div class="faq-item">
<div class="faq-question">Plugin Execution Problems</div>
<p>For detailed extension logs, check the service worker logs:</p>
<ul>
<li>Go to <code>chrome://extensions/</code></li>
<li>Find TLSNotary extension and click "service worker"</li>
<li><strong>Or copy and paste this into address bar:</strong><br>
<code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code>
</li>
<li>Look for "offscreen.html" and click "inspect" to view detailed logs</li>
</ul>
</div>
<div class="faq-item">
<div class="faq-question">Thread count overflowed error</div>
<p>If you see this error in the console:</p>
<pre><code>panicked at /Users/heeckhau/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sharded-slab-0.1.7/src/shard.rs:295:9:
Thread count overflowed the configured max count. Thread index = 142, max threads = 128.</code></pre>
<p><strong>Workaround:</strong> Restart the extension:</p>
<ol>
<li>Go to <code>chrome://extensions/?id=lihhbeidchpkifaeepopfabenlcpfhjn</code></li>
<li>Click the toggle to disable the extension</li>
<li>Click the toggle again to re-enable it</li>
</ol>
<p>This is a known issue: <a href="https://github.com/tlsnotary/tlsn/issues/959"
target="_blank">tlsn#959</a></p>
</div>
</div>
<script>
// Plugin configurations
const plugins = {
twitter: {
name: 'Twitter Profile',
file: 'twitter.js',
parseResult: (json) => {
const screen_name_result = json.results[3].value;
const screen_name = screen_name_result.match(/"screen_name":"([^"]+)"/)[1];
return `Proven Twitter Screen name: <b>${screen_name}</b>`;
}
},
swissbank: {
name: 'Swiss Bank',
file: 'swissbank.js',
parseResult: (json) => {
const lastResult = json.results[json.results.length - 1].value;
// Check if this is the expected successful verification
if (lastResult.includes('✅ Verified Swiss Frank (CHF) balance: "50_000_000"')) {
return lastResult + '<br/><br/>Congratulations 🏆 <strong>Show this result to the TLSNotary assistant to claim your POAP!</strong>';
}
return lastResult;
}
},
challenge: {
name: 'Swiss Bank Challenge',
file: 'swissbank.js',
parseResult: (json) => {
const lastResult = json.results[json.results.length - 1].value;
// Check for any balance verification
const match = lastResult.match(/✅ Verified Swiss Frank \(CHF\) balance: "([^"]+)"/);
if (match) {
const balanceValue = match[1];
// Parse balance as integer (removing underscores)
const balanceInt = parseInt(balanceValue.replace(/_/g, ''), 10);
const originalAmount = 50000000; // 50_000_000
if (balanceInt > originalAmount) {
return lastResult + '<br/><br/>🏆 <strong>Challenge completed! Show this to the TLSNotary assistant!</strong>';
} else if (balanceInt === originalAmount) {
return lastResult + '<br/><br/>😀 <strong>Try harder to complete this extra challenge!</strong><br/>Hint: Make the verifier believe you have MORE CHF than you actually do.';
} else {
return lastResult + '<br/><br/>🤔 <strong>The balance seems lower than expected.</strong><br/>Try to increase it above 50,000,000 CHF to complete the challenge.';
}
}
// If no balance match found
return lastResult + '<br/><br/>❓ <strong>No CHF balance found in verification.</strong> Make sure your regex correctly extracts the balance.';
}
}
};
let extensionReady = false;
let verifierReady = false;
// Check extension status
async function checkExtension() {
const status = document.getElementById('extension-status');
const instructions = document.getElementById('extension-instructions');
const step = document.getElementById('step-extension');
status.textContent = 'Checking extension...';
status.className = 'status checking';
// Wait a bit for tlsn to load if page just loaded
await new Promise(resolve => setTimeout(resolve, 1000));
if (typeof window.tlsn !== 'undefined') {
status.textContent = '✅ Extension installed and ready';
status.className = 'status success';
instructions.style.display = 'none';
step.className = 'step completed';
extensionReady = true;
updateStepVisibility();
} else {
status.textContent = '❌ Extension not found';
status.className = 'status error';
instructions.style.display = 'block';
step.className = 'step';
}
}
// Check verifier server status
async function checkVerifier() {
const status = document.getElementById('verifier-status');
const instructions = document.getElementById('verifier-instructions');
const step = document.getElementById('step-verifier');
status.textContent = 'Checking verifier server...';
status.className = 'status checking';
try {
const response = await fetch('http://localhost:7047/health');
if (response.ok && await response.text() === 'ok') {
status.textContent = '✅ Verifier server running';
status.className = 'status success';
instructions.style.display = 'none';
step.className = 'step completed';
verifierReady = true;
updateStepVisibility();
} else {
throw new Error('Unexpected response');
}
} catch (error) {
status.textContent = '❌ Verifier server not responding';
status.className = 'status error';
instructions.style.display = 'block';
step.className = 'step';
}
}
// Check if browser is Chrome-based
function checkBrowserCompatibility() {
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
const isEdge = /Edg/.test(navigator.userAgent);
const isBrave = navigator.brave && typeof navigator.brave.isBrave === 'function';
const isChromium = /Chromium/.test(navigator.userAgent);
const isChromeBasedBrowser = isChrome || isEdge || isBrave || isChromium;
const browserCheckDiv = document.getElementById('browser-check');
if (!isChromeBasedBrowser) {
browserCheckDiv.style.display = 'block';
// Optionally disable the rest of the tutorial
document.querySelectorAll('.step:not(#browser-check)').forEach(step => {
step.style.opacity = '0.5';
step.style.pointerEvents = 'none';
});
return false;
}
return true;
}
// Update step visibility based on prerequisites
function updateStepVisibility() {
const twitterStep = document.getElementById('step-twitter');
const swissbankStep = document.getElementById('step-swissbank');
const stepExtra = document.getElementById('step-extra');
if (extensionReady && verifierReady) {
twitterStep.className = 'step';
document.getElementById('twitter-ready').style.display = 'block';
swissbankStep.className = 'step';
document.getElementById('swissbank-ready').style.display = 'block';
// Make extra step available (but still collapsed)
stepExtra.className = 'step';
}
}
function showExtraStep() {
const content = document.getElementById('next-content');
const button = document.getElementById('step-extra-toggle'); // Fix: Use correct ID
content.style.display = 'block';
button.style.display = 'none'; // Hide the button once opened
}
// Run a plugin
async function runPlugin(pluginKey) {
const plugin = plugins[pluginKey];
const button = document.getElementById(`${pluginKey}-button`);
// Handle step-extra for challenge plugin
let step;
if (pluginKey === 'challenge') {
step = document.getElementById('step-extra');
} else {
step = document.getElementById(`step-${pluginKey}`);
}
try {
console.log(`Running ${plugin.name} plugin...`);
button.disabled = true;
button.textContent = 'Running...';
// Clear previous results in this step
const existingResults = step.querySelectorAll('.result, .debug, h4');
existingResults.forEach(el => el.remove());
const pluginCode = await fetch(plugin.file).then(r => r.text());
const result = await window.tlsn.execCode(pluginCode);
if (!result || typeof result !== 'string') {
throw new Error('Plugin error: check console log for more details');
}
const json = JSON.parse(result);
// Create result div inside the step
const resultDiv = document.createElement('div');
resultDiv.className = 'result';
resultDiv.innerHTML = plugin.parseResult(json);
step.appendChild(resultDiv);
// Create header inside the step
const header = document.createElement('h4');
header.textContent = `${plugin.name} Results:`;
step.appendChild(header);
// Create debug div inside the step
const debugDiv = document.createElement('div');
debugDiv.className = 'debug';
debugDiv.textContent = JSON.stringify(json.results, null, 2);
step.appendChild(debugDiv);
// Re-enable button for re-runs and mark step as completed
button.textContent = `Run ${plugin.name} Again`;
button.disabled = false;
step.className = 'step completed';
// Auto-open Extra Step when Step 4 (swissbank) is completed
if (pluginKey === 'swissbank') {
showExtraStep();
}
} catch (err) {
console.error(err);
// Clear previous error messages
const existingErrors = step.querySelectorAll('pre[style*="color: red"]');
existingErrors.forEach(el => el.remove());
// Create error div inside the step
const errorDiv = document.createElement('pre');
errorDiv.style.color = 'red';
errorDiv.textContent = err.message;
step.appendChild(errorDiv);
button.textContent = `Run ${plugin.name}`;
button.disabled = false;
}
}
// Initialize checks when page loads
window.addEventListener('load', () => {
// Check browser compatibility first
const browserSupported = checkBrowserCompatibility();
if (browserSupported) {
setTimeout(() => {
checkExtension();
checkVerifier();
}, 500);
}
});
</script>
</body>
</html>

View File

@@ -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;
}
}

View File

@@ -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"
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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" />;
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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,
};
};

View File

@@ -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>
);

View File

@@ -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&apos;ve focused on the prover. Verification is also extremely important. Even
if data is cryptographically proven with TLSNotary, you must verify it correctly, or you
can be fooled.
</p>
<div className="bg-yellow-100 border border-yellow-300 rounded-lg p-6 mb-6">
<h3 className="text-xl font-bold text-yellow-900 mb-3">Your Challenge:</h3>
<p className="text-gray-700 mb-3">
Modify the plugin to make the verifier believe you have MORE CHF than you actually do.
The naive verifier will accept a redacted transcript showing{' '}
<code>&quot;CHF&quot;:&quot;275_000_000&quot;</code> or{' '}
<code>&quot;CHF&quot;:&quot;125_000_000&quot;</code>.
</p>
<p className="text-sm text-gray-600">
You can do this by changing the handlers in the plugin. You cannot change the verifier
code.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
<h4 className="font-bold text-blue-900 mb-3">💡 Hints:</h4>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>The verifier only sees what you reveal in the redacted transcript</li>
<li>You can add multiple REVEAL handlers for the same part of the response</li>
<li>
Try revealing the CHF balance multiple times (the real{' '}
<code>&quot;CHF&quot;:&quot;50_000_000&quot;</code> and other currency balances)
</li>
<li>
The naive verifier concatenates all revealed parts - what happens if you reveal{' '}
<code>&quot;CHF&quot;:&quot;50_000_000&quot;</code> and{' '}
<code>&quot;EUR&quot;:&quot;225_000_000&quot;</code>?
</li>
</ul>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
{validationResults.length > 0 && (
<div className="mb-4 space-y-2">
{validationResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded ${
result.valid ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}
>
{result.message}
</div>
))}
</div>
)}
<div className="flex gap-4 mb-4">
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
{isExecuting ? 'Testing...' : 'Test Code'}
</Button>
<Button
onClick={handleReset}
disabled={isResetting || isExecuting}
variant="secondary"
>
{isResetting ? 'Resetting...' : 'Reset Code'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900 mb-2">Challenge Completed! </p>
<p className="text-gray-700">
You&apos;ve successfully exploited the naive verifier! This demonstrates why proper
verification logic is critical.
</p>
</div>
)}
</div>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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&apos;ve completed.
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-bold text-blue-900 mb-3">Challenges:</h3>
<div className="space-y-4">
{/* Challenge 1 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[1] || completedChallenges.includes(1)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">
Challenge 1: Reveal USD Balance (Nested JSON)
</h4>
{(challengeResults[1] || completedChallenges.includes(1)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a handler to reveal the USD balance from the nested <code>accounts.USD</code>{' '}
field.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;RECV&apos;, part: &apos;BODY&apos;, action: &apos;REVEAL&apos;,
params: &#123; type: &apos;json&apos;, path: &apos;accounts.USD&apos; &#125;
&#125;
</code>
</div>
</div>
{/* Challenge 2 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[2] || completedChallenges.includes(2)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">
Challenge 2: Reveal Cookie Header (SENT)
</h4>
{(challengeResults[2] || completedChallenges.includes(2)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a SENT handler to reveal the Cookie header from the request.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;SENT&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos;, params: &#123; key: &apos;cookie&apos; &#125; &#125;
</code>
</div>
</div>
{/* Challenge 3 */}
<div
className={`p-4 rounded-lg border-2 ${
challengeResults[3] || completedChallenges.includes(3)
? 'bg-green-50 border-green-500'
: 'bg-white border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold text-gray-900">Challenge 3: Reveal Date Header (RECV)</h4>
{(challengeResults[3] || completedChallenges.includes(3)) && (
<span className="text-2xl"></span>
)}
</div>
<p className="text-sm text-gray-700 mb-2">
Add a RECV handler to reveal the Date header from the response.
</p>
<div className="text-xs text-gray-600 bg-gray-100 p-2 rounded">
<code>
&#123; type: &apos;RECV&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos;, params: &#123; key: &apos;date&apos; &#125; &#125;
</code>
</div>
</div>
</div>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
<h3 className="font-bold text-purple-900 mb-3">💡 Documentation & Tips:</h3>
<div className="space-y-3">
{/* Inspection Tip */}
<div className="bg-yellow-50 border border-yellow-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-1">💡 Pro Tip: Inspect First!</p>
<p className="text-xs mb-2">
Before targeting specific fields or headers, reveal everything to see what&apos;s
available:
</p>
<div className="bg-white p-2 rounded space-y-1">
<p className="text-xs font-mono">
&#123; type: &apos;RECV&apos;, part: &apos;BODY&apos;, action: &apos;REVEAL&apos;
&#125; // See all response body
</p>
<p className="text-xs font-mono">
&#123; type: &apos;SENT&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos; &#125; // See all request headers
</p>
<p className="text-xs font-mono">
&#123; type: &apos;RECV&apos;, part: &apos;HEADERS&apos;, action:
&apos;REVEAL&apos; &#125; // See all response headers
</p>
</div>
</div>
{/* Nested JSON Documentation */}
<div className="bg-white border border-gray-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-2">📚 Nested JSON Path Syntax:</p>
<p className="text-xs text-gray-700 mb-2">
Use dot notation to access nested fields in JSON objects:
</p>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-mono">
params: &#123; type: &apos;json&apos;, path: &apos;parent.child&apos; &#125;
</p>
</div>
</div>
{/* Header Key Documentation */}
<div className="bg-white border border-gray-300 rounded-lg p-3">
<p className="text-xs font-semibold mb-2">📚 Targeting Specific Headers:</p>
<p className="text-xs text-gray-700 mb-2">
Use <code>params.key</code> to precisely target a header (case-insensitive):
</p>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-mono">
params: &#123; key: &apos;header-name&apos; &#125;
</p>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold mb-4">Edit Plugin Code</h3>
<CodeEditor value={code} onChange={handleCodeChange} height="600px" />
</div>
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex gap-4 mb-4">
<Button onClick={handleTestCode} disabled={isExecuting} variant="primary">
{isExecuting ? 'Testing...' : 'Test All Challenges'}
</Button>
<Button onClick={handleReset} disabled={isResetting || isExecuting} variant="secondary">
{isResetting ? 'Resetting...' : 'Reset Code'}
</Button>
</div>
<ConsoleOutput result={result} />
</div>
{allChallengesComplete && !isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center mb-6">
<p className="text-xl font-bold text-green-900 mb-2">All Challenges Completed! </p>
<p className="text-gray-700 mb-4">
You&apos;ve successfully completed all advanced challenges!
</p>
<Button onClick={complete} variant="success">
Complete Step 5
</Button>
</div>
)}
{isCompleted && (
<div className="bg-green-50 border border-green-300 rounded-lg p-6 text-center">
<p className="text-xl font-bold text-green-900">Step 5 Completed! </p>
</div>
)}
</div>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.',
},
];
};

View File

@@ -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';
},
};

View File

@@ -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);
}
};

View File

@@ -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
];

View File

@@ -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: [],
};

View File

@@ -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" }]
}

View File

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

View File

@@ -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'],
},
},
},
},
});

View File

@@ -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,

View File

@@ -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!");