From 4d850f969890199a4b4c2ca06f600a81faa7b79a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 13 Oct 2026 05:29:28 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20feat:=20deploy=20internal=20?= =?UTF-8?q?Pulldash=20on=20Cloudflare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entire-Checkpoint: 01M4SVSE6X57CHT2Q05895NP85 --- .github/workflows/ci.yml | 14 +- .github/workflows/deploy-cloudflare.yml | 52 ++++++ .gitignore | 5 + .prettierignore | 11 ++ README.md | 135 ++++++++++++++++ bun.lock | 205 ++++++++++++++++++++++-- cloudflare.config.ts | 34 ++++ package.json | 11 +- scripts/build-browser.test.ts | 73 +++++++++ scripts/build-browser.ts | 14 +- src/api/api.test.ts | 128 +++++++++++++++ src/api/api.ts | 42 +++-- src/api/github-config.json | 3 + src/browser/contexts/auth.tsx | 14 +- src/browser/contexts/telemetry.tsx | 91 +---------- wrangler.config.ts | 8 + 16 files changed, 714 insertions(+), 126 deletions(-) create mode 100644 .github/workflows/deploy-cloudflare.yml create mode 100644 .prettierignore create mode 100644 cloudflare.config.ts create mode 100644 scripts/build-browser.test.ts create mode 100644 src/api/api.test.ts create mode 100644 src/api/github-config.json create mode 100644 wrangler.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c30cc53..cb0d037 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,15 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: "1.4.0" + + - name: Setup Node for Cloudflare CLI + uses: actions/setup-node@v4 + with: + node-version: "22.22.2" - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Check formatting run: bun run fmt:check @@ -33,3 +38,8 @@ jobs: - name: Test run: bun run test + + - name: Validate Cloudflare build + run: bun run check:cloudflare + env: + CF_SEND_TELEMETRY: "false" diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml new file mode 100644 index 0000000..c5eb3c0 --- /dev/null +++ b/.github/workflows/deploy-cloudflare.yml @@ -0,0 +1,52 @@ +name: Deploy Cloudflare + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pulldash-internal-production + cancel-in-progress: false + +jobs: + deploy: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + env: + CF_SEND_TELEMETRY: "false" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.4.0" + + - name: Setup Node for Cloudflare CLI + uses: actions/setup-node@v4 + with: + node-version: "22.22.2" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Check formatting + run: bun run fmt:check + + - name: Typecheck + run: bun typecheck + + - name: Test + run: bun test + + - name: Build and validate deployment + run: bun run check:cloudflare + + - name: Deploy validated build + run: bun run cf deploy --prebuilt --mode production + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 299a0b9..faabb36 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,8 @@ dist/ release/ public/ .env*.local + +# Cloudflare build output and local credentials +.cloudflare/ +.wrangler/ +.dev.vars* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a357a6c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Local agent state is not project source. +.codex/ +.entire/ +.pi/ + +# Generated deployment and application files. +.cloudflare/ +.wrangler/ +dist/ +public/ +release/ diff --git a/README.md b/README.md index 1d90a3d..d8b5287 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,141 @@ bun install bun dev ``` +## Internal Cloudflare deployment + +The internal site runs at **code.internal.fiet.finance** on Worker +`pulldash-internal`, behind the team's existing Cloudflare Access policy. +PostHog is disabled. GitHub requests still go directly from the browser to GitHub; +only device authentication uses the Worker API. + +Use Bun 1.4.0 for installs, builds, and tests. Cloudflare's `cf` CLI must run under +Node 22.22.2; do not force it to use the Bun interpreter. The pinned Wrangler +dependency is only the build backend for `cf`, with no Vite migration. + +```bash +bun install --frozen-lockfile +bun run check:cloudflare +``` + +This builds the browser and API, then validates the production deployment without +uploading it or requiring Cloudflare credentials. Build output lives in +`.cloudflare/`. `cloudflare.config.ts` defines the Worker and runtime settings; +`wrangler.config.ts` selects `dist/browser` as the asset source. + +### GitHub login + +The team's public client ID, `Iv23li2VXQV3ynTftTS5`, lives in +`src/api/github-config.json`. Set `GITHUB_CLIENT_ID` before building to override +it; local and desktop servers also accept that environment variable. +Device flow must be enabled in the app's GitHub settings. No client secret, +private key, or shared GitHub token belongs in the Worker. + +For a GitHub App, install it on the repositories the team reviews and grant the +required repository permissions: contents, pull requests, issues, and actions +read/write; checks and commit statuses read. User tokens are limited by both the app's and the user's +permissions; OAuth scopes do not replace GitHub App permissions. See +[GitHub's user access token documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app). +The existing personal access token login remains available. +Expiring device-flow tokens are stored with their expiry; once expired, users +sign in again. This deployment does not add automatic token refresh. + +### First deployment + +1. Authenticate `cf` using a scoped `CLOUDFLARE_API_TOKEN`, or run + `bun run cf auth login --no-browser` and complete sign-in from your own browser. + Set `CLOUDFLARE_ACCOUNT_ID` to the account owning the active `fiet.finance` zone. + Keep tokens in an ignored environment file or your secret manager. +2. Check the account, zone, existing Access policies, identity provider, and any + existing DNS record for `code.internal.fiet.finance`. Reuse the team's current + policy; do not replace it with an unrestricted allow policy. +3. Run `bun run deploy:cloudflare:bootstrap`. Bootstrap creates the Worker with no + custom domain, `workers.dev` route, or preview URL. Omitting `--mode` also + defaults to this unpublished configuration. +4. Create or reuse a self-hosted Access application protecting this Worker for + **all traffic**, and attach the existing reusable team policy. Preserve the + existing identity provider and session settings. Use the Worker ID returned by + Cloudflare, not its name, for the Access destination. Use `cf cli search` and + `cf schema` to verify the resource commands before changing account settings. +5. Once Access is attached, run `bun run deploy:cloudflare`. Production mode + attaches `code.internal.fiet.finance`; Cloudflare provisions its HTTPS + certificate. Check authorized and unauthorized access before sharing the URL. + +The initial bootstrap deployment is in the **Usher Labs** account +(`a8794e19e02cedc4d87baf4008265dd7`). Its initial version is +`5dbf604a-09f4-483d-b7b7-cc6c0253e1fd`, and its Worker ID is +`88d47ee0269d4952a17d1639cf641f12`. Its reserved +`pulldash-internal.usherlabs.workers.dev` hostname and previews are disabled, and +it has no attached custom domains. The supplied +staging token can deploy Workers, but currently exposes no DNS zones, and that +account reports that Access is not enabled. + +To wire DNS later, ensure `fiet.finance` is an active zone in this same account +and use a credential with the required zone/domain permissions. Enable Zero +Trust and attach the team Access policy to `pulldash-internal` before publishing. +Then add `code.internal.fiet.finance` under the Worker's **Domains & Routes → Add +Custom Domain**, or run the production deployment command. Cloudflare creates +the DNS record and certificate automatically; no server IP or manual CNAME to +`workers.dev` is needed. If the zone is owned by a different account, deploy the +Worker in that account before attaching the domain. See +[Cloudflare's Custom Domain requirements](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/). + +For the provided dotenvx credential source, inject the token only into the child +process; do not copy it into this repository: + +```bash +dotenvx run --redact --strict -f "$HOME/.env" -- bash -c ' + export CLOUDFLARE_API_TOKEN="$USHER_LABS_CLOUDFLARE_STAGING_API_TOKEN" + export CLOUDFLARE_ACCOUNT_ID="a8794e19e02cedc4d87baf4008265dd7" + bun run cf auth whoami +' +``` + +Cloudflare Access admits teammates to the site. Each teammate then authorizes +their own GitHub access. Access protection covers static files and the login API; +the alternative public Worker URLs stay disabled. + +### Manual releases and rollback + +Add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` as secrets on +`usherlabs/pulldash`. Scope the token to the deployment account and the Worker and +domain permissions needed to publish it. Access setup is a separate first-release +step; routine deployments must preserve that application and its policies. + +After the changes reach `main`, trigger the manual workflow: + +```bash +gh workflow run deploy-cloudflare.yml --repo usherlabs/pulldash --ref main +gh run list --repo usherlabs/pulldash --workflow deploy-cloudflare.yml --limit 1 +gh run watch --repo usherlabs/pulldash --exit-status +``` + +The workflow runs formatting checks, type checks, tests, a browser/API build, and a +packaging dry run before deploying that exact prebuilt output. Only the publishing +step receives Cloudflare credentials. Pushes and PRs validate packaging but never +publish. Production releases are serialized and restricted to `main`. + +After publishing, verify a direct PR URL, `/lib/diff-worker.js`, device login, PAT +login, private PR loading, and rejection of an unauthorized user. No browser +automation is used for this project; browser authentication and review checks +are human smoke tests. + +For rollback, use `cf workers deployments list` to identify the previous healthy +Worker version and `cf workers deployments create` to deploy it at 100 percent. +Inspect the installed command help/schema and dry-run the request before applying +it. Rollback changes Worker code and assets; it must leave the domain and Access +policy intact. Keep the previous version ID in the release notes and inspect +Cloudflare Worker/Access logs after a release. + +```bash +bun run cf workers deployments list --worker pulldash-internal +bun run cf workers deployments create --worker pulldash-internal \ + --strategy percentage \ + --versions '[{"version_id":"","percentage":100}]' \ + --dry-run +``` + +After reviewing the dry run, repeat the create command without `--dry-run`. + ## License [AGPL](./LICENSE) diff --git a/bun.lock b/bun.lock index b21a627..3714b5a 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "@types/react-dom": "^19", "@typescript/native-preview": "^7.0.0-dev.20251201.1", "bun-plugin-tailwind": "latest", + "cf": "1.0.0-beta.14", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -44,7 +45,6 @@ "hono": "^4.10.7", "lucide-react": "^0.555.0", "png2icons": "^2.0.1", - "posthog-js": "^1.300.0", "prettier": "^3.7.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -58,12 +58,37 @@ "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", "typescript": "^5", + "wrangler": "4.149.0", }, }, }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@cloudflare/build-output-utils": ["@cloudflare/build-output-utils@0.9.0", "", { "dependencies": { "@cloudflare/config": "0.24.1" } }, "sha512-2FMA4XnVj5u7LYJE9j3uSJ+cU4Zn994WiyEOVhbMV7wh7pcwi1rIYafH6kiWaa+Y3Bjvf0ow7JgoDaO05qYVPg=="], + + "@cloudflare/codemods": ["@cloudflare/codemods@0.4.2", "", { "bin": { "cloudflare-codemods": "dist/bin.mjs" } }, "sha512-oHvDlfhQEIkdf8Mvz4SMgk+6evVlu9rrntRv5KQfRK3H4RkXUmawzYQ7+zdBG0wiWdp/Ku/PqNMWVYAs8T4jrQ=="], + + "@cloudflare/config": ["@cloudflare/config@0.24.1", "", { "dependencies": { "zod": "4.6.5" } }, "sha512-tja1/oHB5sCgigiRrZrESYFUzNEgex1zW0GDWXNbpUVpB8zX7ZQKMYnZYX2zD2sy8dk9paDZDnPC1VSu7y8aZw=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/runtime-types": ["@cloudflare/runtime-types@0.1.9", "", { "dependencies": { "miniflare": "5.20261006.1-alpha", "workerd": "1.20261006.1" } }, "sha512-T3LcgIUw2VMWCw/50deozY/Iqu9j1NN3xbYGTmYbLvGk1a8aNZHplwf/JknYmqSVHZU4eHqsFpkn8IID2W4W/A=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20261006.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20261006.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PyFGjOaldtppnLuKsdkoYWXfLVpDjuf8u+uoykX+1Xvxo2EPmCVViq0iDKm+U7c7PRno85ehS3/MPVXyk2HwHw=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20261006.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ntf9S8eesmVufdjaT2kzWlz6Ng9XGYTc6cHQVRKAg59lFufwsIabGeMT6U4KkrpEeSIxgHq8aPtuLbZXE19tCA=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20261006.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JyeDBymVWw5tVRebSsVE8ojDYxd6UlZrAseHU8G+NpIyd5wF61tXlF/axLeCUqu0pOhBFHb8I5QKi0jwrCK41Q=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20261006.1", "", { "os": "win32", "cpu": "x64" }, "sha512-nWP0URNqOBN9fa/gZ7eh/w051DwjOXN8j4lDZDYVXyQPsux6sV3DfyYZT+CjR/rt9nKBSmJT9QE9/bWT18+84w=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], "@electron/asar": ["@electron/asar@4.0.1", "", { "dependencies": { "commander": "^13.1.0", "glob": "^11.0.1", "minimatch": "^10.0.1" }, "bin": { "asar": "bin/asar.mjs" } }, "sha512-F4Ykm1jiBGY1WV/o8Q8oFW8Nq0u+S2/vPujzNJtdSJ6C4LHC4CiGLn7c17s7SolZ23gcvCebMncmZtNc+MkxPQ=="], @@ -156,6 +181,8 @@ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], @@ -194,6 +221,8 @@ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], @@ -206,6 +235,12 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], @@ -272,7 +307,11 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@posthog/core": ["@posthog/core@1.7.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-d6ZV4grpzeH/6/LP8quMVpSjY1puRkrqfwcPvGRKUAX7tb7YHyp/zMiTDuJmOFbpUxAMBXH5nDwcPiyCY2WGzA=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -350,6 +389,8 @@ "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], @@ -466,6 +507,8 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -498,6 +541,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "cf": ["cf@1.0.0-beta.14", "", { "dependencies": { "@cloudflare/build-output-utils": "0.9.0", "@cloudflare/codemods": "0.4.2", "@cloudflare/config": "0.24.1", "@cloudflare/runtime-types": "0.1.9", "blake3-wasm": "2.1.5", "miniflare": "5.20261006.1-alpha", "minisearch": "7.2.0" }, "bin": { "cf": "./bin/cf", "cloudflare": "./bin/cf" } }, "sha512-CIDV5JAUIhJRTCPG3gR72BBzNbPH0NqpF/C3d6mPTXFd1LMbAgfAv6C0sg0MtLLb0blz1kFudDDd4azlKQ/p9A=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -556,8 +601,6 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], @@ -636,6 +679,8 @@ "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -670,8 +715,6 @@ "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], - "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -830,6 +873,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -954,6 +999,8 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "miniflare": ["miniflare@5.20261006.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.5", "undici": "7.29.1", "workerd": "1.20261006.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-6ylv8tamkrspDI2R/cAYfv+YWmfGNdXe76Pi9TauEUWsFVXn3LNF9Yk8oduzf1a+TYsSRYTqlOE7+nGGRsMHdw=="], + "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -970,6 +1017,8 @@ "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -1018,6 +1067,10 @@ "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -1030,12 +1083,8 @@ "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "posthog-js": ["posthog-js@1.300.0", "", { "dependencies": { "@posthog/core": "1.7.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" } }, "sha512-FpeD4pkpg9i/3F5xpeX+CZeq3R3BaLyFDkzb4sOjpUIW1AAVuOEqPTxnWIess6C3pOw1u3mowq0WSeh1PmrqfA=="], - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - "preact": ["preact@10.28.0", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="], - "prettier": ["prettier@3.7.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg=="], "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="], @@ -1222,8 +1271,12 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], @@ -1268,16 +1321,20 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "workerd": ["workerd@1.20261006.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20261006.1", "@cloudflare/workerd-darwin-arm64": "1.20261006.1", "@cloudflare/workerd-linux-64": "1.20261006.1", "@cloudflare/workerd-linux-arm64": "1.20261006.1", "@cloudflare/workerd-windows-64": "1.20261006.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA=="], + + "wrangler": ["wrangler@4.149.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.2", "miniflare": "5.20261006.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20261006.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20261006.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-OzK7xmB5r5iLKb3cIT3783g13fe6T7xyeKu9LEdUyl+DKAM2KP/jkjnE0OUhyw+mB/XJM9ycjgF3MWm8PR9amg=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1292,6 +1349,12 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -1314,6 +1377,10 @@ "@electron/windows-sign/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -1324,6 +1391,10 @@ "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1376,6 +1447,8 @@ "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "miniflare/sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1406,6 +1479,8 @@ "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "wrangler/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], @@ -1420,6 +1495,10 @@ "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -1460,8 +1539,110 @@ "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "miniflare/sharp/@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], + + "miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + + "miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + + "miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + + "miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + + "miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + + "miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + + "miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + + "miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + + "miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + + "miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + + "miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + + "miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + + "miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + + "miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + + "miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + "@electron/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], diff --git a/cloudflare.config.ts b/cloudflare.config.ts new file mode 100644 index 0000000..d8eee9f --- /dev/null +++ b/cloudflare.config.ts @@ -0,0 +1,34 @@ +import { bindings, defineConfig } from "cf/config"; +import githubConfig from "./src/api/github-config.json" with { type: "json" }; + +export default defineConfig(({ mode }) => { + if (mode && mode !== "bootstrap" && mode !== "production") { + throw new Error(`Unsupported Cloudflare mode: ${mode}`); + } + + return { + accountId: process.env.CLOUDFLARE_ACCOUNT_ID, + worker: { + name: "pulldash-internal", + compatibilityDate: "2026-10-13", + entrypoint: "./src/api/api.ts", + assets: { + notFoundHandling: "single-page-application", + runWorkerFirst: ["/api/*"], + }, + env: { + GITHUB_CLIENT_ID: bindings.text( + process.env.GITHUB_CLIENT_ID ?? githubConfig.clientId + ), + }, + // Publish a domain only after Access has been attached to the bootstrap Worker. + domains: mode === "production" ? ["code.internal.fiet.finance"] : [], + workersDev: false, + previewUrls: false, + observability: { + enabled: true, + traces: { enabled: true }, + }, + }, + }; +}); diff --git a/package.json b/package.json index 35b9630..ee9d199 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,11 @@ "fmt": "prettier --write --cache .", "fmt:check": "prettier --check .", "lint": "echo 'No linter configured yet'", - "test": "bun test" + "test": "bun test", + "build:cloudflare": "bun run build:browser && cf build --mode production", + "check:cloudflare": "bun run build:cloudflare && cf deploy --prebuilt --mode production --dry-run", + "deploy:cloudflare": "bun run check:cloudflare && cf deploy --prebuilt --mode production", + "deploy:cloudflare:bootstrap": "bun run build:browser && cf build --mode bootstrap && cf deploy --prebuilt --mode bootstrap" }, "keywords": [ "github", @@ -59,6 +63,7 @@ "@types/react-dom": "^19", "@typescript/native-preview": "^7.0.0-dev.20251201.1", "bun-plugin-tailwind": "latest", + "cf": "1.0.0-beta.14", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -72,7 +77,6 @@ "hono": "^4.10.7", "lucide-react": "^0.555.0", "png2icons": "^2.0.1", - "posthog-js": "^1.300.0", "prettier": "^3.7.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -85,7 +89,8 @@ "tailwindcss": "^4", "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "wrangler": "4.149.0" }, "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", diff --git a/scripts/build-browser.test.ts b/scripts/build-browser.test.ts new file mode 100644 index 0000000..1a26b76 --- /dev/null +++ b/scripts/build-browser.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test"; +import { mkdtemp, mkdir, rm } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; + +test("failed bundles exit unsuccessfully while watch mode survives build errors", async () => { + const directory = await mkdtemp(join(tmpdir(), "pulldash-build-")); + try { + await mkdir(join(directory, "src/browser"), { recursive: true }); + await mkdir(join(directory, "dist/browser"), { recursive: true }); + await Bun.write(join(directory, "src/browser/logo.svg"), ""); + await Bun.write(join(directory, "dist/browser/index.html"), ""); + const preload = join(directory, "build-fixture.ts"); + await Bun.write( + preload, + `let calls = 0; +Bun.build = async () => ({ success: ++calls !== Number(process.env.FAIL_BUILD_AT), outputs: [], logs: [] });` + ); + for (const failure of ["1", "2"]) { + const child = Bun.spawn( + [ + process.execPath, + "--preload", + preload, + join(import.meta.dir, "build-browser.ts"), + ], + { + cwd: directory, + env: { ...process.env, FAIL_BUILD_AT: failure }, + stdout: "pipe", + stderr: "pipe", + } + ); + const error = await new Response(child.stderr).text(); + expect(error).toContain( + failure === "1" ? "Main build failed" : "Worker build failed" + ); + expect(await child.exited).toBe(1); + } + await Bun.write( + preload, + 'Bun.build = async () => { throw new Error("Fixture build failure"); };' + ); + const watcher = Bun.spawn( + [ + process.execPath, + "--preload", + preload, + join(import.meta.dir, "build-browser.ts"), + "--watch", + ], + { cwd: directory, stdout: "pipe", stderr: "pipe" } + ); + const errors = new Response(watcher.stderr).text(); + let output = ""; + try { + const reader = watcher.stdout.getReader(); + const decoder = new TextDecoder(); + while (!output.includes("Watching for changes")) { + const chunk = await reader.read(); + if (chunk.done) break; + output += decoder.decode(chunk.value); + } + expect(output).toContain("Watching for changes"); + } finally { + watcher.kill(); + await watcher.exited; + expect(await errors).toContain("Fixture build failure"); + } + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/scripts/build-browser.ts b/scripts/build-browser.ts index bce13fa..0324bc0 100644 --- a/scripts/build-browser.ts +++ b/scripts/build-browser.ts @@ -5,7 +5,7 @@ import { resolve } from "path"; const isWatch = process.argv.includes("--watch"); -async function build() { +async function buildBrowser() { // Build main app const mainResult = await Bun.build({ entrypoints: ["./src/browser/index.html"], @@ -86,7 +86,17 @@ if (typeof document === 'undefined') { return true; } -await build(); +async function build() { + try { + return await buildBrowser(); + } catch (error) { + console.error("Browser build failed:", error); + return false; + } +} + +const success = await build(); +if (!success && !isWatch) process.exit(1); if (isWatch) { console.log("\nWatching for changes..."); diff --git a/src/api/api.test.ts b/src/api/api.test.ts new file mode 100644 index 0000000..58a66bd --- /dev/null +++ b/src/api/api.test.ts @@ -0,0 +1,128 @@ +import { expect, spyOn, test } from "bun:test"; +import api from "./api"; + +test("device login uses the configured OAuth app and preserves GitHub responses", async () => { + const requests: { url: string; body: unknown }[] = []; + const upstream = spyOn(globalThis, "fetch").mockImplementation( + Object.assign( + async ( + input: Parameters[0], + init?: Parameters[1] + ) => { + requests.push({ + url: String(input), + body: JSON.parse(String(init?.body)), + }); + return Response.json( + requests.length === 1 + ? { + device_code: "device-code", + user_code: "TEAM-CODE", + interval: 5, + } + : requests.length === 2 + ? { error: "authorization_pending" } + : { access_token: "test-token", expires_in: 28800 } + ); + }, + { preconnect: () => {} } + ) + ); + try { + const env = { GITHUB_CLIENT_ID: "team-client-id" }; + const code = await api.request( + "/api/auth/device/code", + { method: "POST" }, + env + ); + expect(code.status).toBe(200); + expect(await code.json()).toEqual({ + device_code: "device-code", + user_code: "TEAM-CODE", + interval: 5, + }); + const token = await api.request( + "/api/auth/device/token", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ device_code: "device-code" }), + }, + env + ); + expect(token.status).toBe(200); + expect(await token.json()).toEqual({ error: "authorization_pending" }); + const authorized = await api.request( + "/api/auth/device/token", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ device_code: "device-code" }), + }, + env + ); + expect(authorized.status).toBe(200); + expect(authorized.headers.get("Cache-Control")).toBe("no-store"); + expect(await authorized.json()).toEqual({ + access_token: "test-token", + expires_in: 28800, + }); + expect(requests).toEqual([ + { + url: "https://github.com/login/device/code", + body: { client_id: "team-client-id", scope: "repo read:user" }, + }, + { + url: "https://github.com/login/oauth/access_token", + body: { + client_id: "team-client-id", + device_code: "device-code", + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }, + }, + { + url: "https://github.com/login/oauth/access_token", + body: { + client_id: "team-client-id", + device_code: "device-code", + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }, + }, + ]); + const invalid = await api.request( + "/api/auth/device/token", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }, + env + ); + expect(invalid.status).toBe(400); + expect(await invalid.json()).toEqual({ error: "device_code is required" }); + expect(requests).toHaveLength(3); + } finally { + upstream.mockRestore(); + } +}); + +test("device login fails clearly without an OAuth client ID", async () => { + const upstream = spyOn(globalThis, "fetch").mockImplementation( + Object.assign(async () => Response.json({}), { preconnect: () => {} }) + ); + try { + const response = await api.request( + "/api/auth/device/code", + { method: "POST" }, + { GITHUB_CLIENT_ID: "" } + ); + expect(response.status).toBe(503); + expect(await response.json()).toEqual({ + error: + "GitHub device login is not configured. Use a personal access token or configure GITHUB_CLIENT_ID.", + }); + expect(upstream).not.toHaveBeenCalled(); + } finally { + upstream.mockRestore(); + } +}); diff --git a/src/api/api.ts b/src/api/api.ts index 5b815a0..ab22461 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,22 +1,44 @@ import { Hono } from "hono"; +import githubConfig from "./github-config.json"; // ============================================================================ -// GitHub OAuth App Configuration +// GitHub device-flow configuration // ============================================================================ -// OAuth App (not GitHub App) - enables simple user authentication -// like the GitHub CLI, without requiring app installation on repos. -// Users just authorize and get access to their repos based on scopes. -const GITHUB_CLIENT_ID = "Ov23ct2e5eDCkITh5xlh"; -// Note: Client secret would be added here for OAuth web flow in the future -// const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; +// The public client ID is shared with Cloudflare configuration. +// OAuth Apps use scopes; GitHub Apps use their configured permissions and installations. +type Bindings = { GITHUB_CLIENT_ID?: string }; // ============================================================================ // API Routes // ============================================================================ -const api = new Hono() +const api = new Hono<{ + Bindings: Bindings; + Variables: { githubClientId: string }; +}>() .basePath("/api") + .use("/auth/device/*", async (c, next) => { + // Workers supply bindings; the local and desktop servers use environment variables. + const clientId = + c.env?.GITHUB_CLIENT_ID ?? + (typeof process !== "undefined" + ? process.env.GITHUB_CLIENT_ID + : undefined) ?? + githubConfig.clientId; + if (!clientId?.trim()) { + return c.json( + { + error: + "GitHub device login is not configured. Use a personal access token or configure GITHUB_CLIENT_ID.", + }, + 503 + ); + } + c.set("githubClientId", clientId.trim()); + c.header("Cache-Control", "no-store"); + await next(); + }) // Device Authorization - Step 1: Request device code // Proxies to GitHub since their endpoint doesn't support CORS @@ -31,7 +53,7 @@ const api = new Hono() "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: GITHUB_CLIENT_ID, + client_id: c.get("githubClientId"), scope: "repo read:user", }), }); @@ -71,7 +93,7 @@ const api = new Hono() "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: GITHUB_CLIENT_ID, + client_id: c.get("githubClientId"), device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), diff --git a/src/api/github-config.json b/src/api/github-config.json new file mode 100644 index 0000000..fd30b87 --- /dev/null +++ b/src/api/github-config.json @@ -0,0 +1,3 @@ +{ + "clientId": "Iv23li2VXQV3ynTftTS5" +} diff --git a/src/browser/contexts/auth.tsx b/src/browser/contexts/auth.tsx index 6338e59..2e44923 100644 --- a/src/browser/contexts/auth.tsx +++ b/src/browser/contexts/auth.tsx @@ -7,15 +7,6 @@ import { type ReactNode, } from "react"; -// ============================================================================ -// GitHub OAuth App Configuration -// ============================================================================ - -// OAuth App Client ID - enables simple user authentication like GitHub CLI. -// Users just authorize and get access to their repos based on scopes. -// No app installation required on repos/orgs. -export const GITHUB_CLIENT_ID = "Ov23ct2e5eDCkITh5xlh"; - // Storage keys const TOKEN_STORAGE_KEY = "pulldash_github_token"; const TOKEN_EXPIRY_KEY = "pulldash_github_token_expiry"; @@ -36,6 +27,7 @@ interface TokenResponse { access_token: string; token_type: string; scope: string; + expires_in?: number; error?: string; error_description?: string; } @@ -115,6 +107,8 @@ function storeToken(token: string, expiresIn?: number): void { if (expiresIn) { const expiryDate = new Date(Date.now() + expiresIn * 1000); localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString()); + } else { + localStorage.removeItem(TOKEN_EXPIRY_KEY); } } catch { console.error("Failed to store token in localStorage"); @@ -301,7 +295,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { if (tokenData.access_token) { // Success! Store the token and disable anonymous mode - storeToken(tokenData.access_token); + storeToken(tokenData.access_token, tokenData.expires_in); setStoredAnonymousMode(false); setState({ isAuthenticated: true, diff --git a/src/browser/contexts/telemetry.tsx b/src/browser/contexts/telemetry.tsx index 0899228..a5f0077 100644 --- a/src/browser/contexts/telemetry.tsx +++ b/src/browser/contexts/telemetry.tsx @@ -1,23 +1,4 @@ -import { - createContext, - useContext, - useEffect, - useRef, - type ReactNode, -} from "react"; -import posthog from "posthog-js"; -import { useCurrentUser } from "./github"; -import { version } from "../../../package.json"; - -// ============================================================================ -// Configuration -// ============================================================================ - -const POSTHOG_KEY = "phc_vvticSI4cYwo89gWzSwHeLIMC8jgNy5TeYq2THJX3X5"; -const POSTHOG_HOST = "https://us.i.posthog.com"; - -// App version from package.json (injected at build time or read from meta tag) -const APP_VERSION = version; +import { createContext, useContext, type ReactNode } from "react"; // ============================================================================ // Platform Detection @@ -42,18 +23,6 @@ export function isElectron(): boolean { return false; } -function getPlatform(): string { - if (isElectron()) { - // Try to get more specific platform info - const userAgent = navigator.userAgent.toLowerCase(); - if (userAgent.includes("mac")) return "electron-mac"; - if (userAgent.includes("win")) return "electron-windows"; - if (userAgent.includes("linux")) return "electron-linux"; - return "electron"; - } - return "web"; -} - // ============================================================================ // Event Types // ============================================================================ @@ -120,63 +89,11 @@ const TelemetryContext = createContext(null); // Provider // ============================================================================ -export function TelemetryProvider({ children }: { children: ReactNode }) { - const currentUser = useCurrentUser(); - const initializedRef = useRef(false); - const identifiedUserIdRef = useRef(null); - - // Initialize PostHog once - useEffect(() => { - if (initializedRef.current) return; - initializedRef.current = true; - - posthog.init(POSTHOG_KEY, { - api_host: POSTHOG_HOST, - // Don't capture page views automatically - we'll do it manually - capture_pageview: false, - // Respect user's Do Not Track setting - respect_dnt: true, - // Don't persist across sessions if user clears storage - persistence: "localStorage", - // Add default properties to all events - loaded: (ph) => { - ph.register({ - app_version: APP_VERSION, - platform: getPlatform(), - is_electron: isElectron(), - }); - }, - }); - - // Track app opened - posthog.capture("app_opened"); - }, []); - - // Identify user when GitHub user is available - useEffect(() => { - if (currentUser && currentUser.id !== identifiedUserIdRef.current) { - identifiedUserIdRef.current = currentUser.id; - // Use GitHub user ID as the stable identifier - posthog.identify(String(currentUser.id), { - github_id: currentUser.id, - github_username: currentUser.login, - github_name: currentUser.name, - github_email: currentUser.email, - github_avatar_url: currentUser.avatar_url, - github_profile_url: currentUser.html_url, - github_bio: currentUser.bio, - github_company: currentUser.company, - github_location: currentUser.location, - }); - } - }, [currentUser]); - - const track = (event: TelemetryEvent, properties?: TelemetryProperties) => { - posthog.capture(event, properties); - }; +const telemetry: TelemetryContextValue = { track: () => {} }; +export function TelemetryProvider({ children }: { children: ReactNode }) { return ( - + {children} ); diff --git a/wrangler.config.ts b/wrangler.config.ts new file mode 100644 index 0000000..ca738c0 --- /dev/null +++ b/wrangler.config.ts @@ -0,0 +1,8 @@ +import { defineWranglerConfig } from "wrangler/experimental-config"; + +// cf uses Wrangler to bundle the API; the browser assets are built separately with Bun. +export default defineWranglerConfig({ + assetsDirectory: "./dist/browser", + types: { generate: false }, + sendMetrics: false, +}); From 1083c0441d0f1348fd0e1f9bca9066aa46408973 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 14 Oct 2026 14:40:20 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20clear=20outdated=20PR?= =?UTF-8?q?=20results=20when=20filters=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SECURITY.md | 73 +++++++++++++++++++ src/browser/components/home.tsx | 20 +++++- src/browser/contexts/github.test.tsx | 102 +++++++++++++++++++++++++++ src/browser/contexts/github.tsx | 26 +++++-- 4 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 SECURITY.md create mode 100644 src/browser/contexts/github.test.tsx diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..66df788 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,73 @@ +# Security and privacy review + +Reviewed on 2026-10-14 for the internal Cloudflare deployment at +`code.internal.fiet.finance`. This document records the current protections and +open hardening work; it is not a penetration test or a guarantee of security. + +## Telemetry and data recipients + +- PostHog was removed from the application and dependencies. Telemetry calls are + no-ops in `src/browser/contexts/telemetry.tsx`. No other analytics SDK or + intentional reporting to the upstream Pulldash operators was found in the + reviewed source or locally built frontend assets. +- GitHub API requests for PRs, reviews, and file contents go directly from the + browser to GitHub. Diff parsing and highlighting happen locally in browser + workers. The application has no server-side repository database. +- Device authorization is proxied through the team's Cloudflare Worker to + GitHub. The access token returned by GitHub passes through that Worker to the + browser. Authentication responses use `Cache-Control: no-store`; the + application does not persist tokens server-side. +- Cloudflare Worker logs and traces remain enabled in the team's account. + Request URLs, status codes, timings, and related metadata may be recorded. + Repository identifiers can appear in application URLs. No external + observability export, Logpush, or tail consumer was configured for this Worker + at review time. See [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/) + and [Workers traces](https://developers.cloudflare.com/workers/observability/traces/). +- PR descriptions and comments can contain external images. Rendering an image + can send the viewer's IP address and request metadata to its host. The + application does not intentionally attach GitHub authorization headers to + those image requests. Some GitHub images are proxied, but the application does + not enforce a GitHub-only image policy. + +## Confirmed deployment protections + +- Cloudflare Access protects the Worker, including the site and authentication + API. Anonymous HTTP probes redirect to the Access login page. +- The configured allow policy requires an email address under `usher.so`. + The Access session duration is seven days. +- `workers.dev` access and preview URLs are disabled. +- GitHub and the application use HTTPS. Repository permissions are still + enforced by GitHub independently of the Cloudflare Access policy. +- The configured GitHub client ID is public configuration, not a client secret. + +## Open hardening work + +1. **Protect GitHub tokens in the browser.** Tokens are stored in `localStorage` + in `src/browser/contexts/auth.tsx`. JavaScript running on the application's + origin can read them; an XSS vulnerability could expose them. Tokens can + remain stored after a Cloudflare Access session ends. Consider memory-only + storage, or a server-side session with an HttpOnly cookie if stronger token + isolation is required. See [OWASP storage guidance](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#storage-apis). +2. **Clear or isolate private local state.** Normal logout clears the token and + GitHub request cache. Draft comments, review text, viewed filenames, and tab + state persist separately in `localStorage`. Their keys are not scoped to the + GitHub account. Clear sensitive state on logout or isolate it by user, and + handle token replacement/account switching explicitly. +3. **Restrict rendered content and outbound resources.** Add a Content Security + Policy and an explicit referrer policy, restrict external images or require + user consent before loading them, and sanitize GitHub-rendered HTML locally. + The Markdown path uses `rehype-sanitize`; the pre-rendered HTML path currently + relies on GitHub's sanitization instead of applying a local sanitizer. +4. **Review permissions and logging.** Verify the team's GitHub App ownership, + device-flow setting, permissions, and installations. Prefer the minimum + repository access needed. Decide whether full Worker log/trace sampling is + necessary and whether URL query strings should be redacted. + +## Review scope and limitations + +The review covered application source, locally built frontend assets, the active +Worker's API module, Cloudflare deployment settings, Access policy, and anonymous +HTTP probes. It did not inspect an authenticated browser's live network traffic, +download the deployed frontend assets, or verify the GitHub App's permissions in +its management settings. Findings should be revisited after deployment, +authentication, rendering, or dependency changes. diff --git a/src/browser/components/home.tsx b/src/browser/components/home.tsx index 8892a5b..af3df44 100644 --- a/src/browser/components/home.tsx +++ b/src/browser/components/home.tsx @@ -975,8 +975,24 @@ export function Home() { Sign in with GitHub - ) : loadingPrs || - (config.repos.length > 0 && prs.length === 0 && !prList.error) ? ( + ) : prList.error ? ( +
+ +

+ Failed to load pull requests +

+

+ {prList.error} +

+ +
+ ) : loadingPrs ? ( ) : prs.length === 0 ? (
diff --git a/src/browser/contexts/github.test.tsx b/src/browser/contexts/github.test.tsx new file mode 100644 index 0000000..0792d06 --- /dev/null +++ b/src/browser/contexts/github.test.tsx @@ -0,0 +1,102 @@ +import { expect, spyOn, test } from "bun:test"; +import { createGitHubStore, type PRSearchResult } from "./github"; + +test("switching to Open clears closed results even when the new search fails", async () => { + const closedPR: PRSearchResult = { + id: 1, + number: 1, + title: "Closed PR", + state: "closed", + draft: false, + html_url: "https://github.com/test/repo/pull/1", + repository_url: "https://api.github.com/repos/test/repo", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + user: null, + labels: [], + }; + const queries: string[] = []; + const upstream = spyOn(globalThis, "fetch").mockImplementation( + Object.assign( + async (input: Parameters[0]) => { + const url = new URL(String(input)); + if (url.pathname === "/graphql") { + return Response.json({ data: { pr0: { pullRequest: null } } }); + } + const query = url.searchParams.get("q")!; + queries.push(query); + if (query.includes("is:closed")) { + return Response.json({ items: [closedPR], total_count: 1 }); + } + return Response.json( + { message: "Resource not accessible by integration" }, + { status: 403 } + ); + }, + { preconnect: () => {} } + ) + ); + try { + const store = createGitHubStore(); + store.initializeAnonymous(); + await store.fetchPRList(["is:pr is:closed repo:test/repo"]); + expect(store.getState().prList.items).toEqual([closedPR]); + + const loadingItems: PRSearchResult[][] = []; + const unsubscribe = store.subscribe(() => { + if (store.getState().prList.loading) { + loadingItems.push(store.getState().prList.items); + } + }); + await store.fetchPRList(["is:pr is:open repo:test/repo"]); + unsubscribe(); + expect(loadingItems).toEqual([[]]); + expect(store.getState().prList.items).toEqual([]); + expect(store.getState().prList.totalCount).toBe(0); + expect(store.getState().prList.loading).toBe(false); + expect(store.getState().prList.error).toContain( + "Resource not accessible by integration" + ); + expect(queries).toEqual([ + "is:pr is:closed repo:test/repo", + "is:pr is:open repo:test/repo", + ]); + store.reset(); + } finally { + upstream.mockRestore(); + } +}); + +test("an unsuccessful PR search can be retried and settle with no open PRs", async () => { + let requests = 0; + const upstream = spyOn(globalThis, "fetch").mockImplementation( + Object.assign( + async () => { + requests++; + return requests === 1 + ? Response.json({ message: "Search unavailable" }, { status: 503 }) + : Response.json({ items: [], total_count: 0 }); + }, + { preconnect: () => {} } + ) + ); + try { + const store = createGitHubStore(); + store.initializeAnonymous(); + const queries = ["is:pr is:open repo:test/repo"]; + await store.fetchPRList(queries); + expect(store.getState().prList.error).toContain("Search unavailable"); + + await store.fetchPRList(queries, 1, 30, { backgroundRefresh: true }); + expect(store.getState().prList).toMatchObject({ + items: [], + totalCount: 0, + loading: false, + error: null, + }); + expect(requests).toBe(2); + store.reset(); + } finally { + upstream.mockRestore(); + } +}); diff --git a/src/browser/contexts/github.tsx b/src/browser/contexts/github.tsx index d67b08c..ff2020d 100644 --- a/src/browser/contexts/github.tsx +++ b/src/browser/contexts/github.tsx @@ -283,7 +283,10 @@ class RequestCache { setPending(key: string, promise: Promise): void { this.pending.set(key, { promise, timestamp: Date.now() }); - promise.finally(() => this.pending.delete(key)); + promise.then( + () => this.pending.delete(key), + () => this.pending.delete(key) + ); } clearPending(key: string): void { @@ -435,7 +438,7 @@ type Listener = () => void; // GitHub Store - Combines API client + state management // ============================================================================ -function createGitHubStore() { +export function createGitHubStore() { let state: GitHubState = { ready: false, error: null, @@ -672,7 +675,11 @@ function createGitHubStore() { return; } - const cacheKey = `prlist:${queries.sort().join("|")}:${page}:${perPage}`; + const queryKey = [...queries].sort().join("|"); + const selectionChanged = + queryKey !== [...state.prListQueries].sort().join("|") || + page !== state.prListPage; + const cacheKey = `prlist:${queryKey}:${page}:${perPage}`; const FRESH_TTL = 30_000; // 30 seconds // Check for stale data - show immediately if we have any @@ -694,10 +701,17 @@ function createGitHubStore() { }); // If fresh, don't revalidate if (!stale.isStale) return; - } else if (!backgroundRefresh) { - // Only show loading state if this is not a background refresh + } else { + // Results from another filter/page must not survive a failed search. setState((s) => ({ - prList: { ...s.prList, loading: true, error: null }, + prList: { + ...s.prList, + items: selectionChanged ? [] : s.prList.items, + totalCount: selectionChanged ? 0 : s.prList.totalCount, + lastFetchedAt: selectionChanged ? null : s.prList.lastFetchedAt, + loading: selectionChanged || !backgroundRefresh, + error: null, + }, prListQueries: queries, prListPage: page, }));