From 68d4145cd7fd9ea59bce056d9656db498fa6e3f2 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 22:23:53 +0200 Subject: [PATCH 1/8] feat: add Cloudflare Worker SDK example --- .dev.vars.example | 4 + .github/workflows/ci.yml | 28 ++ .npmrc | 5 + CONTRIBUTING.md | 13 + README.md | 17 +- SECURITY.md | 5 + package-lock.json | 607 +++++++++++++++++++++++++++++++++++++++ package.json | 28 ++ scripts/simulate.ts | 24 ++ scripts/verify-sdk.ts | 64 +++++ src/http.ts | 21 ++ src/index.ts | 30 ++ src/sdk.ts | 151 ++++++++++ tsconfig.json | 13 + wrangler.jsonc | 16 ++ 15 files changed, 1024 insertions(+), 2 deletions(-) create mode 100644 .dev.vars.example create mode 100644 .github/workflows/ci.yml create mode 100644 .npmrc create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/simulate.ts create mode 100644 scripts/verify-sdk.ts create mode 100644 src/http.ts create mode 100644 src/index.ts create mode 100644 src/sdk.ts create mode 100644 tsconfig.json create mode 100644 wrangler.jsonc diff --git a/.dev.vars.example b/.dev.vars.example new file mode 100644 index 0000000..613fc80 --- /dev/null +++ b/.dev.vars.example @@ -0,0 +1,4 @@ +INVISIBLE_COORDINATOR_WS_URL=wss://coordinator.example/ws-noise +INVISIBLE_REQUIRED_MODE=dev +INVISIBLE_RELEASE_MRTD=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +DEMO_DESTINATION_ADDRESS=11111111111111111111111111111111 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..342f9a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run build + - run: npm run simulate + - run: npm run verify:sdk + - run: npm run audit:high diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..95e4910 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +@invisible:registry=https://npm.pkg.github.com +@invisible-labs:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NPM_TOKEN} +save-exact=true +ignore-scripts=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f37758e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Keep this Worker example small and copy-pasteable. + +```bash +npm ci +npm run typecheck +npm run build +npm run simulate +npm run audit:high +``` + +Do not commit secrets, wallet private keys, recovery codes, FROST material, nonces, signatures, or real user data. diff --git a/README.md b/README.md index 6ac306c..169deaf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ # Invisible Worker -Cloudflare Worker server example for Invisible private transfers. +Cloudflare Worker example for server-side Invisible private transfers. -Implementation lives in pull requests. +```bash +npm ci +npm run dev +``` + +Routes: + +```txt +GET /health +GET /sdk +POST /private-transfer +``` + +Set secrets with Wrangler. Do not commit secrets. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e269dfb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security + +Report security issues privately to the maintainers. + +This example must never log or persist coordinator secrets, wallet private keys, recovery codes, FROST shares, nonces, signatures, or raw decrypted payloads. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..24f9c18 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,607 @@ +{ + "name": "invisible-worker", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "invisible-worker", + "version": "0.1.0", + "devDependencies": { + "@cloudflare/workers-types": "4.20260612.1", + "@types/node": "24.10.3", + "tsx": "4.21.0", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=24.0.0" + }, + "optionalDependencies": { + "@invisible/sdk": "0.0.0" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260612.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260612.1.tgz", + "integrity": "sha512-PMQI7XP/wrMhxyjseUHoHj6XFqkHaf4utWQ/hhefVY8oMK2LJ730oeQ7H/nZSVMexZe39DzsdOx7sf1PqMr7+Q==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@invisible/sdk": { + "optional": true + }, + "node_modules/@types/node": { + "version": "24.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", + "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9817623 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "invisible-worker", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "dev": "npx wrangler dev", + "deploy": "npx wrangler deploy", + "typecheck": "tsc --noEmit", + "build": "tsc --noEmit", + "simulate": "tsx scripts/simulate.ts", + "verify:sdk": "tsx scripts/verify-sdk.ts", + "cf-typegen": "npx wrangler types", + "audit:high": "npm audit --audit-level=high" + }, + "optionalDependencies": { + "@invisible/sdk": "0.0.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "4.20260612.1", + "@types/node": "24.10.3", + "tsx": "4.21.0", + "typescript": "5.9.3" + } +} diff --git a/scripts/simulate.ts b/scripts/simulate.ts new file mode 100644 index 0000000..b142982 --- /dev/null +++ b/scripts/simulate.ts @@ -0,0 +1,24 @@ +import worker from "../src/index.js"; +import type { WorkerEnv } from "../src/sdk.js"; + +const env: WorkerEnv = { + INVISIBLE_COORDINATOR_WS_URL: "wss://coordinator.example/ws-noise", + INVISIBLE_REQUIRED_MODE: "dev", + INVISIBLE_RELEASE_MRTD: + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + DEMO_DESTINATION_ADDRESS: "11111111111111111111111111111111", +}; + +const health = await worker.fetch(new Request("https://worker.example/health"), env); +if (health.status !== 200) throw new Error("health route failed"); + +const transfer = await worker.fetch( + new Request("https://worker.example/private-transfer", { + method: "POST", + body: JSON.stringify({ amountLamports: 1000 }), + }), + env, +); +if (transfer.status !== 200) throw new Error("private transfer route failed"); + +console.log("worker simulation ok"); diff --git a/scripts/verify-sdk.ts b/scripts/verify-sdk.ts new file mode 100644 index 0000000..b664501 --- /dev/null +++ b/scripts/verify-sdk.ts @@ -0,0 +1,64 @@ +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; + +const packageSpec = process.env.INVISIBLE_SDK_PACKAGE ?? "latest"; +const required = process.env.VERIFY_SDK_REQUIRED === "1"; +const workspace = join(tmpdir(), `invisible-sdk-verify-${process.pid}`); + +await mkdir(workspace, { recursive: true }); + +try { + await writeFile( + join(workspace, "package.json"), + JSON.stringify({ type: "module", dependencies: { "@invisible/sdk": packageSpec } }, null, 2), + ); + await writeFile( + join(workspace, ".npmrc"), + [ + "@invisible:registry=https://npm.pkg.github.com", + "@invisible-labs:registry=https://npm.pkg.github.com", + "//npm.pkg.github.com/:_authToken=${NPM_TOKEN}", + "ignore-scripts=true", + "", + ].join("\n"), + ); + + const install = spawnSync("npm", ["install", "--omit=dev"], { + cwd: workspace, + encoding: "utf8", + stdio: "pipe", + }); + if (install.status !== 0) { + if (required) { + process.stderr.write(install.stderr); + process.exit(install.status ?? 1); + } + skipUnavailable(); + } + + const verify = spawnSync( + process.execPath, + [ + "--input-type=module", + "--eval", + "await import('@invisible/sdk'); await import('@invisible/sdk/user'); await import('@invisible/sdk/storage'); console.log('sdk imports ok')", + ], + { cwd: workspace, encoding: "utf8", stdio: "pipe" }, + ); + if (verify.status !== 0) { + if (!required) skipUnavailable(); + process.stderr.write(verify.stderr); + process.exit(verify.status ?? 1); + } + process.stdout.write(verify.stdout); +} finally { + await rm(workspace, { force: true, recursive: true }); +} + +function skipUnavailable(): never { + console.log("sdk verification skipped: package is not available yet"); + process.exit(0); + throw new Error("unreachable"); +} diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..75bccad --- /dev/null +++ b/src/http.ts @@ -0,0 +1,21 @@ +export function json(data: unknown, init: ResponseInit = {}): Response { + return new Response(JSON.stringify(data), { + ...init, + headers: { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + ...init.headers, + }, + }); +} + +export async function readJsonBody(request: Request, maxBytes = 4096): Promise { + const contentLength = request.headers.get("content-length"); + if (contentLength && Number(contentLength) > maxBytes) { + throw new Error("Request body is too large."); + } + + const body = await request.text(); + if (body.length > maxBytes) throw new Error("Request body is too large."); + return JSON.parse(body) as T; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3ea9204 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,30 @@ +import { json, readJsonBody } from "./http.js"; +import { sdkStatus, startPrivateTransfer, type PrivateTransferRequest, type WorkerEnv } from "./sdk.js"; + +export default { + async fetch(request: Request, env: WorkerEnv): Promise { + const url = new URL(request.url); + + try { + if (request.method === "GET" && url.pathname === "/health") { + return json({ ok: true }); + } + + if (request.method === "GET" && url.pathname === "/sdk") { + return json(await sdkStatus()); + } + + if (request.method === "POST" && url.pathname === "/private-transfer") { + const input = await readJsonBody(request); + return json(await startPrivateTransfer(env, input)); + } + + return json({ error: "not_found" }, { status: 404 }); + } catch (error) { + return json( + { error: error instanceof Error ? error.message : "request failed" }, + { status: 400 }, + ); + } + }, +}; diff --git a/src/sdk.ts b/src/sdk.ts new file mode 100644 index 0000000..3c31c0f --- /dev/null +++ b/src/sdk.ts @@ -0,0 +1,151 @@ +type SdkSession = { readonly attested?: boolean }; +type SdkRoot = { + createSession(options: { coordinator: CoordinatorPoolConfig; storage?: unknown }): Promise; + normalizeError?(error: unknown, fallback?: string): string; +}; +type SdkUser = { + contractRequest( + session: SdkSession, + args: { + amountLamports: number; + payoutPolicy: PayoutPolicy; + sync?: boolean; + }, + ): Promise; +}; +type SdkStorage = { inMemoryStorage?(): unknown }; + +export type WorkerEnv = { + INVISIBLE_COORDINATOR_WS_URL: string; + INVISIBLE_REQUIRED_MODE: "dev" | "prod" | "auto"; + INVISIBLE_RELEASE_MRTD: string; + DEMO_DESTINATION_ADDRESS: string; +}; + +export type CoordinatorPoolConfig = { + endpoints: Array<{ + wsUrl: string; + expectedHostname: string; + requiredMode: "dev" | "prod" | "auto"; + releasePin: { mrtd: string }; + }>; + allowedRoles?: string[]; + preferLeader?: boolean; +}; + +export type PayoutPolicy = { + destinations: Array<{ + address: string; + sharePercent: number; + }>; +}; + +export type AcceptanceReceipt = { + accepted?: boolean; + requestId?: string; + request_id?: string; +}; + +export type PrivateTransferRequest = { + amountLamports: number; + destinationAddress?: string; +}; + +export async function sdkStatus(): Promise<{ packageAvailable: boolean }> { + return { packageAvailable: (await loadSdk()) !== null }; +} + +export async function startPrivateTransfer(env: WorkerEnv, input: PrivateTransferRequest) { + const sdk = await loadSdk(); + if (!sdk) { + return { + status: "sdk_missing", + message: "Install @invisible/sdk from the private package registry before running this Worker.", + }; + } + + try { + const session = await sdk.root.createSession({ + coordinator: buildCoordinatorPool(env), + storage: sdk.storage.inMemoryStorage?.(), + }); + const receipt = await sdk.user.contractRequest(session, { + amountLamports: assertLamports(input.amountLamports), + payoutPolicy: singleDestinationPolicy(input.destinationAddress ?? env.DEMO_DESTINATION_ADDRESS), + sync: true, + }); + + return { + status: "accepted", + requestId: receipt.requestId ?? receipt.request_id ?? "accepted", + }; + } catch (error) { + const message = normalizeSdkError(sdk.root, error); + if (message.includes("NOT_ATTESTED") || message.includes("not attested")) { + return { + status: "sdk_not_ready", + message: "SDK connected, but this package build has not completed coordinator attestation yet.", + }; + } + if (message.includes("NOT_IMPLEMENTED") || message.includes("not implemented")) { + return { + status: "sdk_not_ready", + message: "SDK package is installed, but this command is still preview-only in the current build.", + }; + } + return { status: "failed", message }; + } +} + +export function buildCoordinatorPool(env: WorkerEnv): CoordinatorPoolConfig { + const endpoint = new URL(env.INVISIBLE_COORDINATOR_WS_URL); + if (endpoint.protocol !== "wss:" && endpoint.protocol !== "ws:") { + throw new Error("INVISIBLE_COORDINATOR_WS_URL must use ws:// or wss://"); + } + + return { + endpoints: [ + { + wsUrl: env.INVISIBLE_COORDINATOR_WS_URL, + expectedHostname: endpoint.hostname, + requiredMode: env.INVISIBLE_REQUIRED_MODE, + releasePin: { mrtd: env.INVISIBLE_RELEASE_MRTD }, + }, + ], + allowedRoles: ["leader"], + preferLeader: true, + }; +} + +function singleDestinationPolicy(destinationAddress: string): PayoutPolicy { + return { + destinations: [{ address: destinationAddress.trim(), sharePercent: 100 }], + }; +} + +function assertLamports(value: number): number { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error("amountLamports must be a positive safe integer."); + } + return value; +} + +async function loadSdk(): Promise<{ root: SdkRoot; user: SdkUser; storage: SdkStorage } | null> { + try { + const scope = "@invisible"; + const [root, user, storage] = await Promise.all([ + import(`${scope}/sdk`) as Promise, + import(`${scope}/sdk/user`) as Promise, + import(`${scope}/sdk/storage`) as Promise, + ]); + return { root, user, storage }; + } catch { + return null; + } +} + +function normalizeSdkError(sdk: SdkRoot, error: unknown): string { + if (sdk.normalizeError) return sdk.normalizeError(error, "Invisible transfer failed."); + if (error instanceof Error) return error.message; + return "Invisible transfer failed."; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..329ea09 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "types": ["@cloudflare/workers-types", "node"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "scripts/**/*.ts", "worker-configuration.d.ts"] +} diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..dab47af --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,16 @@ +{ + "name": "invisible-worker", + "main": "src/index.ts", + "compatibility_date": "2026-06-19", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true, + "head_sampling_rate": 1 + }, + "vars": { + "INVISIBLE_COORDINATOR_WS_URL": "wss://coordinator.example/ws-noise", + "INVISIBLE_REQUIRED_MODE": "dev", + "INVISIBLE_RELEASE_MRTD": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "DEMO_DESTINATION_ADDRESS": "11111111111111111111111111111111" + } +} From ea2c22f27c8f97547daf906cc3493bfaddb83261 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 22:27:56 +0200 Subject: [PATCH 2/8] fix: generate Worker SDK bundle imports --- README.md | 8 ++++++++ package.json | 1 + scripts/prepare-sdk-imports.ts | 37 ++++++++++++++++++++++++++++++++++ src/sdk-imports.generated.ts | 5 +++++ src/sdk-types.ts | 31 ++++++++++++++++++++++++++++ src/sdk.ts | 32 ++++------------------------- 6 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 scripts/prepare-sdk-imports.ts create mode 100644 src/sdk-imports.generated.ts create mode 100644 src/sdk-types.ts diff --git a/README.md b/README.md index 169deaf..b1afd05 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,11 @@ POST /private-transfer ``` Set secrets with Wrangler. Do not commit secrets. + +When `@invisible/sdk` is available: + +```bash +INVISIBLE_SDK_PACKAGE=latest npm run verify:sdk +npm run prepare:sdk +npm run deploy +``` diff --git a/package.json b/package.json index 9817623..b2aa4e1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "typecheck": "tsc --noEmit", "build": "tsc --noEmit", "simulate": "tsx scripts/simulate.ts", + "prepare:sdk": "tsx scripts/prepare-sdk-imports.ts", "verify:sdk": "tsx scripts/verify-sdk.ts", "cf-typegen": "npx wrangler types", "audit:high": "npm audit --audit-level=high" diff --git a/scripts/prepare-sdk-imports.ts b/scripts/prepare-sdk-imports.ts new file mode 100644 index 0000000..289499a --- /dev/null +++ b/scripts/prepare-sdk-imports.ts @@ -0,0 +1,37 @@ +import { access, writeFile } from "node:fs/promises"; + +const output = new URL("../src/sdk-imports.generated.ts", import.meta.url); + +try { + await access(new URL("../node_modules/@invisible/sdk/package.json", import.meta.url)); + await writeGeneratedLoader(true); + console.log("generated static @invisible/sdk imports"); +} catch { + await writeGeneratedLoader(false); + console.log("generated SDK-missing stub"); +} + +async function writeGeneratedLoader(usePackage: boolean): Promise { + const source = usePackage + ? [ + 'import * as root from "@invisible/sdk";', + 'import * as user from "@invisible/sdk/user";', + 'import * as storage from "@invisible/sdk/storage";', + 'import type { SdkBundle } from "./sdk-types.js";', + "", + "export async function loadSdkBundle(): Promise {", + " return { root, user, storage };", + "}", + "", + ] + : [ + 'import type { SdkBundle } from "./sdk-types.js";', + "", + "export async function loadSdkBundle(): Promise {", + " return null;", + "}", + "", + ]; + + await writeFile(output, source.join("\n")); +} diff --git a/src/sdk-imports.generated.ts b/src/sdk-imports.generated.ts new file mode 100644 index 0000000..630d908 --- /dev/null +++ b/src/sdk-imports.generated.ts @@ -0,0 +1,5 @@ +import type { SdkBundle } from "./sdk-types.js"; + +export async function loadSdkBundle(): Promise { + return null; +} diff --git a/src/sdk-types.ts b/src/sdk-types.ts new file mode 100644 index 0000000..d6b2f9c --- /dev/null +++ b/src/sdk-types.ts @@ -0,0 +1,31 @@ +export type SdkSession = { readonly attested?: boolean }; + +export type SdkRoot = { + createSession(options: { coordinator: unknown; storage?: unknown }): Promise; + normalizeError?(error: unknown, fallback?: string): string; +}; + +export type SdkUser = { + contractRequest( + session: SdkSession, + args: { + amountLamports: number; + payoutPolicy: unknown; + sync?: boolean; + }, + ): Promise<{ + accepted?: boolean; + requestId?: string; + request_id?: string; + }>; +}; + +export type SdkStorage = { + inMemoryStorage?(): unknown; +}; + +export type SdkBundle = { + root: SdkRoot; + user: SdkUser; + storage: SdkStorage; +}; diff --git a/src/sdk.ts b/src/sdk.ts index 3c31c0f..b92caa8 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,19 +1,5 @@ -type SdkSession = { readonly attested?: boolean }; -type SdkRoot = { - createSession(options: { coordinator: CoordinatorPoolConfig; storage?: unknown }): Promise; - normalizeError?(error: unknown, fallback?: string): string; -}; -type SdkUser = { - contractRequest( - session: SdkSession, - args: { - amountLamports: number; - payoutPolicy: PayoutPolicy; - sync?: boolean; - }, - ): Promise; -}; -type SdkStorage = { inMemoryStorage?(): unknown }; +import { loadSdkBundle } from "./sdk-imports.generated.js"; +import type { SdkRoot } from "./sdk-types.js"; export type WorkerEnv = { INVISIBLE_COORDINATOR_WS_URL: string; @@ -130,18 +116,8 @@ function assertLamports(value: number): number { return value; } -async function loadSdk(): Promise<{ root: SdkRoot; user: SdkUser; storage: SdkStorage } | null> { - try { - const scope = "@invisible"; - const [root, user, storage] = await Promise.all([ - import(`${scope}/sdk`) as Promise, - import(`${scope}/sdk/user`) as Promise, - import(`${scope}/sdk/storage`) as Promise, - ]); - return { root, user, storage }; - } catch { - return null; - } +async function loadSdk() { + return loadSdkBundle(); } function normalizeSdkError(sdk: SdkRoot, error: unknown): string { From 47bffdb8d740678638161b55a476e2a13b992760 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 22:33:30 +0200 Subject: [PATCH 3/8] fix: protect Worker transfer endpoint --- .dev.vars.example | 2 ++ .github/workflows/ci.yml | 1 + README.md | 11 +++++++++ package.json | 1 + scripts/check-contract.ts | 19 +++++++++++++++ scripts/simulate.ts | 25 +++++++++++++++++++- src/http.ts | 42 +++++++++++++++++++++++++++++--- src/index.ts | 50 +++++++++++++++++++++++++++++++++++++-- src/sdk.ts | 9 +++++-- wrangler.jsonc | 1 + 10 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 scripts/check-contract.ts diff --git a/.dev.vars.example b/.dev.vars.example index 613fc80..a0c2e48 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1,4 +1,6 @@ INVISIBLE_COORDINATOR_WS_URL=wss://coordinator.example/ws-noise INVISIBLE_REQUIRED_MODE=dev INVISIBLE_RELEASE_MRTD=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +INVISIBLE_INTEL_ROOT_FINGERPRINT=0000000000000000000000000000000000000000000000000000000000000000 DEMO_DESTINATION_ADDRESS=11111111111111111111111111111111 +INVISIBLE_WORKER_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 342f9a0..1e68201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - run: npm ci - run: npm run typecheck - run: npm run build + - run: npm run check:contract - run: npm run simulate - run: npm run verify:sdk - run: npm run audit:high diff --git a/README.md b/README.md index b1afd05..71369dd 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,17 @@ POST /private-transfer Set secrets with Wrangler. Do not commit secrets. +Required runtime config: + +```txt +INVISIBLE_COORDINATOR_WS_URL= +INVISIBLE_RELEASE_MRTD= +INVISIBLE_INTEL_ROOT_FINGERPRINT= +INVISIBLE_WORKER_API_KEY= +``` + +`POST /private-transfer` requires `Authorization: Bearer `. + When `@invisible/sdk` is available: ```bash diff --git a/package.json b/package.json index b2aa4e1..a19681a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "typecheck": "tsc --noEmit", "build": "tsc --noEmit", "simulate": "tsx scripts/simulate.ts", + "check:contract": "tsx scripts/check-contract.ts", "prepare:sdk": "tsx scripts/prepare-sdk-imports.ts", "verify:sdk": "tsx scripts/verify-sdk.ts", "cf-typegen": "npx wrangler types", diff --git a/scripts/check-contract.ts b/scripts/check-contract.ts new file mode 100644 index 0000000..ba439cb --- /dev/null +++ b/scripts/check-contract.ts @@ -0,0 +1,19 @@ +import { buildCoordinatorPool, type WorkerEnv } from "../src/sdk.js"; + +const env: WorkerEnv = { + INVISIBLE_COORDINATOR_WS_URL: "wss://coordinator.example/ws-noise", + INVISIBLE_REQUIRED_MODE: "dev", + INVISIBLE_RELEASE_MRTD: + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + INVISIBLE_INTEL_ROOT_FINGERPRINT: + "0000000000000000000000000000000000000000000000000000000000000000", + DEMO_DESTINATION_ADDRESS: "11111111111111111111111111111111", +}; + +const pool = buildCoordinatorPool(env); +const pin = pool.endpoints[0]?.releasePin; +if (!pin?.mrtd || !pin.intelRootFingerprint) { + throw new Error("coordinator release pin must include mrtd and intelRootFingerprint"); +} + +console.log("worker contract ok"); diff --git a/scripts/simulate.ts b/scripts/simulate.ts index b142982..8e7b13d 100644 --- a/scripts/simulate.ts +++ b/scripts/simulate.ts @@ -6,7 +6,10 @@ const env: WorkerEnv = { INVISIBLE_REQUIRED_MODE: "dev", INVISIBLE_RELEASE_MRTD: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + INVISIBLE_INTEL_ROOT_FINGERPRINT: + "0000000000000000000000000000000000000000000000000000000000000000", DEMO_DESTINATION_ADDRESS: "11111111111111111111111111111111", + INVISIBLE_WORKER_API_KEY: "test-key", }; const health = await worker.fetch(new Request("https://worker.example/health"), env); @@ -15,10 +18,30 @@ if (health.status !== 200) throw new Error("health route failed"); const transfer = await worker.fetch( new Request("https://worker.example/private-transfer", { method: "POST", + headers: { authorization: "Bearer test-key" }, body: JSON.stringify({ amountLamports: 1000 }), }), env, ); -if (transfer.status !== 200) throw new Error("private transfer route failed"); +if (transfer.status !== 503) throw new Error("private transfer should fail closed without SDK"); + +const unauthorized = await worker.fetch( + new Request("https://worker.example/private-transfer", { + method: "POST", + body: JSON.stringify({ amountLamports: 1000 }), + }), + env, +); +if (unauthorized.status !== 401) throw new Error("private transfer route must require auth"); + +const tooLarge = await worker.fetch( + new Request("https://worker.example/private-transfer", { + method: "POST", + headers: { authorization: "Bearer test-key" }, + body: JSON.stringify({ amountLamports: 1000, memo: "x".repeat(5000) }), + }), + env, +); +if (tooLarge.status !== 413) throw new Error("private transfer route must enforce body limits"); console.log("worker simulation ok"); diff --git a/src/http.ts b/src/http.ts index 75bccad..5807cca 100644 --- a/src/http.ts +++ b/src/http.ts @@ -9,13 +9,49 @@ export function json(data: unknown, init: ResponseInit = {}): Response { }); } +export class HttpError extends Error { + constructor( + readonly status: number, + message: string, + ) { + super(message); + this.name = "HttpError"; + } +} + export async function readJsonBody(request: Request, maxBytes = 4096): Promise { const contentLength = request.headers.get("content-length"); if (contentLength && Number(contentLength) > maxBytes) { - throw new Error("Request body is too large."); + throw new HttpError(413, "Request body is too large."); + } + + if (!request.body) { + throw new HttpError(400, "Request body is required."); + } + + const reader = request.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maxBytes) { + await reader.cancel("request body too large"); + throw new HttpError(413, "Request body is too large."); + } + chunks.push(value); + } + + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; } - const body = await request.text(); - if (body.length > maxBytes) throw new Error("Request body is too large."); + const body = new TextDecoder().decode(bytes); + if (!body.trim()) throw new HttpError(400, "Request body is required."); return JSON.parse(body) as T; } diff --git a/src/index.ts b/src/index.ts index 3ea9204..8c894e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { json, readJsonBody } from "./http.js"; +import { HttpError, json, readJsonBody } from "./http.js"; import { sdkStatus, startPrivateTransfer, type PrivateTransferRequest, type WorkerEnv } from "./sdk.js"; export default { @@ -15,12 +15,17 @@ export default { } if (request.method === "POST" && url.pathname === "/private-transfer") { + await requireBearerToken(request, env); const input = await readJsonBody(request); - return json(await startPrivateTransfer(env, input)); + const result = await startPrivateTransfer(env, input); + return json(result, { status: statusForTransfer(result.status) }); } return json({ error: "not_found" }, { status: 404 }); } catch (error) { + if (error instanceof HttpError) { + return json({ error: error.message }, { status: error.status }); + } return json( { error: error instanceof Error ? error.message : "request failed" }, { status: 400 }, @@ -28,3 +33,44 @@ export default { } }, }; + +async function requireBearerToken(request: Request, env: WorkerEnv): Promise { + const expected = env.INVISIBLE_WORKER_API_KEY?.trim(); + if (!expected) throw new HttpError(503, "Worker API key is not configured."); + + const header = request.headers.get("authorization") ?? ""; + const provided = header.startsWith("Bearer ") ? header.slice("Bearer ".length).trim() : ""; + if (!provided || !(await timingSafeEqual(provided, expected))) { + throw new HttpError(401, "Unauthorized."); + } +} + +async function timingSafeEqual(a: string, b: string): Promise { + const encoder = new TextEncoder(); + const [left, right] = await Promise.all([ + crypto.subtle.digest("SHA-256", encoder.encode(a)), + crypto.subtle.digest("SHA-256", encoder.encode(b)), + ]); + const leftBytes = new Uint8Array(left); + const rightBytes = new Uint8Array(right); + let diff = leftBytes.length ^ rightBytes.length; + for (let i = 0; i < Math.max(leftBytes.length, rightBytes.length); i += 1) { + diff |= (leftBytes[i] ?? 0) ^ (rightBytes[i] ?? 0); + } + return diff === 0; +} + +function statusForTransfer(status: string): number { + switch (status) { + case "accepted": + return 202; + case "sdk_missing": + return 503; + case "sdk_not_ready": + return 501; + case "failed": + return 502; + default: + return 500; + } +} diff --git a/src/sdk.ts b/src/sdk.ts index b92caa8..09ba1d6 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -5,7 +5,9 @@ export type WorkerEnv = { INVISIBLE_COORDINATOR_WS_URL: string; INVISIBLE_REQUIRED_MODE: "dev" | "prod" | "auto"; INVISIBLE_RELEASE_MRTD: string; + INVISIBLE_INTEL_ROOT_FINGERPRINT: string; DEMO_DESTINATION_ADDRESS: string; + INVISIBLE_WORKER_API_KEY?: string; }; export type CoordinatorPoolConfig = { @@ -13,7 +15,7 @@ export type CoordinatorPoolConfig = { wsUrl: string; expectedHostname: string; requiredMode: "dev" | "prod" | "auto"; - releasePin: { mrtd: string }; + releasePin: { mrtd: string; intelRootFingerprint: string }; }>; allowedRoles?: string[]; preferLeader?: boolean; @@ -95,7 +97,10 @@ export function buildCoordinatorPool(env: WorkerEnv): CoordinatorPoolConfig { wsUrl: env.INVISIBLE_COORDINATOR_WS_URL, expectedHostname: endpoint.hostname, requiredMode: env.INVISIBLE_REQUIRED_MODE, - releasePin: { mrtd: env.INVISIBLE_RELEASE_MRTD }, + releasePin: { + mrtd: env.INVISIBLE_RELEASE_MRTD, + intelRootFingerprint: env.INVISIBLE_INTEL_ROOT_FINGERPRINT, + }, }, ], allowedRoles: ["leader"], diff --git a/wrangler.jsonc b/wrangler.jsonc index dab47af..282e5e9 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -11,6 +11,7 @@ "INVISIBLE_COORDINATOR_WS_URL": "wss://coordinator.example/ws-noise", "INVISIBLE_REQUIRED_MODE": "dev", "INVISIBLE_RELEASE_MRTD": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "INVISIBLE_INTEL_ROOT_FINGERPRINT": "0000000000000000000000000000000000000000000000000000000000000000", "DEMO_DESTINATION_ADDRESS": "11111111111111111111111111111111" } } From 81ead8cddf70283ffa4f5021400c459316fa2644 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 22:40:44 +0200 Subject: [PATCH 4/8] fix: validate Worker attestation mode --- scripts/check-contract.ts | 9 +++++++++ scripts/verify-sdk.ts | 5 ++++- src/sdk.ts | 9 +++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/scripts/check-contract.ts b/scripts/check-contract.ts index ba439cb..acd45fb 100644 --- a/scripts/check-contract.ts +++ b/scripts/check-contract.ts @@ -16,4 +16,13 @@ if (!pin?.mrtd || !pin.intelRootFingerprint) { throw new Error("coordinator release pin must include mrtd and intelRootFingerprint"); } +try { + buildCoordinatorPool({ ...env, INVISIBLE_REQUIRED_MODE: "" }); + throw new Error("attestation mode must fail closed"); +} catch (error) { + if (!(error instanceof Error) || !error.message.includes("INVISIBLE_REQUIRED_MODE")) { + throw error; + } +} + console.log("worker contract ok"); diff --git a/scripts/verify-sdk.ts b/scripts/verify-sdk.ts index b664501..6fee1e2 100644 --- a/scripts/verify-sdk.ts +++ b/scripts/verify-sdk.ts @@ -4,7 +4,10 @@ import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; const packageSpec = process.env.INVISIBLE_SDK_PACKAGE ?? "latest"; -const required = process.env.VERIFY_SDK_REQUIRED === "1"; +const required = + process.env.VERIFY_SDK_REQUIRED === "1" || + Boolean(process.env.INVISIBLE_SDK_PACKAGE) || + Boolean(process.env.NPM_TOKEN); const workspace = join(tmpdir(), `invisible-sdk-verify-${process.pid}`); await mkdir(workspace, { recursive: true }); diff --git a/src/sdk.ts b/src/sdk.ts index 09ba1d6..0f72540 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -3,7 +3,7 @@ import type { SdkRoot } from "./sdk-types.js"; export type WorkerEnv = { INVISIBLE_COORDINATOR_WS_URL: string; - INVISIBLE_REQUIRED_MODE: "dev" | "prod" | "auto"; + INVISIBLE_REQUIRED_MODE: string; INVISIBLE_RELEASE_MRTD: string; INVISIBLE_INTEL_ROOT_FINGERPRINT: string; DEMO_DESTINATION_ADDRESS: string; @@ -96,7 +96,7 @@ export function buildCoordinatorPool(env: WorkerEnv): CoordinatorPoolConfig { { wsUrl: env.INVISIBLE_COORDINATOR_WS_URL, expectedHostname: endpoint.hostname, - requiredMode: env.INVISIBLE_REQUIRED_MODE, + requiredMode: readRequiredMode(env.INVISIBLE_REQUIRED_MODE), releasePin: { mrtd: env.INVISIBLE_RELEASE_MRTD, intelRootFingerprint: env.INVISIBLE_INTEL_ROOT_FINGERPRINT, @@ -121,6 +121,11 @@ function assertLamports(value: number): number { return value; } +function readRequiredMode(value: string): "dev" | "prod" | "auto" { + if (value === "dev" || value === "prod" || value === "auto") return value; + throw new Error("INVISIBLE_REQUIRED_MODE must be dev, prod, or auto"); +} + async function loadSdk() { return loadSdkBundle(); } From 68f580153acb05333ac2c43e107d9153021b620a Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 23:02:12 +0200 Subject: [PATCH 5/8] fix: gate legacy dcap collateral mode --- .dev.vars.example | 1 + README.md | 2 ++ scripts/check-contract.ts | 24 ++++++++++++++++++++++++ src/sdk.ts | 23 +++++++++++++++++++++-- wrangler.jsonc | 1 + 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.dev.vars.example b/.dev.vars.example index a0c2e48..10ea9d0 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -2,5 +2,6 @@ INVISIBLE_COORDINATOR_WS_URL=wss://coordinator.example/ws-noise INVISIBLE_REQUIRED_MODE=dev INVISIBLE_RELEASE_MRTD=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 INVISIBLE_INTEL_ROOT_FINGERPRINT=0000000000000000000000000000000000000000000000000000000000000000 +INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL=false DEMO_DESTINATION_ADDRESS=11111111111111111111111111111111 INVISIBLE_WORKER_API_KEY= diff --git a/README.md b/README.md index 71369dd..35f82e8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ INVISIBLE_INTEL_ROOT_FINGERPRINT= INVISIBLE_WORKER_API_KEY= ``` +The SDK requires DCAP collateral by default. For a legacy non-production coordinator that does not emit collateral yet, set `INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL=true`. The example rejects that flag in `prod` mode. + `POST /private-transfer` requires `Authorization: Bearer `. When `@invisible/sdk` is available: diff --git a/scripts/check-contract.ts b/scripts/check-contract.ts index acd45fb..f1767e9 100644 --- a/scripts/check-contract.ts +++ b/scripts/check-contract.ts @@ -15,6 +15,30 @@ const pin = pool.endpoints[0]?.releasePin; if (!pin?.mrtd || !pin.intelRootFingerprint) { throw new Error("coordinator release pin must include mrtd and intelRootFingerprint"); } +if ("allowMissingDcapCollateral" in pin) { + throw new Error("missing DCAP collateral escape hatch must be disabled by default"); +} + +const legacyDevPin = buildCoordinatorPool({ + ...env, + INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL: "true", +}).endpoints[0]?.releasePin; +if (legacyDevPin?.allowMissingDcapCollateral !== true) { + throw new Error("non-prod legacy DCAP collateral escape hatch must be explicit"); +} + +try { + buildCoordinatorPool({ + ...env, + INVISIBLE_REQUIRED_MODE: "prod", + INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL: "true", + }); + throw new Error("prod mode must reject missing DCAP collateral escape hatch"); +} catch (error) { + if (!(error instanceof Error) || !error.message.includes("DCAP")) { + throw error; + } +} try { buildCoordinatorPool({ ...env, INVISIBLE_REQUIRED_MODE: "" }); diff --git a/src/sdk.ts b/src/sdk.ts index 0f72540..ed635c1 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -6,6 +6,7 @@ export type WorkerEnv = { INVISIBLE_REQUIRED_MODE: string; INVISIBLE_RELEASE_MRTD: string; INVISIBLE_INTEL_ROOT_FINGERPRINT: string; + INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL?: string; DEMO_DESTINATION_ADDRESS: string; INVISIBLE_WORKER_API_KEY?: string; }; @@ -15,7 +16,11 @@ export type CoordinatorPoolConfig = { wsUrl: string; expectedHostname: string; requiredMode: "dev" | "prod" | "auto"; - releasePin: { mrtd: string; intelRootFingerprint: string }; + releasePin: { + mrtd: string; + intelRootFingerprint: string; + allowMissingDcapCollateral?: boolean; + }; }>; allowedRoles?: string[]; preferLeader?: boolean; @@ -90,16 +95,25 @@ export function buildCoordinatorPool(env: WorkerEnv): CoordinatorPoolConfig { if (endpoint.protocol !== "wss:" && endpoint.protocol !== "ws:") { throw new Error("INVISIBLE_COORDINATOR_WS_URL must use ws:// or wss://"); } + const requiredMode = readRequiredMode(env.INVISIBLE_REQUIRED_MODE); + const allowMissingDcapCollateral = readBoolean( + env.INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL, + false, + ); + if (requiredMode === "prod" && allowMissingDcapCollateral) { + throw new Error("INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL is only allowed outside prod mode"); + } return { endpoints: [ { wsUrl: env.INVISIBLE_COORDINATOR_WS_URL, expectedHostname: endpoint.hostname, - requiredMode: readRequiredMode(env.INVISIBLE_REQUIRED_MODE), + requiredMode, releasePin: { mrtd: env.INVISIBLE_RELEASE_MRTD, intelRootFingerprint: env.INVISIBLE_INTEL_ROOT_FINGERPRINT, + ...(allowMissingDcapCollateral ? { allowMissingDcapCollateral: true } : {}), }, }, ], @@ -126,6 +140,11 @@ function readRequiredMode(value: string): "dev" | "prod" | "auto" { throw new Error("INVISIBLE_REQUIRED_MODE must be dev, prod, or auto"); } +function readBoolean(value: string | undefined, fallback: boolean): boolean { + if (value === undefined) return fallback; + return value === "true" || value === "1"; +} + async function loadSdk() { return loadSdkBundle(); } diff --git a/wrangler.jsonc b/wrangler.jsonc index 282e5e9..17f9d61 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -12,6 +12,7 @@ "INVISIBLE_REQUIRED_MODE": "dev", "INVISIBLE_RELEASE_MRTD": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "INVISIBLE_INTEL_ROOT_FINGERPRINT": "0000000000000000000000000000000000000000000000000000000000000000", + "INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL": "false", "DEMO_DESTINATION_ADDRESS": "11111111111111111111111111111111" } } From d3404e449dcb8810281d0007a19ddf282128007c Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Fri, 19 Jun 2026 23:36:22 +0200 Subject: [PATCH 6/8] docs: clarify dcap collateral preview mode --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35f82e8..feda9b6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ INVISIBLE_INTEL_ROOT_FINGERPRINT= INVISIBLE_WORKER_API_KEY= ``` -The SDK requires DCAP collateral by default. For a legacy non-production coordinator that does not emit collateral yet, set `INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL=true`. The example rejects that flag in `prod` mode. +The SDK requires DCAP collateral by default. Production examples need a coordinator that emits `dcap_collateral` or a compatible attestation manifest. For legacy previews only, set `INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL=true`; this maps to `releasePin.allowMissingDcapCollateral` and is rejected in `prod` mode. `POST /private-transfer` requires `Authorization: Bearer `. @@ -37,3 +37,5 @@ INVISIBLE_SDK_PACKAGE=latest npm run verify:sdk npm run prepare:sdk npm run deploy ``` + +`npm run verify:sdk` installs `@invisible/sdk` in a temporary consumer project. It must pass from the published or packaged SDK artifact, not from local monorepo paths or generated FROST files. From d2983b6f3cf6dc02cc2618d493c5f22dc1ea0931 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Sat, 20 Jun 2026 02:35:14 +0200 Subject: [PATCH 7/8] docs: note coordinator sdk compatibility --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index feda9b6..e1e1bd9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ INVISIBLE_WORKER_API_KEY= The SDK requires DCAP collateral by default. Production examples need a coordinator that emits `dcap_collateral` or a compatible attestation manifest. For legacy previews only, set `INVISIBLE_ALLOW_MISSING_DCAP_COLLATERAL=true`; this maps to `releasePin.allowMissingDcapCollateral` and is rejected in `prod` mode. +If the configured coordinator is still on an older wire contract, this preview can fail before a transfer is created. Once the matching coordinator and wire-contract rollout is live, the SDK package and coordinator will speak the same message names and payload shapes. + `POST /private-transfer` requires `Authorization: Bearer `. When `@invisible/sdk` is available: From 82a048e17a40ebe9bded9c49f90fdc1eba7bbc30 Mon Sep 17 00:00:00 2001 From: Rami Abdou Date: Sun, 21 Jun 2026 20:12:20 +0200 Subject: [PATCH 8/8] feat: add LP SDK worker route --- README.md | 14 +- package-lock.json | 459 ++++++++++++++++++++++++++++++++- package.json | 2 +- scripts/prepare-sdk-imports.ts | 3 +- scripts/simulate.ts | 10 + scripts/verify-sdk.ts | 7 +- src/index.ts | 33 ++- src/sdk-types.ts | 39 +++ src/sdk.ts | 234 ++++++++++++++++- 9 files changed, 782 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e1e1bd9..bcb6e3a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Routes: GET /health GET /sdk POST /private-transfer +POST /lp ``` Set secrets with Wrangler. Do not commit secrets. @@ -31,13 +32,22 @@ The SDK requires DCAP collateral by default. Production examples need a coordina If the configured coordinator is still on an older wire contract, this preview can fail before a transfer is created. Once the matching coordinator and wire-contract rollout is live, the SDK package and coordinator will speak the same message names and payload shapes. `POST /private-transfer` requires `Authorization: Bearer `. +`POST /lp` requires the same bearer token and accepts: + +```json +{ "action": "create" } +``` + +For existing positions, pass `positionCode` with `recover`, `complete-dkg`, +`prepare-funding`, `reconcile-funding`, `refill`, or `withdraw`. `withdraw` +also requires `destinationAddress`. When `@invisible/sdk` is available: ```bash -INVISIBLE_SDK_PACKAGE=latest npm run verify:sdk +INVISIBLE_SDK_PACKAGE=npm:@invisible-labs/sdk@0.1.0-dev.1.2 npm run verify:sdk npm run prepare:sdk npm run deploy ``` -`npm run verify:sdk` installs `@invisible/sdk` in a temporary consumer project. It must pass from the published or packaged SDK artifact, not from local monorepo paths or generated FROST files. +`npm run verify:sdk` installs `@invisible/sdk@npm:@invisible-labs/sdk@0.1.0-dev.1.2` in a temporary consumer project when `INVISIBLE_SDK_PACKAGE` is unset. It allows freshly published private dev packages for that temporary install only, and must pass from the published or packaged SDK artifact, not from local monorepo paths or generated FROST files. diff --git a/package-lock.json b/package-lock.json index 24f9c18..7181336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "invisible-worker", "version": "0.1.0", + "dependencies": { + "@invisible/sdk": "npm:@invisible-labs/sdk@0.1.0-dev.1.2" + }, "devDependencies": { "@cloudflare/workers-types": "4.20260612.1", "@types/node": "24.10.3", @@ -17,7 +20,7 @@ "node": ">=24.0.0" }, "optionalDependencies": { - "@invisible/sdk": "0.0.0" + "@invisible/sdk": "npm:@invisible-labs/sdk@0.1.0-dev.1.2" } }, "node_modules/@cloudflare/workers-types": { @@ -470,8 +473,335 @@ } }, "node_modules/@invisible/sdk": { + "name": "@invisible-labs/sdk", + "version": "0.1.0-dev.1.2", + "resolved": "https://npm.pkg.github.com/download/@invisible-labs/sdk/0.1.0-dev.1.2/62e7af1fd4461c07e229b4abe6109764a8ee677d", + "integrity": "sha512-B1pHdobtm0osKHbrFGbQqja4pGT3JXJHfo/OWB0vfOtl98jWE7oaRvQgWgzKGy3zdZNR8RngOtGXlyw2HBCIyw==", + "optional": true, + "dependencies": { + "@peculiar/x509": "1.14.3", + "@stablelib/chacha20poly1305": "2.0.1", + "@stablelib/hmac": "2.0.1", + "@stablelib/sha256": "2.0.1", + "@stablelib/x25519": "2.0.1", + "ajv": "8.20.0" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", + "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", + "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", + "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", + "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-rsa": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", + "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", + "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pfx": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", + "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", + "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", + "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stablelib/aead": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-2.0.0.tgz", + "integrity": "sha512-U/RMANRxbT/ahIpYsPSiFwDFNjADHdnCFfmo09MO1ai2XmerPAOPtMl0qmX7XVvygnACC6ijKDyHBoT2rGyElg==", + "license": "MIT", "optional": true }, + "node_modules/@stablelib/binary": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-2.0.1.tgz", + "integrity": "sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/int": "^2.0.1" + } + }, + "node_modules/@stablelib/bytes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-2.0.1.tgz", + "integrity": "sha512-QIzI6V7nkJA5CjOZ7GoceBd4CIKrJoC471VaI6jh1xPQ2cMhkhQK4HddyzCXOR2y+fBF3/5B2HO3FXXI9C+Xzg==", + "license": "MIT", + "optional": true + }, + "node_modules/@stablelib/chacha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-2.0.1.tgz", + "integrity": "sha512-lS1FqtNqofxe2vLkRsLli2m3x/XanUyAYRphLhdHumKeIsLbjbCXdCq3Pf/eWiO7G3QlSG5ViqnoVjktzfLWMg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/binary": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/chacha20poly1305": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha20poly1305/-/chacha20poly1305-2.0.1.tgz", + "integrity": "sha512-kOoBsXbDPVRlelzXl+5WViycgM19lD7lF3Bc3KWI+DzId0Stc2HlxAfSc+Xpn3RgqSCl1ZNTXr33LegqBhBBaw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/aead": "^2.0.0", + "@stablelib/binary": "^2.0.1", + "@stablelib/chacha": "^2.0.1", + "@stablelib/constant-time": "^2.0.1", + "@stablelib/poly1305": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/constant-time": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-2.0.1.tgz", + "integrity": "sha512-0NWPogffRm+UWBH0+iM5otZmNrVe5OHFIvyoNIVankMAYOQzMwcdVALOVPrB5Ho0dST+Oc3H8/hPh65Z8R/uew==", + "license": "MIT", + "optional": true + }, + "node_modules/@stablelib/hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stablelib/hash/-/hash-2.0.0.tgz", + "integrity": "sha512-u3WPSqGido8lwJuMcrBgM5K54LrPGhkWAdtsyccf7dGsLixAZUds77zOAbu7bvKPwQlmoByH0txBi5rTmEKuHg==", + "license": "MIT", + "optional": true + }, + "node_modules/@stablelib/hmac": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/hmac/-/hmac-2.0.1.tgz", + "integrity": "sha512-YDMEmogYNDWESU8ggMiTi4VDMfHt2XG+pgVskJCHuKVlmWwXxYKZ6bn5dq24/jU3vzZSajwKzRCU6iafT2OKUA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/constant-time": "^2.0.1", + "@stablelib/hash": "^2.0.0", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/int": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-2.0.1.tgz", + "integrity": "sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==", + "license": "MIT", + "optional": true + }, + "node_modules/@stablelib/keyagreement": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-2.0.1.tgz", + "integrity": "sha512-2+tWBLCMtWlHQ7GqjD5L+lQRyWtun4Lou0IOdTML8zuTuAS0EgihnHFx+4uMZwYU1In40J/WlpyKSLidHfStRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/bytes": "^2.0.1" + } + }, + "node_modules/@stablelib/poly1305": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-2.0.1.tgz", + "integrity": "sha512-D8xfZcL/5zeVARJ9I2fZOUCnZvsJx8G4JDQvD+ecsnaCildPtqZDwqqjtOc4cfHAdScrMjOzUVsssjXUTBw3YQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/constant-time": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/random": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-2.0.1.tgz", + "integrity": "sha512-W6GAtXEEs7r+dSbuBsvoFmlyL3gLxle41tQkjKu17dDWtDdjhVUbtRfRCQcCUeczwkgjQxMPopgwYEvxXtHXGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/binary": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/sha256": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/sha256/-/sha256-2.0.1.tgz", + "integrity": "sha512-LA1PaLDc6Lv72ppA4PEZ7abDE741KfG7k7QhBiUyIfViMqrwWv8HqQQFPeuPfS4k2OxFv++IAgc8HlvdBatD+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/binary": "^2.0.1", + "@stablelib/hash": "^2.0.0", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/wipe": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-2.0.1.tgz", + "integrity": "sha512-1eU2K9EgOcV4qc9jcP6G72xxZxEm5PfeI5H55l08W95b4oRJaqhmlWRc4xZAm6IVSKhVNxMi66V67hCzzuMTAg==", + "license": "MIT", + "optional": true + }, + "node_modules/@stablelib/x25519": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-2.0.1.tgz", + "integrity": "sha512-qi04HS2puHaBf50kM/kes5QcZFGsx8yF0YmCjLCOa/LPmnBaKEKX9ZR82OnnCwMn72YH13R/bBZgr/UP0aPFfA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@stablelib/keyagreement": "^2.0.1", + "@stablelib/random": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, "node_modules/@types/node": { "version": "24.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", @@ -482,6 +812,38 @@ "undici-types": "~7.16.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -524,6 +886,30 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "optional": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -552,6 +938,50 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "optional": true + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -562,6 +992,13 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -582,6 +1019,26 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index a19681a..a87fd32 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "audit:high": "npm audit --audit-level=high" }, "optionalDependencies": { - "@invisible/sdk": "0.0.0" + "@invisible/sdk": "npm:@invisible-labs/sdk@0.1.0-dev.1.2" }, "devDependencies": { "@cloudflare/workers-types": "4.20260612.1", diff --git a/scripts/prepare-sdk-imports.ts b/scripts/prepare-sdk-imports.ts index 289499a..ca5238c 100644 --- a/scripts/prepare-sdk-imports.ts +++ b/scripts/prepare-sdk-imports.ts @@ -16,11 +16,12 @@ async function writeGeneratedLoader(usePackage: boolean): Promise { ? [ 'import * as root from "@invisible/sdk";', 'import * as user from "@invisible/sdk/user";', + 'import * as lp from "@invisible/sdk/lp";', 'import * as storage from "@invisible/sdk/storage";', 'import type { SdkBundle } from "./sdk-types.js";', "", "export async function loadSdkBundle(): Promise {", - " return { root, user, storage };", + " return { root, user, storage, lp };", "}", "", ] diff --git a/scripts/simulate.ts b/scripts/simulate.ts index 8e7b13d..2ac6e8f 100644 --- a/scripts/simulate.ts +++ b/scripts/simulate.ts @@ -25,6 +25,16 @@ const transfer = await worker.fetch( ); if (transfer.status !== 503) throw new Error("private transfer should fail closed without SDK"); +const lp = await worker.fetch( + new Request("https://worker.example/lp", { + method: "POST", + headers: { authorization: "Bearer test-key" }, + body: JSON.stringify({ action: "recover", positionCode: "lp-code" }), + }), + env, +); +if (lp.status !== 503) throw new Error("LP route should fail closed without SDK"); + const unauthorized = await worker.fetch( new Request("https://worker.example/private-transfer", { method: "POST", diff --git a/scripts/verify-sdk.ts b/scripts/verify-sdk.ts index 6fee1e2..a21d12a 100644 --- a/scripts/verify-sdk.ts +++ b/scripts/verify-sdk.ts @@ -3,7 +3,8 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; -const packageSpec = process.env.INVISIBLE_SDK_PACKAGE ?? "latest"; +const packageSpec = process.env.INVISIBLE_SDK_PACKAGE ?? "npm:@invisible-labs/sdk@0.1.0-dev.1.2"; +const privateSdkInstallArgs = ["install", "--omit=dev", "--min-release-age=0"]; const required = process.env.VERIFY_SDK_REQUIRED === "1" || Boolean(process.env.INVISIBLE_SDK_PACKAGE) || @@ -28,7 +29,7 @@ try { ].join("\n"), ); - const install = spawnSync("npm", ["install", "--omit=dev"], { + const install = spawnSync("npm", privateSdkInstallArgs, { cwd: workspace, encoding: "utf8", stdio: "pipe", @@ -46,7 +47,7 @@ try { [ "--input-type=module", "--eval", - "await import('@invisible/sdk'); await import('@invisible/sdk/user'); await import('@invisible/sdk/storage'); console.log('sdk imports ok')", + "await import('@invisible/sdk'); await import('@invisible/sdk/user'); await import('@invisible/sdk/lp'); await import('@invisible/sdk/storage'); console.log('sdk imports ok')", ], { cwd: workspace, encoding: "utf8", stdio: "pipe" }, ); diff --git a/src/index.ts b/src/index.ts index 8c894e5..630b13b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,12 @@ import { HttpError, json, readJsonBody } from "./http.js"; -import { sdkStatus, startPrivateTransfer, type PrivateTransferRequest, type WorkerEnv } from "./sdk.js"; +import { + runLpAction, + sdkStatus, + startPrivateTransfer, + type LpActionRequest, + type PrivateTransferRequest, + type WorkerEnv, +} from "./sdk.js"; export default { async fetch(request: Request, env: WorkerEnv): Promise { @@ -21,6 +28,13 @@ export default { return json(result, { status: statusForTransfer(result.status) }); } + if (request.method === "POST" && url.pathname === "/lp") { + await requireBearerToken(request, env); + const input = await readJsonBody(request); + const result = await runLpAction(env, input); + return json(result, { status: statusForLp(result.status) }); + } + return json({ error: "not_found" }, { status: 404 }); } catch (error) { if (error instanceof HttpError) { @@ -74,3 +88,20 @@ function statusForTransfer(status: string): number { return 500; } } + +function statusForLp(status: string): number { + switch (status) { + case "position": + case "funding": + case "withdrawal": + return 202; + case "sdk_missing": + return 503; + case "sdk_not_ready": + return 501; + case "failed": + return 502; + default: + return 500; + } +} diff --git a/src/sdk-types.ts b/src/sdk-types.ts index d6b2f9c..b561a1e 100644 --- a/src/sdk-types.ts +++ b/src/sdk-types.ts @@ -2,6 +2,7 @@ export type SdkSession = { readonly attested?: boolean }; export type SdkRoot = { createSession(options: { coordinator: unknown; storage?: unknown }): Promise; + closeSession?(session: SdkSession): void; normalizeError?(error: unknown, fallback?: string): string; }; @@ -24,8 +25,46 @@ export type SdkStorage = { inMemoryStorage?(): unknown; }; +export type SdkLpPosition = { + id: string; + status: string; + targetShardCount: number; + shards: Array<{ status: string }>; + committedLamports: number; + earnedLamports: number; +}; + +export type SdkLp = { + createPosition( + session: SdkSession, + args?: { committedLamports?: number; shardCount?: number }, + ): Promise<{ positionId: string; lpPositionCode: string; position?: SdkLpPosition }>; + recoverPosition(session: SdkSession, args: { code: string }): Promise; + completeDkgBatch(session: SdkSession, positionId: string): Promise; + prepareInitialFunding( + session: SdkSession, + positionId: string, + ): Promise<{ + address: string; + requiredLamports: number; + qrPayload: string; + position?: SdkLpPosition; + } | null>; + reconcileFunding(session: SdkSession, positionId: string): Promise; + refill(session: SdkSession, positionId: string): Promise; + withdrawPosition( + session: SdkSession, + positionId: string, + args: { destinationAddresses: string[]; allowManyToOne: boolean }, + ): Promise<{ + execution: { withdrawalId: string; txSignatures: string[] }; + position: SdkLpPosition; + }>; +}; + export type SdkBundle = { root: SdkRoot; user: SdkUser; storage: SdkStorage; + lp: SdkLp; }; diff --git a/src/sdk.ts b/src/sdk.ts index ed635c1..f1767c8 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -1,5 +1,5 @@ import { loadSdkBundle } from "./sdk-imports.generated.js"; -import type { SdkRoot } from "./sdk-types.js"; +import type { SdkBundle, SdkLpPosition, SdkRoot } from "./sdk-types.js"; export type WorkerEnv = { INVISIBLE_COORDINATOR_WS_URL: string; @@ -44,6 +44,66 @@ export type PrivateTransferRequest = { destinationAddress?: string; }; +export type LpAction = + | "create" + | "recover" + | "complete-dkg" + | "prepare-funding" + | "reconcile-funding" + | "refill" + | "withdraw"; + +export type LpActionRequest = { + action: LpAction; + positionCode?: string; + destinationAddress?: string; +}; + +export type LpActionResult = + | { + status: "position"; + action: LpAction; + message: string; + position: LpPositionSummary; + lpPositionCode?: string; + } + | { + status: "funding"; + action: "prepare-funding"; + message: string; + position: LpPositionSummary; + address: string; + requiredLamports: number; + qrPayload: string; + } + | { + status: "withdrawal"; + action: "withdraw"; + message: string; + position: LpPositionSummary; + withdrawalId: string; + txSignatures: string[]; + } + | { status: "sdk_missing"; message: string } + | { status: "sdk_not_ready"; message: string } + | { status: "failed"; message: string }; + +export type LpPositionSummary = { + id: string; + status: string; + targetShardCount: number; + shardCount: number; + availableShardCount: number; + fundingQueuedShardCount: number; + pregeneratedShardCount: number; + committedLamports: number; + earnedLamports: number; +}; + +const LP_DEFAULT_TARGET_SHARDS = 200; +const LP_INITIAL_FUNDING_LAMPORTS = 101_000_000; +const LP_WITHDRAWAL_ALLOW_MANY_TO_ONE = true; + export async function sdkStatus(): Promise<{ packageAvailable: boolean }> { return { packageAvailable: (await loadSdk()) !== null }; } @@ -62,16 +122,20 @@ export async function startPrivateTransfer(env: WorkerEnv, input: PrivateTransfe coordinator: buildCoordinatorPool(env), storage: sdk.storage.inMemoryStorage?.(), }); - const receipt = await sdk.user.contractRequest(session, { - amountLamports: assertLamports(input.amountLamports), - payoutPolicy: singleDestinationPolicy(input.destinationAddress ?? env.DEMO_DESTINATION_ADDRESS), - sync: true, - }); + try { + const receipt = await sdk.user.contractRequest(session, { + amountLamports: assertLamports(input.amountLamports), + payoutPolicy: singleDestinationPolicy(input.destinationAddress ?? env.DEMO_DESTINATION_ADDRESS), + sync: true, + }); - return { - status: "accepted", - requestId: receipt.requestId ?? receipt.request_id ?? "accepted", - }; + return { + status: "accepted", + requestId: receipt.requestId ?? receipt.request_id ?? "accepted", + }; + } finally { + sdk.root.closeSession?.(session); + } } catch (error) { const message = normalizeSdkError(sdk.root, error); if (message.includes("NOT_ATTESTED") || message.includes("not attested")) { @@ -90,6 +154,128 @@ export async function startPrivateTransfer(env: WorkerEnv, input: PrivateTransfe } } +export async function runLpAction(env: WorkerEnv, input: LpActionRequest): Promise { + const sdk = await loadSdk(); + if (!sdk) { + return { + status: "sdk_missing", + message: "Install @invisible/sdk from the private package registry before running LP actions.", + }; + } + + try { + const session = await sdk.root.createSession({ + coordinator: buildCoordinatorPool(env), + storage: sdk.storage.inMemoryStorage?.(), + }); + + try { + if (input.action === "create") { + const result = await sdk.lp.createPosition(session, { + committedLamports: LP_INITIAL_FUNDING_LAMPORTS, + shardCount: LP_DEFAULT_TARGET_SHARDS, + }); + const position = + result.position ?? (await sdk.lp.recoverPosition(session, { code: result.lpPositionCode })); + return { + status: "position", + action: input.action, + message: "LP position created. Store the LP Position Code before continuing.", + position: summarizeLpPosition(position), + lpPositionCode: result.lpPositionCode, + }; + } + + const recovered = await recoverLpPositionForAction(sdk, session, input.positionCode); + if (input.action === "recover") { + return { + status: "position", + action: input.action, + message: "LP position recovered.", + position: summarizeLpPosition(recovered), + }; + } + + if (input.action === "complete-dkg") { + const position = await sdk.lp.completeDkgBatch(session, recovered.id); + return { + status: "position", + action: input.action, + message: "LP DKG batch completed or reconciled.", + position: summarizeLpPosition(position), + }; + } + + if (input.action === "prepare-funding") { + const plan = await sdk.lp.prepareInitialFunding(session, recovered.id); + if (plan === null) { + return { + status: "position", + action: input.action, + message: "No LP_DKG_0 funding action is currently available.", + position: summarizeLpPosition(recovered), + }; + } + return { + status: "funding", + action: input.action, + message: "Fund only LP_DKG_0 with the exact required amount.", + position: summarizeLpPosition(plan.position ?? recovered), + address: plan.address, + requiredLamports: plan.requiredLamports, + qrPayload: plan.qrPayload, + }; + } + + if (input.action === "reconcile-funding") { + const position = await sdk.lp.reconcileFunding(session, recovered.id); + return { + status: "position", + action: input.action, + message: "LP funding reconciled from coordinator state.", + position: summarizeLpPosition(position), + }; + } + + if (input.action === "refill") { + const position = await sdk.lp.refill(session, recovered.id); + return { + status: "position", + action: input.action, + message: "LP refill requested through the SDK lifecycle.", + position: summarizeLpPosition(position), + }; + } + + const destination = input.destinationAddress?.trim(); + if (!destination) throw new Error("destinationAddress is required for withdraw."); + const result = await sdk.lp.withdrawPosition(session, recovered.id, { + destinationAddresses: [destination], + allowManyToOne: LP_WITHDRAWAL_ALLOW_MANY_TO_ONE, + }); + return { + status: "withdrawal", + action: input.action, + message: "LP withdrawal requested. Reconciliation stays SDK-owned.", + position: summarizeLpPosition(result.position), + withdrawalId: result.execution.withdrawalId, + txSignatures: result.execution.txSignatures, + }; + } finally { + sdk.root.closeSession?.(session); + } + } catch (error) { + const message = normalizeSdkError(sdk.root, error); + if (message.includes("NOT_ATTESTED") || message.includes("not attested")) { + return { + status: "sdk_not_ready", + message: "SDK connected, but this package build has not completed coordinator attestation yet.", + }; + } + return { status: "failed", message }; + } +} + export function buildCoordinatorPool(env: WorkerEnv): CoordinatorPoolConfig { const endpoint = new URL(env.INVISIBLE_COORDINATOR_WS_URL); if (endpoint.protocol !== "wss:" && endpoint.protocol !== "ws:") { @@ -149,6 +335,34 @@ async function loadSdk() { return loadSdkBundle(); } +async function recoverLpPositionForAction( + sdk: SdkBundle, + session: Awaited>, + positionCode: string | undefined, +): Promise { + const code = positionCode?.trim(); + if (!code) throw new Error("positionCode is required for this LP action."); + return sdk.lp.recoverPosition(session, { code }); +} + +function summarizeLpPosition(position: SdkLpPosition): LpPositionSummary { + return { + id: position.id, + status: position.status, + targetShardCount: position.targetShardCount, + shardCount: position.shards.length, + availableShardCount: countLpShards(position, "AVAILABLE"), + fundingQueuedShardCount: countLpShards(position, "FUNDING_QUEUED"), + pregeneratedShardCount: countLpShards(position, "PREGENERATED"), + committedLamports: position.committedLamports, + earnedLamports: position.earnedLamports, + }; +} + +function countLpShards(position: SdkLpPosition, status: string): number { + return position.shards.filter((shard) => shard.status === status).length; +} + function normalizeSdkError(sdk: SdkRoot, error: unknown): string { if (sdk.normalizeError) return sdk.normalizeError(error, "Invisible transfer failed."); if (error instanceof Error) return error.message;