Compare commits

..

11 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
Hendrik Eeckhaut
af9148ee4a fix: removed old tlsn-js dependency (#224) 2026-01-20 09:39:36 +01:00
Hendrik Eeckhaut
6331f03940 feat(demo): show git hash in footer (#222) 2026-01-20 16:36:35 +08:00
tsukino
3b4554cd2a chore: update lock file and extension version (#223) 2026-01-20 16:29:22 +08:00
Hendrik Eeckhaut
367479fd77 feat: tlsn updated to 0.14 (#221)
* tlsn updated to 0.14

* package lock

* package lock
2026-01-19 17:24:32 +08:00
Hendrik Eeckhaut
d42db516c7 build: fix demo plugins build (#220) 2026-01-14 16:55:22 +01:00
Hendrik Eeckhaut
1b776929d8 Demo fix (#219)
build: Corrected verifier host + build cleanup (demo)
2026-01-14 14:30:52 +01:00
59 changed files with 1746 additions and 4068 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
@@ -86,5 +103,5 @@ jobs:
tags: ${{ steps.meta-verifier-webapp.outputs.tags }}
labels: ${{ steps.meta-verifier-webapp.outputs.labels }}
build-args: |
VERIFIER_HOST=demo-staging.tlsnotary.org
SSL=true
VITE_VERIFIER_HOST=demo-staging.tlsnotary.org
VITE_SSL=true

View File

@@ -591,37 +591,32 @@ 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
- `start.sh` - Setup script with URL templating
**Docker Services:**
1. `verifier` - TLSNotary verifier server (port 7047)
2. `demo-static` - nginx serving static plugin files
3. `nginx` - Reverse proxy (port 80)
**Environment Variables:**
- `VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
- `SSL` - Use https/wss protocols (default: `false`)
**Environment Variables (via `.env` files or Docker build args):**
- `VITE_VERIFIER_HOST` - Verifier server host (default: `localhost:7047`)
- `VITE_SSL` - Use https/wss protocols (default: `false`)
**Usage:**
```bash
# Local development
./start.sh
# Local development with npm
npm run demo
# Production with SSL
VERIFIER_HOST=verifier.tlsnotary.org SSL=true ./start.sh
# Docker (detached mode)
npm run docker:up
# Docker detached mode
./start.sh -d
# Docker with custom verifier
VITE_VERIFIER_HOST=verifier.example.com VITE_SSL=true docker compose up --build
```
The `start.sh` script:
1. Processes plugin files, replacing `verifierUrl` and `proxyUrl` placeholders
2. Copies processed files to `generated/` directory
3. Starts Docker Compose services
## Important Implementation Notes
### Plugin API Changes

View File

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

512
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "tlsn-monorepo",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tlsn-monorepo",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -4864,10 +4864,6 @@
"resolved": "packages/plugin-sdk",
"link": true
},
"node_modules/@tlsn/ts-plugin-sample": {
"resolved": "packages/ts-plugin-sample",
"link": true
},
"node_modules/@tlsnotary/demo": {
"resolved": "packages/demo",
"link": true
@@ -15660,20 +15656,6 @@
"node": ">=14.0.0"
}
},
"node_modules/tlsn-js": {
"version": "0.1.0-alpha.12.0",
"license": "ISC",
"dependencies": {
"tlsn-wasm": "0.1.0-alpha.12"
},
"engines": {
"node": ">= 16.20.2"
}
},
"node_modules/tlsn-js/node_modules/tlsn-wasm": {
"version": "0.1.0-alpha.12",
"license": "MIT OR Apache-2.0"
},
"node_modules/tlsn-wasm": {
"resolved": "packages/tlsn-wasm-pkg",
"link": true
@@ -17416,7 +17398,7 @@
}
},
"packages/extension": {
"version": "0.1.0.1300",
"version": "0.1.0.1400",
"license": "MIT",
"dependencies": {
"@codemirror/lang-javascript": "^6.2.4",
@@ -17446,7 +17428,6 @@
"redux-thunk": "^2.4.2",
"stream-browserify": "^3.0.0",
"tailwindcss": "^3.3.3",
"tlsn-js": "^0.1.0-alpha.12.0",
"tlsn-wasm": "./lib/tlsn-wasm-pkg/",
"util": "^0.12.5"
},
@@ -17513,10 +17494,13 @@
},
"packages/extension/lib/tlsn-wasm-pkg": {
"name": "tlsn-wasm",
"version": "0.1.0-alpha.13",
"extraneous": true,
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0"
},
"packages/extension/node_modules/tlsn-wasm": {
"resolved": "packages/extension/lib/tlsn-wasm-pkg",
"link": true
},
"packages/plugin-sdk": {
"name": "@tlsn/plugin-sdk",
"version": "0.1.0",
@@ -17555,486 +17539,8 @@
},
"packages/tlsn-wasm-pkg": {
"name": "tlsn-wasm",
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.14",
"license": "MIT OR Apache-2.0"
},
"packages/ts-plugin-sample": {
"name": "@tlsn/ts-plugin-sample",
"version": "0.1.0-alpha.13",
"license": "MIT",
"dependencies": {
"@tlsn/plugin-sdk": "*"
},
"devDependencies": {
"esbuild": "^0.24.2",
"typescript": "^5.5.4"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"packages/ts-plugin-sample/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,14 @@ 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';
const SSL = process.env.VITE_SSL === 'true';
const VERIFIER_URL = `${SSL ? 'https' : 'http'}://${VERIFIER_HOST}`;
const PROXY_URL = `${SSL ? 'wss' : 'ws'}://${VERIFIER_HOST}/proxy?token=`;
// Build each plugin separately as plain ES module
for (const plugin of plugins) {
@@ -28,12 +35,8 @@ for (const plugin of plugins) {
},
},
define: {
VITE_VERIFIER_URL: JSON.stringify(
process.env.VITE_VERIFIER_URL || 'http://localhost:7047'
),
VITE_PROXY_URL: JSON.stringify(
process.env.VITE_PROXY_URL || 'ws://localhost:7047/proxy?token='
),
VITE_VERIFIER_URL: JSON.stringify(VERIFIER_URL),
VITE_PROXY_URL: JSON.stringify(PROXY_URL),
},
});
console.log(`✓ Built ${plugin}.js`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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