diff --git a/.github/workflows/build-and-docs.yml b/.github/workflows/build-and-docs.yml index d4bd260da..89fc27371 100644 --- a/.github/workflows/build-and-docs.yml +++ b/.github/workflows/build-and-docs.yml @@ -18,13 +18,16 @@ on: push: branches: - main + - '*-webtest' pull_request: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +# and creation of Discussions for conjectures (see site/create_discussions.js) permissions: contents: read pages: write id-token: write + discussions: write jobs: build: @@ -37,7 +40,20 @@ jobs: with: fetch-depth: 0 + - name: Detect webtest mode + id: mode + run: | + if [[ "${{ github.ref_name }}" == *-webtest ]]; then + echo "webtest=true" >> "$GITHUB_OUTPUT" + echo "::notice::Webtest mode: skipping Lean build, downloading JSON from live site" + else + echo "webtest=false" >> "$GITHUB_OUTPUT" + fi + + # ---- Full Lean build (skipped on -webtest branches) ---- + - name: Install elan + if: steps.mode.outputs.webtest == 'false' run: | set -o pipefail curl -sSfL https://github.com/leanprover/elan/releases/download/v1.4.2/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz @@ -45,6 +61,7 @@ jobs: echo "$HOME/.elan/bin" >> $GITHUB_PATH - name: Generate All.lean + if: steps.mode.outputs.webtest == 'false' run: | lake exe mk_all --lib FormalConjectures || true # Avoid including Test files from `Util/` to avoid inflating the @@ -53,6 +70,7 @@ jobs: grep -v "FormalConjectures\.Util\." FormalConjectures.lean > FormalConjectures/All.lean - name: Restore ~/.cache/mathlib + if: steps.mode.outputs.webtest == 'false' uses: actions/cache/restore@v3 with: path: ~/.cache/mathlib @@ -62,15 +80,18 @@ jobs: oleans- - name: Get olean cache + if: steps.mode.outputs.webtest == 'false' run: | lake exe cache unpack lake exe cache get - name: Build project + if: steps.mode.outputs.webtest == 'false' run: | lake build - name: Build literate source pages + if: steps.mode.outputs.webtest == 'false' run: | cd docbuild # Some modules crash verso-literate (e.g. metaprogramming-heavy util files), @@ -82,27 +103,32 @@ jobs: lake exe verso-html .lake/build/literate ../_literate_html || true - name: Post-process literate HTML + if: steps.mode.outputs.webtest == 'false' run: | python3 site/fix_literate_html.py _literate_html - name: Pack olean cache + if: steps.mode.outputs.webtest == 'false' run: | lake exe cache pack ls ~/.cache/mathlib - name: Save ~/.cache/mathlib + if: steps.mode.outputs.webtest == 'false' uses: actions/cache/save@v3 with: path: ~/.cache key: oleans-${{ hashFiles('lake-manifest.json') }}-${{ hashFiles('**/*.lean') }} - name: Prepare documentation + if: steps.mode.outputs.webtest == 'false' run: | cd docbuild export MATHLIB_NO_CACHE_ON_UPDATE=1 # avoids "Failed to prune ProofWidgets cloud release: no such file or directory" lake update formal_conjectures - name: Cache documentation + if: steps.mode.outputs.webtest == 'false' uses: actions/cache@v5 with: path: docbuild/.lake/build/doc @@ -112,11 +138,13 @@ jobs: MathlibDoc- - name: Build documentation + if: steps.mode.outputs.webtest == 'false' run: | cd docbuild lake build FormalConjectures:docs - name: Extract documentation + if: steps.mode.outputs.webtest == 'false' run: | cd docbuild mkdir out @@ -128,36 +156,71 @@ jobs: rsync -a --files-from=out-files.txt --relative .lake/build/doc ./out - name: Set up Python + if: steps.mode.outputs.webtest == 'false' uses: actions/setup-python@v6 with: python-version: '3.12.9' - name: Install Python dependencies + if: steps.mode.outputs.webtest == 'false' run: | python -m pip install --upgrade pip pip install pandas==2.2.3 numpy==2.2.3 plotly==5.20.0 beautifulsoup4 lxml - name: Run plotting script + if: steps.mode.outputs.webtest == 'false' run: | python docbuild/scripts/plot_growth.py - name: Inject stats into index.html + if: steps.mode.outputs.webtest == 'false' shell: bash run: | cd docbuild lake exe overwrite_index ./out/index.html ./out/file_counts_dark.html ./out/file_counts_white.html | tee -a "$GITHUB_STEP_SUMMARY" + # ---- Webtest mode: download processed JSON from live site ---- + + - name: Download conjectures JSON from live site + if: steps.mode.outputs.webtest == 'true' + run: | + mkdir -p site/data + curl -sSfL https://google-deepmind.github.io/formal-conjectures/data/conjectures.json \ + -o site/data/conjectures.json + echo "::notice::Downloaded $(wc -c < site/data/conjectures.json) bytes from live site" + + # ---- Website build (always runs) ---- + - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '22' - name: Generate conjectures data for website + if: steps.mode.outputs.webtest == 'false' run: | mkdir -p site/data - lake exe extract_names --exclude=statement,docstring,formalProofKind,formalProofLink,moduleDocstrings > site/data/conjectures.json || true + lake exe extract_names > site/data/conjectures.json || true + + - name: Inject voting config + run: | + REPO_OWNER="${{ github.repository_owner }}" + REPO_NAME="${{ github.event.repository.name }}" + REPO_ID=$(curl -sSf -H "Authorization: Bearer ${{ github.token }}" \ + "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}" | jq -r '.node_id') + if [ -z "$REPO_ID" ] || [ "$REPO_ID" = "null" ]; then + echo "::error::Failed to fetch repo node_id for ${REPO_OWNER}/${REPO_NAME}" + exit 1 + fi + echo "::notice::Configuring voting for ${REPO_OWNER}/${REPO_NAME} (${REPO_ID})" + sed -i "s|REPLACE_WITH_PROXY_URL|https://formal-conjectures-web-worker.uc.r.appspot.com|" site/src/js/voting.js + sed -i "s|REPLACE_WITH_CLIENT_ID|Iv23lid2mjCGp7EIKrJn|" site/src/js/voting.js + sed -i "s|REPLACE_WITH_REPO_OWNER|${REPO_OWNER}|" site/src/js/voting.js + sed -i "s|REPLACE_WITH_REPO_NAME|${REPO_NAME}|" site/src/js/voting.js + sed -i "s|REPLACE_WITH_REPO_ID|${REPO_ID}|" site/src/js/voting.js - name: Extract Verso fragments for website + if: steps.mode.outputs.webtest == 'false' run: | python3 site/extract_verso_fragments.py _literate_html site/data/verso-fragments.json @@ -166,9 +229,19 @@ jobs: cd site node build.js env: - BASE_PATH: /formal-conjectures + BASE_PATH: /${{ github.event.repository.name }} - - name: Assemble deploy artifact + - name: Create missing discussions + if: github.ref == 'refs/heads/main' || endsWith(github.ref_name, '-webtest') + run: | + node site/create_discussions.js "${{ github.repository_owner }}" "${{ github.event.repository.name }}" + env: + GITHUB_TOKEN: ${{ github.token }} + + # ---- Deploy artifact ---- + + - name: Assemble deploy artifact (full) + if: steps.mode.outputs.webtest == 'false' run: | mkdir -p _deploy # Website goes at root @@ -178,6 +251,12 @@ jobs: # Literate source pages go under /src cp -r _literate_html _deploy/src + - name: Assemble deploy artifact (webtest) + if: steps.mode.outputs.webtest == 'true' + run: | + mkdir -p _deploy + cp -r site/site/* _deploy/ + - name: Upload deploy artifact id: deployment uses: actions/upload-pages-artifact@v4 @@ -186,7 +265,7 @@ jobs: # Deployment job deploy: - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || endsWith(github.ref_name, '-webtest') environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -196,3 +275,6 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + - name: Print site URL + run: echo "::notice::Site deployed to ${{ steps.deployment.outputs.page_url }}" diff --git a/.gitignore b/.gitignore index 67bbac8d2..65faa1087 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .cache # `lake` stores its files here: .lake +# App Engine secrets (never commit real credentials) +site/appengine/app.secrets.yaml # Verso literate build output (generated by `lake build :literate` + `verso-html`) _literate_html/ diff --git a/site/appengine/.gcloudignore b/site/appengine/.gcloudignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/site/appengine/.gcloudignore @@ -0,0 +1 @@ +node_modules/ diff --git a/site/appengine/README.md b/site/appengine/README.md new file mode 100644 index 000000000..43ebc0d2a --- /dev/null +++ b/site/appengine/README.md @@ -0,0 +1,106 @@ +# App Engine Proxy + +Handles GitHub App OAuth token exchange and anonymous discussion reads for the voting system. One deployment serves all forks. + +For an overview of the voting system, see [`docs/voting.md`](../docs/voting.md). + +## Shared Proxy + +The default deployment at `formal-conjectures-web-worker.uc.r.appspot.com` is used automatically by the CI workflow. Forkers don't need their own proxy — just install the [formal-conjectures-voting](https://github.com/apps/formal-conjectures-voting) GitHub App on their fork. + +The shared proxy: +- Uses GitHub App installation tokens (works for any repo where the app is installed) +- Only serves `google-deepmind/formal-conjectures` and its forks +- Allows any `*.github.io` origin via CORS +- Routes OAuth callbacks so a single registered callback URL works for all forks + +## Running Your Own Proxy + +### Prerequisites + +- [Node.js](https://nodejs.org/) >= 22 +- [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) +- A GCP project with App Engine enabled + +### Create a GitHub App + +1. Go to https://github.com/settings/apps/new +2. Set **Callback URL** to `https://YOUR_PROJECT.REGION.r.appspot.com/oauth/callback` +3. Uncheck **Webhook > Active** +4. Under **Repository permissions**, set **Discussions** to **Read & write** +5. Click **Create GitHub App** +6. Note the **App ID**, copy the **Client ID** +7. Generate a **client secret** and a **private key** (PEM file) +8. Install the app on your target repo(s) + +### Store Secrets + +```bash +gcloud config set project YOUR_PROJECT +gcloud services enable secretmanager.googleapis.com + +echo -n "your_client_id" | gcloud secrets create GH_CLIENT_ID --data-file=- +echo -n "your_client_secret" | gcloud secrets create GH_CLIENT_SECRET --data-file=- +gcloud secrets create GH_APP_PRIVATE_KEY --data-file=path/to/private-key.pem + +PROJECT_ID=$(gcloud config get-value project) +for SECRET in GH_CLIENT_ID GH_CLIENT_SECRET GH_APP_PRIVATE_KEY; do + gcloud secrets add-iam-policy-binding $SECRET \ + --member="serviceAccount:${PROJECT_ID}@appspot.gserviceaccount.com" \ + --role="roles/secretmanager.secretAccessor" +done +``` + +### Deploy + +Edit `app.yaml` to set `GH_APP_ID` to your App ID, then: + +```bash +cd site/appengine +npm install +gcloud app deploy +``` + +Update `WORKER_URL` in `voting.js` to your App Engine URL. + +### Local Development + +```bash +cd site/appengine && npm install +export GH_APP_ID=your_app_id +export GH_CLIENT_ID=your_client_id +export GH_CLIENT_SECRET=your_client_secret +export GH_APP_PRIVATE_KEY="$(cat path/to/private-key.pem)" +export ALLOWED_ORIGIN=http://localhost:8000 +node server.js +``` + +Proxy runs on `http://localhost:8080`. Secrets from env vars skip Secret Manager. + +## API Endpoints + +### `POST /token` + +Exchanges a GitHub OAuth `code` for an access token. Body: `{ "code": "..." }`. + +### `GET /oauth/callback?return_to=URL` + +OAuth redirect bounce. Validates `return_to` is `*.github.io` or localhost, appends the `code` parameter, and redirects. + +### `GET /discussions?owner=OWNER&repo=REPO` + +Aggregated discussion data (votes, predictions, difficulty). Uses GitHub App installation tokens. Cached 60 seconds. Returns 403 if the repo is not `google-deepmind/formal-conjectures` or a fork of it. + +## Configuration + +| Variable | Source | Description | +|---|---|---| +| `ALLOWED_ORIGIN` | `app.yaml` / env | Extra CORS origins (`.github.io` allowed automatically) | +| `GH_APP_ID` | `app.yaml` / env | GitHub App ID | +| `GH_CLIENT_ID` | Secret Manager / env | GitHub App client ID | +| `GH_CLIENT_SECRET` | Secret Manager / env | GitHub App client secret | +| `GH_APP_PRIVATE_KEY` | Secret Manager / env | GitHub App private key (PEM) | + +## Cost + +App Engine F1 instances include 28 free instance-hours/day. With `max_instances: 1`, this stays within the free tier. diff --git a/site/appengine/app.yaml b/site/appengine/app.yaml new file mode 100644 index 000000000..da0a11fd0 --- /dev/null +++ b/site/appengine/app.yaml @@ -0,0 +1,14 @@ +runtime: nodejs22 + +instance_class: F1 + +automatic_scaling: + max_instances: 1 + +env_variables: + GH_APP_ID: "3005907" + # CORS: any *.github.io origin is allowed automatically. + # ALLOWED_ORIGIN is only needed for additional origins (e.g. custom domains). + ALLOWED_ORIGIN: "" + # Secrets (GH_CLIENT_ID, GH_CLIENT_SECRET, GH_APP_PRIVATE_KEY) are loaded + # from Google Cloud Secret Manager at startup. See README.md for setup. diff --git a/site/appengine/package-lock.json b/site/appengine/package-lock.json new file mode 100644 index 000000000..c232c99b9 --- /dev/null +++ b/site/appengine/package-lock.json @@ -0,0 +1,1754 @@ +{ + "name": "fc-oauth-proxy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fc-oauth-proxy", + "dependencies": { + "@google-cloud/secret-manager": "^5.6.0", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@google-cloud/secret-manager": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-5.6.0.tgz", + "integrity": "sha512-0daW/OXQEVc6VQKPyJTQNyD+563I/TYQ7GCQJx4dq3lB666R9FUPvqHx9b/o/qQtZ5pfuoCbGZl3krpxgTSW8Q==", + "dependencies": { + "google-gax": "^4.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/site/appengine/package.json b/site/appengine/package.json new file mode 100644 index 000000000..123f80366 --- /dev/null +++ b/site/appengine/package.json @@ -0,0 +1,15 @@ +{ + "name": "fc-oauth-proxy", + "private": true, + "engines": { + "node": ">=22" + }, + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "@google-cloud/secret-manager": "^5.6.0", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.0" + } +} diff --git a/site/appengine/server.js b/site/appengine/server.js new file mode 100644 index 000000000..b534f544f --- /dev/null +++ b/site/appengine/server.js @@ -0,0 +1,408 @@ +'use strict'; + +const express = require('express'); +const jwt = require('jsonwebtoken'); + +const app = express(); +app.use(express.json()); + +// --------------------------------------------------------------------------- +// Configuration — loaded at startup, secrets filled in by loadSecrets() +// --------------------------------------------------------------------------- +const config = { + ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN || '*', + GH_APP_ID: process.env.GH_APP_ID || '', + GH_CLIENT_ID: process.env.GH_CLIENT_ID || '', + GH_CLIENT_SECRET: process.env.GH_CLIENT_SECRET || '', + GH_APP_PRIVATE_KEY: process.env.GH_APP_PRIVATE_KEY || '', +}; + +// Secrets to load from Secret Manager when not set via env vars +const SECRET_NAMES = ['GH_CLIENT_ID', 'GH_CLIENT_SECRET', 'GH_APP_PRIVATE_KEY']; + +/** + * Load any missing secrets from Google Cloud Secret Manager. + * Env vars take precedence (for local dev). In production on App Engine, + * secrets come from Secret Manager. + */ +async function loadSecrets() { + const missing = SECRET_NAMES.filter(name => !config[name]); + if (missing.length === 0) return; + + let client; + try { + const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); + client = new SecretManagerServiceClient(); + } catch (e) { + console.error('Secret Manager client unavailable and secrets not in env vars.'); + console.error('For local dev, set env vars: ' + missing.join(', ')); + process.exit(1); + } + + const [projectId] = await client.getProjectId().then(id => [id]); + + for (const name of missing) { + try { + const [version] = await client.accessSecretVersion({ + name: `projects/${projectId}/secrets/${name}/versions/latest`, + }); + config[name] = version.payload.data.toString('utf8'); + console.log(` Loaded secret: ${name}`); + } catch (e) { + console.error(`Failed to load secret ${name}: ${e.message}`); + process.exit(1); + } + } +} + +// --------------------------------------------------------------------------- +// GitHub App installation token management +// --------------------------------------------------------------------------- + +// Cache: "owner/repo" -> { token, expiresAt } +const installationTokenCache = new Map(); + +/** + * Create a JWT for the GitHub App, valid for 10 minutes. + */ +function createAppJWT() { + const now = Math.floor(Date.now() / 1000); + return jwt.sign( + { iat: now - 60, exp: now + 600, iss: config.GH_APP_ID }, + config.GH_APP_PRIVATE_KEY, + { algorithm: 'RS256' }, + ); +} + +/** + * Get a GitHub API installation token for the given repo. + * Returns a cached token if still valid (with 5-minute margin). + */ +async function getInstallationToken(owner, repo) { + const cacheKey = `${owner}/${repo}`; + const cached = installationTokenCache.get(cacheKey); + if (cached && cached.expiresAt > Date.now() + 5 * 60 * 1000) { + return cached.token; + } + + const appJWT = createAppJWT(); + + // Find the installation ID for this repo + const installResp = await fetch(`https://api.github.com/repos/${owner}/${repo}/installation`, { + headers: { + Authorization: `Bearer ${appJWT}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'fc-oauth-proxy', + }, + }); + if (!installResp.ok) { + const text = await installResp.text(); + throw new Error(`Failed to find app installation for ${owner}/${repo}: ${installResp.status} ${text}`); + } + const { id: installationId } = await installResp.json(); + + // Create an installation access token + const tokenResp = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, { + method: 'POST', + headers: { + Authorization: `Bearer ${appJWT}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'fc-oauth-proxy', + }, + }); + if (!tokenResp.ok) { + const text = await tokenResp.text(); + throw new Error(`Failed to create installation token: ${tokenResp.status} ${text}`); + } + const { token, expires_at } = await tokenResp.json(); + + installationTokenCache.set(cacheKey, { + token, + expiresAt: new Date(expires_at).getTime(), + }); + + return token; +} + +// --------------------------------------------------------------------------- +// CORS +// --------------------------------------------------------------------------- +function getCorsHeaders(req) { + const origin = req.headers.origin || ''; + // Allow any *.github.io origin (all GitHub Pages sites), plus any + // explicitly listed origins (e.g. localhost for local dev). + const allowed = config.ALLOWED_ORIGIN.split(',').map(s => s.trim()); + const isGitHubPages = /^https:\/\/[a-zA-Z0-9_-]+\.github\.io$/.test(origin); + const corsOrigin = (isGitHubPages || allowed.includes(origin)) ? origin : allowed[0] || '*'; + return { + 'Access-Control-Allow-Origin': corsOrigin, + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }; +} + +// Log all requests +app.use((req, res, next) => { + console.log(`[${req.method}] ${req.path} origin=${req.headers.origin || '(none)'}`); + next(); +}); + +app.options('*', (req, res) => { + res.set(getCorsHeaders(req)).status(204).end(); +}); + +// --------------------------------------------------------------------------- +// GitHub GraphQL helper +// --------------------------------------------------------------------------- +async function ghGraphQL(query, variables, token) { + const resp = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'fc-oauth-proxy', + }, + body: JSON.stringify({ query, variables }), + }); + if (!resp.ok) throw new Error(`GraphQL request failed: ${resp.status}`); + const json = await resp.json(); + if (json.errors) throw new Error(json.errors[0].message); + return json.data; +} + +// --------------------------------------------------------------------------- +// Repo validation — only serve repos that are formal-conjectures or forks of it +// --------------------------------------------------------------------------- +const UPSTREAM_FULL_NAME = 'google-deepmind/formal-conjectures'; +const validatedRepos = new Map(); // "owner/repo" -> true/false + +async function isAllowedRepo(owner, repo) { + const key = `${owner}/${repo}`; + if (validatedRepos.has(key)) return validatedRepos.get(key); + + if (key === UPSTREAM_FULL_NAME) { + validatedRepos.set(key, true); + return true; + } + + // Check if the repo is a fork of the upstream + try { + const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { + headers: { + 'User-Agent': 'fc-oauth-proxy', + Accept: 'application/vnd.github+json', + }, + }); + if (!resp.ok) { + validatedRepos.set(key, false); + return false; + } + const data = await resp.json(); + const allowed = data.fork && data.source && data.source.full_name === UPSTREAM_FULL_NAME; + validatedRepos.set(key, allowed); + return allowed; + } catch (e) { + return false; + } +} + +// --------------------------------------------------------------------------- +// Fetch all discussions +// --------------------------------------------------------------------------- +async function fetchAllDiscussions(owner, name) { + const token = await getInstallationToken(owner, name); + const result = {}; + + let hasNextPage = true; + let afterCursor = null; + + while (hasNextPage) { + const data = await ghGraphQL(` + query($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + discussions(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + id + number + title + reactions(content: HEART) { totalCount } + thumbsUpReactions: reactions(content: THUMBS_UP) { totalCount } + thumbsDownReactions: reactions(content: THUMBS_DOWN) { totalCount } + comments(first: 100) { + pageInfo { hasNextPage endCursor } + nodes { + body + author { login } + } + } + } + } + } + } + `, { owner, name, after: afterCursor }, token); + + const discussions = data.repository.discussions; + + for (const disc of discussions.nodes) { + let allComments = [...disc.comments.nodes]; + let commentPage = disc.comments.pageInfo; + while (commentPage.hasNextPage) { + const cData = await ghGraphQL(` + query($discId: ID!, $after: String) { + node(id: $discId) { + ... on Discussion { + comments(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + body + author { login } + } + } + } + } + } + `, { discId: disc.id, after: commentPage.endCursor }, token); + const moreComments = cData.node.comments; + allComments = allComments.concat(moreComments.nodes); + commentPage = moreComments.pageInfo; + } + + const difficultyByUser = {}; + for (const comment of allComments) { + if (!comment.author) continue; + const lines = comment.body.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (/^difficulty [0-9]$/i.test(trimmed)) { + difficultyByUser[comment.author.login] = parseInt(trimmed.split(' ')[1], 10); + } + } + } + + const values = Object.values(difficultyByUser); + const numRatings = values.length; + const avgDifficulty = numRatings > 0 + ? Math.round((values.reduce((a, b) => a + b, 0) / numRatings) * 10) / 10 + : null; + + result[disc.title] = { + count: disc.reactions.totalCount, + thumbsUp: disc.thumbsUpReactions.totalCount, + thumbsDown: disc.thumbsDownReactions.totalCount, + avgDifficulty, + numRatings, + discussionId: disc.id, + discussionNumber: disc.number, + }; + } + + hasNextPage = discussions.pageInfo.hasNextPage; + afterCursor = discussions.pageInfo.endCursor; + } + + return result; +} + +// --------------------------------------------------------------------------- +// Routes +// --------------------------------------------------------------------------- + +// GET /oauth/callback — bounce OAuth code back to the user's site. +// GitHub passes the return URL through the OAuth `state` parameter. +app.get('/oauth/callback', (req, res) => { + const returnTo = req.query.state; + const code = req.query.code; + if (!returnTo || !code) { + return res.status(400).send('Missing state or code parameter'); + } + + // Only redirect to *.github.io or localhost (prevent open redirect) + let target; + try { + target = new URL(returnTo); + } catch (e) { + return res.status(400).send('Invalid return URL in state parameter'); + } + const isGitHubPages = /^[a-zA-Z0-9_-]+\.github\.io$/.test(target.hostname); + const isLocalhost = target.hostname === 'localhost' || target.hostname === '127.0.0.1'; + if (!isGitHubPages && !isLocalhost) { + return res.status(403).send('Return URL must be a *.github.io or localhost URL'); + } + + target.searchParams.set('code', code); + res.redirect(target.toString()); +}); + +// POST /token — OAuth code exchange +app.post('/token', async (req, res) => { + const cors = getCorsHeaders(req); + res.set(cors); + + const { code } = req.body || {}; + if (!code) { + return res.status(400).json({ error: 'Missing code parameter' }); + } + + try { + const ghResponse = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + client_id: config.GH_CLIENT_ID, + client_secret: config.GH_CLIENT_SECRET, + code, + }), + }); + + if (!ghResponse.ok) { + return res.status(502).json({ error: 'GitHub token exchange failed' }); + } + + const data = await ghResponse.json(); + res.json(data); + } catch (e) { + console.error('Token exchange error:', e); + res.status(500).json({ error: 'Token exchange failed' }); + } +}); + +// GET /discussions?owner=X&repo=Y — read-only proxy for anonymous users +app.get('/discussions', async (req, res) => { + const cors = getCorsHeaders(req); + res.set(cors); + + const owner = req.query.owner; + const repo = req.query.repo; + if (!owner || !repo) { + return res.status(400).json({ error: 'Missing owner or repo query parameter' }); + } + + if (!await isAllowedRepo(owner, repo)) { + return res.status(403).json({ error: 'Repository is not formal-conjectures or a fork of it' }); + } + + try { + const data = await fetchAllDiscussions(owner, repo); + res.set('Cache-Control', 'public, max-age=60').json(data); + } catch (e) { + console.error('Failed to fetch discussions:', e); + res.status(500).json({ error: 'Failed to fetch discussions' }); + } +}); + +// Fallback 404 +app.use((req, res) => { + res.set(getCorsHeaders(req)).status(404).json({ error: 'Not found' }); +}); + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- +const PORT = process.env.PORT || 8080; + +loadSecrets().then(() => { + app.listen(PORT, () => { + console.log(`fc-oauth-proxy listening on port ${PORT}`); + }); +}); diff --git a/site/build.js b/site/build.js index 0b881b847..f609c2c7c 100644 --- a/site/build.js +++ b/site/build.js @@ -175,13 +175,8 @@ function processEntry(entry) { code, name: AMS_SUBJECTS[parseInt(code, 10)] || `AMS ${code}`, })); - // Pick only the fields the website actually uses. Avoids leaking large - // unused fields (statement, docstring, formalProofKind, formalProofLink) - // into the client-side JSON. Docstrings come from versoFragments instead. return { - theorem: entry.theorem, - module: entry.module, - category: entry.category, + ...entry, displayTheorem: entry.theorem.replace(/[«»]/g, ''), displayModule: entry.module.replace(/[«»]/g, ''), githubPath: moduleToGitHubPath(entry.module), @@ -298,21 +293,30 @@ function subjectListHTML(bySubject) { function main() { console.log('Building Formal Conjectures website...'); - // Read raw data - let rawData = []; + // Read data — accepts either the raw extract_names format ({ problems: [...] }) + // or the already-processed format ({ conjectures: [...], stats: {...} }). + let conjectures = []; + let stats = null; if (fs.existsSync('data/conjectures.json')) { const parsed = JSON.parse(fs.readFileSync('data/conjectures.json', 'utf8')); - // extract_names outputs { problems: [...], moduleDocstrings: {...} } - rawData = parsed.problems || []; + if (parsed.problems) { + // Raw extract_names format + conjectures = parsed.problems.map(processEntry); + } else if (parsed.conjectures) { + // Already-processed format (e.g. downloaded from the live site) + conjectures = parsed.conjectures; + stats = parsed.stats || null; + console.log(' Using pre-processed data.'); + } } - if (rawData.length === 0) { - console.error('Error: no conjectures loaded. Run `lake exe extract_names > site/data/conjectures.json` first.'); + if (conjectures.length === 0) { + console.error('Error: no conjectures loaded. Run `lake exe extract_names > site/data/conjectures.json` first,'); + console.error('or download the processed JSON from the live site into data/conjectures.json.'); process.exit(1); } - const conjectures = rawData.map(processEntry); - const stats = computeStats(conjectures); + if (!stats) stats = computeStats(conjectures); // Load Verso literate fragments (module docstrings + const links) let versoFragments = { moduleDocs: {}, constLinks: {} }; diff --git a/site/create_discussions.js b/site/create_discussions.js new file mode 100644 index 000000000..80649c691 --- /dev/null +++ b/site/create_discussions.js @@ -0,0 +1,204 @@ +#!/usr/bin/env node +/** + * create_discussions.js — Pre-create GitHub Discussions for conjectures. + * + * Reads the processed conjectures JSON, compares against existing discussions, + * and creates a Discussion for each conjecture that doesn't have one yet. + * + * Usage: + * GITHUB_TOKEN= node create_discussions.js + * + * The token needs Discussions read+write permission. In GitHub Actions, use + * the built-in GITHUB_TOKEN with `discussions: write` in the workflow + * permissions block: + * + * permissions: + * discussions: write + * + * Then pass it as: + * env: + * GITHUB_TOKEN: ${{ github.token }} + */ + +'use strict'; + +const CATEGORY_NAME = 'Problems'; + +async function graphql(query, variables, token) { + const resp = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'fc-create-discussions', + }, + body: JSON.stringify({ query, variables }), + }); + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`GraphQL ${resp.status}: ${text}`); + } + const json = await resp.json(); + if (json.errors) throw new Error(json.errors[0].message); + return json.data; +} + +async function getRepoId(owner, repo, token) { + const data = await graphql(` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { id } + } + `, { owner, repo }, token); + return data.repository.id; +} + +async function getCategoryId(owner, repo, token) { + const data = await graphql(` + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + discussionCategories(first: 25) { + nodes { id name } + } + } + } + `, { owner, repo }, token); + const match = data.repository.discussionCategories.nodes.find( + c => c.name === CATEGORY_NAME + ); + if (!match) { + throw new Error( + `Discussion category "${CATEGORY_NAME}" not found. ` + + `Create it in the repository settings under Discussions.` + ); + } + return match.id; +} + +async function getExistingDiscussionTitles(owner, repo, token) { + const titles = new Set(); + let hasNextPage = true; + let after = null; + + while (hasNextPage) { + const data = await graphql(` + query($owner: String!, $repo: String!, $after: String) { + repository(owner: $owner, name: $repo) { + discussions(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { title } + } + } + } + `, { owner, repo, after }, token); + + for (const d of data.repository.discussions.nodes) { + titles.add(d.title); + } + hasNextPage = data.repository.discussions.pageInfo.hasNextPage; + after = data.repository.discussions.pageInfo.endCursor; + } + + return titles; +} + +async function createDiscussion(repoId, categoryId, title, body, token) { + await graphql(` + mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) { + createDiscussion(input: { + repositoryId: $repoId, + categoryId: $categoryId, + title: $title, + body: $body + }) { + discussion { number } + } + } + `, { repoId, categoryId, title, body }, token); +} + +async function main() { + const token = process.env.GITHUB_TOKEN; + if (!token) { + console.error('GITHUB_TOKEN environment variable is required.'); + process.exit(1); + } + + const [owner, repo] = process.argv.slice(2); + if (!owner || !repo) { + console.error('Usage: GITHUB_TOKEN= node create_discussions.js '); + process.exit(1); + } + + // Load conjectures + const fs = require('fs'); + const dataPath = require('path').join(__dirname, 'data', 'conjectures.json'); + if (!fs.existsSync(dataPath)) { + console.error(`Data file not found: ${dataPath}`); + process.exit(1); + } + const raw = JSON.parse(fs.readFileSync(dataPath, 'utf8')); + const conjectures = raw.conjectures || raw.problems || raw; + if (!Array.isArray(conjectures) || conjectures.length === 0) { + console.error('No conjectures found in data file.'); + process.exit(1); + } + + console.log(`Loaded ${conjectures.length} conjectures.`); + + // Get repo metadata + const [repoId, categoryId, existing] = await Promise.all([ + getRepoId(owner, repo, token), + getCategoryId(owner, repo, token), + getExistingDiscussionTitles(owner, repo, token), + ]); + + console.log(`Found ${existing.size} existing discussions.`); + + // Find missing discussions + const theoremNames = conjectures.map(c => c.theorem); + const missing = theoremNames.filter(name => !existing.has(name)); + console.log(`${missing.length} discussions to create.`); + + if (missing.length === 0) { + console.log('All discussions already exist.'); + return; + } + + // Create missing discussions with rate limiting. + // GitHub's secondary rate limit for mutations requires a delay between each. + // After hitting the limit, we do a long cooldown before resuming. + const DELAY_MS = 2000; + const COOLDOWN_MS = 120000; // 2 minutes after a rate limit hit + let created = 0; + let errors = 0; + for (const name of missing) { + const shortName = name.split('.').pop(); + const body = `Discussion for theorem **${shortName}**.\n\nFull Lean name: \`${name}\``; + let success = false; + while (!success) { + try { + await createDiscussion(repoId, categoryId, name, body, token); + created++; + success = true; + if (created % 50 === 0) { + console.log(` Created ${created}/${missing.length}...`); + } + } catch (e) { + if (e.message.includes('too quickly') || e.message.includes('rate limit') || e.message.includes('abuse')) { + console.log(` Rate limited at ${created}/${missing.length}, cooling down ${COOLDOWN_MS / 1000}s...`); + await new Promise(r => setTimeout(r, COOLDOWN_MS)); + } else { + console.error(` Failed to create discussion for ${name}: ${e.message}`); + errors++; + break; + } + } + } + // Delay between every creation to stay under secondary rate limits + await new Promise(r => setTimeout(r, DELAY_MS)); + } + + console.log(`Done. Created ${created}, errors ${errors}, total ${existing.size + created}.`); +} + +main(); diff --git a/site/docs/voting.md b/site/docs/voting.md index 688e48c96..db0573949 100644 --- a/site/docs/voting.md +++ b/site/docs/voting.md @@ -1,70 +1,134 @@ # Voting System -The Formal Conjectures website includes a voting and difficulty rating system. Votes and ratings are stored in Cloudflare KV via a worker. GitHub OAuth is used only for identity verification — the consent screen requires zero permissions ("Verify your GitHub identity"). +The Formal Conjectures website includes a voting, truth prediction, and difficulty rating system backed by GitHub Discussions. GitHub OAuth is used for identity verification — the consent screen requires zero additional OAuth scopes. ## Architecture ``` Browser (voting.js) - └── All operations → Cloudflare Worker (worker/) - ├── OAuth token exchange - ├── Vote and difficulty CRUD - └── Cloudflare KV storage + ├── Reads: GET /discussions?owner=X&repo=Y → App Engine proxy → GitHub GraphQL + └── Writes: directly → GitHub GraphQL (user's OAuth token) + ├── Likes = HEART reactions on Discussion + ├── Truth predictions = THUMBS_UP / THUMBS_DOWN reactions + └── Difficulty ratings = comments matching /^difficulty [0-9]$/i ``` -No GitHub API calls are made from the browser except `/user` during OAuth login to fetch the username. See [`worker/README.md`](../worker/README.md) for API endpoints, KV data model, and worker setup. +All data is stored as native GitHub Discussions features (reactions and comments) on the repository. There is no separate database. + +A shared App Engine proxy (`formal-conjectures-web-worker`) handles OAuth token exchange and anonymous discussion reads. It uses GitHub App installation tokens, so it works automatically for any repo where the [formal-conjectures-voting](https://github.com/apps/formal-conjectures-voting) app is installed. The proxy only serves `google-deepmind/formal-conjectures` and its forks. + +OAuth callbacks are also routed through the proxy, so a single registered callback URL works for all forks — no per-fork GitHub App configuration is needed. + +## How It Works for Forkers + +To deploy the website with voting on your fork: + +1. **Enable GitHub Pages** on the fork (Settings > Pages > GitHub Actions) +2. **Enable Discussions** on the fork (Settings > General > Features) +3. **Create a "Problems" discussion category** (Discussions > Categories > New category, name it exactly `Problems`, choose the "Open-ended discussion" format) +4. **Install the GitHub App**: https://github.com/apps/formal-conjectures-voting/installations/new +5. **Push a branch ending in `-webtest`** (e.g. `my-feature-webtest`) + +The CI workflow configures everything automatically. No GCP project, secrets, or tokens are needed. + +If the GitHub App is not installed, vote counts won't load but the rest of the site works normally. + +## Webtest Branches + +Branches whose name ends with `-webtest` trigger a fast website-only build: + +- The Lean build, docgen, and `extract_names` steps are **skipped** +- The conjectures JSON is **downloaded from the live site** +- Voting configuration is **injected automatically** from the repository context +- The site is **deployed to GitHub Pages** + +This works on any repo, including upstream. On `main`, the full Lean build runs and voting is also configured. + +Branches without the `-webtest` suffix are unaffected — PRs and feature branches run the normal build without deploying. ## How Votes Work -- Each user can vote once per theorem (like/unlike toggle) -- Deduplication is handled via a `voters` array in the KV value -- When a vote is removed and there are no remaining votes or ratings, the KV key is deleted +- Each user can vote once per theorem (like/unlike toggle via HEART reaction) +- Discussions are created lazily when a logged-in user first views a theorem's discussion link, or when they first vote/predict/rate +- Discussion titles are the fully-qualified Lean theorem name + +## Truth Predictions + +- Users can predict whether a conjecture is true (thumbs up) or false (thumbs down) +- Each user can have one prediction per theorem; changing removes the old one +- The browse page shows aggregated prediction counts ## Difficulty Ratings -- Users can independently rate the difficulty of each theorem on a 0–10 scale -- Each user can rate each theorem once; submitting again overwrites the previous rating +- Users can rate the difficulty of each theorem on a 0-9 scale +- Ratings are stored as discussion comments containing only `difficulty N` +- Submitting a new rating deletes the previous single-line rating comment - The browse page shows the average difficulty alongside the vote count -- The theorem detail page shows a dropdown to rate difficulty and the current average + +## Consent Modal + +The first time a user clicks vote, predict, or rate difficulty, a modal explains: +- Why GitHub login is needed (identity verification, zero additional OAuth scopes) +- That all activity is public (stored as GitHub Discussions, visible to anyone) +- What specifically is stored (heart reactions, thumbs reactions, comments) + +The user acts with their own GitHub permissions. The acknowledgement is stored in `localStorage` so the modal only appears once per browser. ## OAuth Flow -1. User clicks "Sign in with GitHub" -2. Browser redirects to `https://github.com/login/oauth/authorize` with `client_id` and `redirect_uri` (no scopes — zero permissions) -3. GitHub redirects back to the page with `?code=...` -4. `voting.js` detects the code, POSTs `{ code }` to the worker's `/token` endpoint -5. The worker exchanges the code for an access token using the client secret -6. `voting.js` fetches `/user` from GitHub to get the username +1. User clicks a voting/prediction/rating action +2. Consent modal appears (first time only) +3. Browser redirects to GitHub OAuth; the callback URL points to the shared proxy +4. GitHub redirects to the proxy's `/oauth/callback` with `?code=...` +5. The proxy validates the return URL and bounces the code back to the user's site +6. `voting.js` exchanges the code for a token via the proxy's `/token` endpoint 7. Token and username are stored in `localStorage` ## Client Configuration -Two constants at the top of `src/js/voting.js`: +Constants at the top of `src/js/voting.js` (placeholders replaced by CI at build time): + +| Constant | Injected value | Description | +|---|---|---| +| `WORKER_URL` | `https://formal-conjectures-web-worker.uc.r.appspot.com` | Shared proxy URL | +| `GH_CLIENT_ID` | `Iv23lid2mjCGp7EIKrJn` | Shared GitHub App client ID | +| `REPO_OWNER` | From `github.repository_owner` | GitHub repo owner | +| `REPO_NAME` | From `github.event.repository.name` | GitHub repo name | +| `REPO_ID` | Fetched via GitHub API | GitHub repo GraphQL node ID | -| Constant | Description | -|---|---| -| `WORKER_URL` | URL of the Cloudflare Worker | -| `GH_CLIENT_ID` | GitHub OAuth App client ID | +For local development, edit these directly in `voting.js`. -## Setting Up for Development +## Local Development -1. **Register a GitHub OAuth App** at https://github.com/settings/developers - - Set the callback URL to your local dev URL (e.g., `http://localhost:8000`) - - Do NOT request any scopes (zero permissions) -2. **Set up the worker** — follow [`worker/README.md`](../worker/README.md) for KV namespace creation, secrets, and local dev -3. **Update `voting.js` constants** — point `WORKER_URL` to `http://localhost:8787`, set `GH_CLIENT_ID` -4. **Start the worker**: `cd worker && npm run dev` -5. **Build and serve the site**: `npm run build && cd site && python3 -m http.server 8000` +1. **Start the proxy** (see [`appengine/README.md`](../appengine/README.md) for details): + +```bash +cd site/appengine && npm install +export GH_APP_ID=3005907 +export GH_CLIENT_ID=your_client_id +export GH_CLIENT_SECRET=your_client_secret +export GH_APP_PRIVATE_KEY="$(cat path/to/private-key.pem)" +export ALLOWED_ORIGIN=http://localhost:8000 +node server.js +``` + +2. **Edit `voting.js`** constants to point at localhost and your repo. + +3. **Build and serve**: + +```bash +cd site && node build.js && cd site && python3 -m http.server 8000 +``` -## Deploying to Production +## Running Your Own Proxy -1. **Create a GitHub OAuth App** (or reuse the dev one) — set the **Authorization callback URL** to your production site URL -2. **Deploy the worker** — follow [`worker/README.md`](../worker/README.md) for KV namespace, secrets, CORS origin, and deployment -3. **Update `voting.js` constants** — set `WORKER_URL` to your deployed worker URL and `GH_CLIENT_ID` to your production client ID -4. **Build and deploy the site**: `npm run build` and deploy the `site/` directory (e.g., via GitHub Pages) +See [`appengine/README.md`](../appengine/README.md) for full instructions on deploying your own App Engine proxy. ## Limitations -- Cloudflare KV is eventually consistent — there may be brief delays before vote counts update across regions -- The `GET /votes` endpoint lists all KV keys, which may be slow with very large numbers of voted theorems -- Vote counts are cached in memory after the first fetch within a page session +- GitHub GraphQL API rate limits (5000 points/hour for authenticated users) +- The `/discussions` endpoint paginates all discussions, which may be slow with very large numbers +- Vote counts are cached in memory per page session +- The proxy caches discussion data for 60 seconds +- The GitHub App must be installed on any repo that uses the shared proxy +- Webtest builds depend on the live site being available for the conjectures JSON diff --git a/site/src/css/style.css b/site/src/css/style.css index 6e223b33b..bacbdc9e7 100644 --- a/site/src/css/style.css +++ b/site/src/css/style.css @@ -856,13 +856,12 @@ a.cat-stat:hover { text-decoration: none; opacity: 0.8; } font-variant-numeric: tabular-nums; } +/* To enable interactive voting for logged-out users, remove display: none */ .auth-prompt { + display: none; font-size: 0.8rem; color: var(--color-text-muted); } -.auth-prompt:hover { - color: var(--color-link); -} /* Difficulty rating */ .difficulty-widget { @@ -912,6 +911,145 @@ a.cat-stat:hover { text-decoration: none; opacity: 0.8; } font-variant-numeric: tabular-nums; } +/* Discussion link */ +.discussion-link { + display: inline-block; + font-size: 0.85rem; + color: var(--color-link); + margin-bottom: 1rem; +} +.discussion-link--loading { + color: var(--color-text-muted); +} + +/* Truth prediction */ +.truth-widget { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 2rem; +} + +.truth-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.45rem 0.85rem; + border: 1px solid var(--color-border); + border-radius: 999px; + background: white; + color: var(--color-text-muted); + font-size: 0.875rem; + font-family: inherit; + cursor: pointer; + transition: all 0.15s; +} +.truth-btn:hover { + border-color: var(--color-text-muted); + color: var(--color-text); +} +.truth-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.truth-btn__label { font-weight: 600; } +.truth-btn__count { font-weight: 600; font-variant-numeric: tabular-nums; } + +.truth-btn--active-up { + background: #ecfdf5; + border-color: #10b981; + color: #065f46; +} +.truth-btn--active-up:hover { + border-color: #059669; + color: #064e3b; +} + +.truth-btn--active-down { + background: #fef2f2; + border-color: #ef4444; + color: #991b1b; +} +.truth-btn--active-down:hover { + border-color: #dc2626; + color: #7f1d1d; +} + +.theorem-card__truth { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.78rem; + color: var(--color-text-muted); + margin-right: 0.5rem; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +/* ============================================================ + Consent modal + ============================================================ */ +.consent-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.consent-dialog { + background: white; + border-radius: var(--radius); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 520px; + width: 100%; + padding: 2rem; + max-height: 90vh; + overflow-y: auto; +} + +.consent-dialog__title { + font-family: var(--font-display); + font-size: 1.3rem; + font-weight: normal; + margin-bottom: 1rem; +} + +.consent-dialog__body { + font-size: 0.9rem; + line-height: 1.7; + color: var(--color-text); + margin-bottom: 1.5rem; +} + +.consent-dialog__body p { + margin-bottom: 0.75rem; +} + +.consent-dialog__body ul { + list-style: disc; + padding-left: 1.5rem; + margin-bottom: 0.75rem; +} + +.consent-dialog__body li { + margin-bottom: 0.25rem; +} + +.consent-dialog__body a { + color: var(--color-link); +} + +.consent-dialog__actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + /* ============================================================ Hamburger menu button (hidden on desktop) ============================================================ */ diff --git a/site/src/js/browse.js b/site/src/js/browse.js index eb0768997..e460ad2d6 100644 --- a/site/src/js/browse.js +++ b/site/src/js/browse.js @@ -16,7 +16,7 @@ let allConjectures = []; let filtered = []; let currentPage = 1; -// Active filter state (driven by URL ↔ UI) +// Active filter state (driven by URL <-> UI) const state = { query: '', categories: new Set(), @@ -85,19 +85,29 @@ function applyFilters() { // Sort filtered.sort((a, b) => { - if (state.sort === 'votes' && FC.voting) { - const aVotes = FC.voting.getVote(a.theorem).count; - const bVotes = FC.voting.getVote(b.theorem).count; - return bVotes - aVotes || a.theorem.localeCompare(b.theorem); + if (state.sort === 'most-liked') { + const aLikes = FC.voting.getVote(a.theorem).count; + const bLikes = FC.voting.getVote(b.theorem).count; + return bLikes - aLikes || a.theorem.localeCompare(b.theorem); } - if (state.sort === 'difficulty' && FC.voting) { + if (state.sort === 'difficulty-desc' || state.sort === 'difficulty-asc') { const aDiff = FC.voting.getVote(a.theorem).avgDifficulty; const bDiff = FC.voting.getVote(b.theorem).avgDifficulty; - // nulls sort last if (aDiff === null && bDiff === null) return a.theorem.localeCompare(b.theorem); if (aDiff === null) return 1; if (bDiff === null) return -1; - return bDiff - aDiff || a.theorem.localeCompare(b.theorem); + const dir = state.sort === 'difficulty-desc' ? -1 : 1; + return dir * (aDiff - bDiff) || a.theorem.localeCompare(b.theorem); + } + if (state.sort === 'prediction-true' || state.sort === 'prediction-false') { + const aVote = FC.voting.getVote(a.theorem); + const bVote = FC.voting.getVote(b.theorem); + const aScore = aVote.thumbsUp - aVote.thumbsDown; + const bScore = bVote.thumbsUp - bVote.thumbsDown; + const aTotal = aVote.thumbsUp + aVote.thumbsDown; + const bTotal = bVote.thumbsUp + bVote.thumbsDown; + const dir = state.sort === 'prediction-true' ? -1 : 1; + return dir * (aScore - bScore) || bTotal - aTotal || a.theorem.localeCompare(b.theorem); } if (state.sort === 'category') return a.category.localeCompare(b.category) || a.theorem.localeCompare(b.theorem); if (state.sort === 'collection') return a.collection.localeCompare(b.collection) || a.theorem.localeCompare(b.theorem); @@ -135,8 +145,9 @@ function renderCard(c) {
- ${FC.voting ? FC.voting.renderCardVoteCount(c.theorem) : ''} - ${FC.voting ? FC.voting.renderCardDifficulty(c.theorem) : ''} + ${FC.voting.renderCardVoteCount(c.theorem)} + ${FC.voting.renderCardTruth(c.theorem)} + ${FC.voting.renderCardDifficulty(c.theorem)} ${FC.escapeHTML(catMeta.label)}
`; @@ -187,7 +198,7 @@ function renderPagination() { paginationEl.appendChild(btn); }; - addBtn('‹', currentPage - 1, currentPage === 1); + addBtn('\u2039', currentPage - 1, currentPage === 1); // Show pages around the current page const WINDOW = 2; @@ -196,13 +207,13 @@ function renderPagination() { addBtn(p, p); } else if (Math.abs(p - currentPage) === WINDOW + 1) { const ellipsis = document.createElement('span'); - ellipsis.textContent = '…'; + ellipsis.textContent = '\u2026'; ellipsis.style.padding = '0 0.25rem'; paginationEl.appendChild(ellipsis); } } - addBtn('›', currentPage + 1, currentPage === totalPages); + addBtn('\u203A', currentPage + 1, currentPage === totalPages); } // --------------------------------------------------------------------------- @@ -258,9 +269,9 @@ async function init() { allConjectures = data.conjectures; - // Handle OAuth callback and prefetch votes (disabled) - // await FC.voting.handleOAuthCallback(); - // await FC.voting.fetchAllVotes(); + // Handle OAuth callback and prefetch votes + await FC.voting.handleOAuthCallback(); + await FC.voting.fetchAllVotes(); // Collect unique values for filters const categories = new Set(allConjectures.map(c => c.category)); diff --git a/site/src/js/theorem.js b/site/src/js/theorem.js index 2df894436..3bb4d4761 100644 --- a/site/src/js/theorem.js +++ b/site/src/js/theorem.js @@ -42,6 +42,22 @@ async function init() { const verso = data.versoFragments || { moduleDocs: {}, constLinks: {} }; renderDetail(theorem, siblings, verso); + + // Voting integration (sign-in temporarily hidden) + await FC.voting.handleOAuthCallback(); + const widget = document.getElementById('vote-widget'); + const discLink = document.getElementById('discussion-link'); + const truthWidget = document.getElementById('truth-widget'); + const diffWidget = document.getElementById('difficulty-widget'); + if (widget) FC.voting.renderVoteButton(theorem.theorem, widget); + if (truthWidget) FC.voting.renderTruthWidget(theorem.theorem, truthWidget); + if (diffWidget) FC.voting.renderDifficultyWidget(theorem.theorem, diffWidget); + FC.voting.fetchAllVotes().then(() => { + if (widget) FC.voting.renderVoteButton(theorem.theorem, widget); + if (discLink) FC.voting.renderDiscussionLink(theorem.theorem, discLink); + if (truthWidget) FC.voting.renderTruthWidget(theorem.theorem, truthWidget); + if (diffWidget) FC.voting.renderDifficultyWidget(theorem.theorem, diffWidget); + }); } /** @@ -462,6 +478,11 @@ function renderDetail(theorem, siblings, verso) { ${FC.escapeHTML(catMeta.label)} +
+ +
+
+ ${moduleDocSection} ${docSection} diff --git a/site/src/js/voting.js b/site/src/js/voting.js index 7b1513155..886ee670c 100644 --- a/site/src/js/voting.js +++ b/site/src/js/voting.js @@ -1,9 +1,10 @@ /** - * voting.js — Cloudflare KV-backed voting and difficulty rating system. + * voting.js — GitHub Discussions-backed voting, truth prediction, and difficulty rating system. * - * Votes and difficulty ratings (0–10) are stored in Cloudflare KV via the - * worker. GitHub OAuth is used only for identity verification (zero - * permissions required). + * Likes = HEART reactions, truth predictions = THUMBS_UP/THUMBS_DOWN reactions, + * difficulty = any line matching /^difficulty [0-9]$/i in any comment (latest per user wins). + * Writes go directly to GitHub GraphQL using the user's token. + * Anonymous reads go through the App Engine proxy. * * Extends window.FC with a `voting` namespace. */ @@ -12,19 +13,30 @@ (function () { // --------------------------------------------------------------------------- - // Configuration — change these for your deployment / testing + // Configuration // --------------------------------------------------------------------------- - const WORKER_URL = 'http://localhost:8787'; - const GH_CLIENT_ID = 'Iv23lid2mjCGp7EIKrJn'; + const WORKER_URL = 'REPLACE_WITH_PROXY_URL'; // e.g. 'http://localhost:8080' or 'https://PROJECT.REGION.r.appspot.com' + const GH_CLIENT_ID = 'REPLACE_WITH_CLIENT_ID'; // GitHub App client ID + const REPO_OWNER = 'REPLACE_WITH_REPO_OWNER'; // e.g. 'google-deepmind' + const REPO_NAME = 'REPLACE_WITH_REPO_NAME'; // e.g. 'formal-conjectures' + const REPO_ID = 'REPLACE_WITH_REPO_ID'; // GraphQL node ID (gh api repos/OWNER/NAME --jq .node_id) + + const GH_GRAPHQL = 'https://api.github.com/graphql'; + const GH_API = 'https://api.github.com'; - const GH_API = 'https://api.github.com'; const LS_TOKEN_KEY = 'fc_gh_token'; const LS_USER_KEY = 'fc_gh_user'; + const LS_CONSENT_KEY = 'fc_gh_consent_acknowledged'; // --------------------------------------------------------------------------- - // In-memory vote cache: Map + // Caches // --------------------------------------------------------------------------- + // Map let voteCache = null; + // Map + const discussionIdCache = new Map(); + // Cached category ID for creating discussions + let categoryIdCache = null; // --------------------------------------------------------------------------- // Auth helpers @@ -40,13 +52,19 @@ }; } + function hasConsented() { + return localStorage.getItem(LS_CONSENT_KEY) === 'true'; + } + function login() { - const redirectUri = window.location.href.split('?')[0] + window.location.search; - const params = new URLSearchParams({ - client_id: GH_CLIENT_ID, - redirect_uri: redirectUri, - }); - window.location.href = `https://github.com/login/oauth/authorize?${params}`; + // Route OAuth callback through the shared proxy, which bounces back to + // the user's site with the code. This lets one registered callback URL + // work for all forks. + // Pass the return URL in the OAuth state parameter. GitHub passes it + // through to the callback unchanged. The redirect_uri must exactly match + // the registered callback URL (no extra query params). + const redirectUri = `${WORKER_URL}/oauth/callback`; + window.location.href = `https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(GH_CLIENT_ID)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(window.location.href)}`; } function logout() { @@ -56,12 +74,97 @@ window.location.reload(); } + // --------------------------------------------------------------------------- + // Consent modal + // --------------------------------------------------------------------------- + function showConsentModal() { + return new Promise(function (resolve) { + if (hasConsented()) { + resolve(true); + return; + } + + const overlay = document.createElement('div'); + overlay.className = 'consent-overlay'; + + const dialog = document.createElement('div'); + dialog.className = 'consent-dialog'; + dialog.setAttribute('role', 'dialog'); + dialog.setAttribute('aria-modal', 'true'); + dialog.setAttribute('aria-labelledby', 'consent-title'); + + dialog.innerHTML = ` + + + + `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + // Focus the accept button + const acceptBtn = dialog.querySelector('#consent-accept'); + const cancelBtn = dialog.querySelector('#consent-cancel'); + acceptBtn.focus(); + + function close(accepted) { + document.removeEventListener('keydown', onKey); + overlay.remove(); + if (accepted) { + localStorage.setItem(LS_CONSENT_KEY, 'true'); + } + resolve(accepted); + } + + acceptBtn.addEventListener('click', function () { close(true); }); + cancelBtn.addEventListener('click', function () { close(false); }); + overlay.addEventListener('click', function (e) { + if (e.target === overlay) close(false); + }); + + function onKey(e) { + if (e.key === 'Escape') close(false); + } + document.addEventListener('keydown', onKey); + }); + } + + /** + * Show consent modal (if needed), then redirect to GitHub OAuth. + * Returns false if the user cancelled the consent dialog. + */ + async function loginWithConsent() { + const accepted = await showConsentModal(); + if (!accepted) return false; + login(); + return true; // page will redirect + } + async function handleOAuthCallback() { const params = new URLSearchParams(window.location.search); const code = params.get('code'); if (!code) return; - // Strip the code from the URL immediately + // Clean the URL immediately params.delete('code'); params.delete('state'); const clean = params.toString() @@ -70,16 +173,20 @@ window.history.replaceState(null, '', clean); try { - // Exchange code for token via our worker const tokenResp = await fetch(`${WORKER_URL}/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }), }); if (!tokenResp.ok) throw new Error('Token exchange failed'); - const { access_token } = await tokenResp.json(); + const tokenData = await tokenResp.json(); + console.log('Token exchange response keys:', Object.keys(tokenData), 'token_type:', tokenData.token_type); + const { access_token } = tokenData; + if (!access_token) { + console.error('Token exchange error response:', tokenData); + throw new Error('No access token returned'); + } - // Fetch GitHub user info const userResp = await fetch(`${GH_API}/user`, { headers: { Authorization: `Bearer ${access_token}` }, }); @@ -91,127 +198,497 @@ } catch (e) { console.error('OAuth callback error:', e); } + } // --------------------------------------------------------------------------- - // Vote data fetching + // GraphQL helper // --------------------------------------------------------------------------- + async function graphql(query, variables, token) { + const resp = await fetch(GH_GRAPHQL, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'fc-voting-client', + }, + body: JSON.stringify({ query, variables }), + }); + if (!resp.ok) { + const text = await resp.text().catch(() => ''); + console.error(`GraphQL ${resp.status}: ${text}`); + throw new Error(`GraphQL request failed: ${resp.status}`); + } + const json = await resp.json(); + if (json.errors) { + console.error('GraphQL errors:', json.errors); + throw new Error(json.errors[0].message); + } + return json.data; + } + + // --------------------------------------------------------------------------- + // Discussion helpers + // --------------------------------------------------------------------------- + const DISCUSSION_CATEGORY_NAME = 'Problems'; + + async function fetchCategoryId(token) { + if (categoryIdCache) return categoryIdCache; + const data = await graphql(` + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + discussionCategories(first: 25) { + nodes { id name } + } + } + } + `, { owner: REPO_OWNER, name: REPO_NAME }, token); + const cats = data.repository.discussionCategories.nodes; + const match = cats.find(c => c.name === DISCUSSION_CATEGORY_NAME); + if (!match) throw new Error(`Discussion category "${DISCUSSION_CATEGORY_NAME}" not found. Create it in the repository settings under Discussions.`); + categoryIdCache = match.id; + return categoryIdCache; + } + + function updateCacheDiscussionNumber(theoremName, number) { + if (!voteCache) voteCache = new Map(); + if (voteCache.has(theoremName)) { + voteCache.get(theoremName).discussionNumber = number; + } else { + voteCache.set(theoremName, { ...getDefaults(), discussionNumber: number }); + } + } + + // In-flight discussion creation promises to prevent concurrent duplicates + const pendingDiscussions = new Map(); + + async function ensureDiscussion(theoremName) { + if (discussionIdCache.has(theoremName)) { + return discussionIdCache.get(theoremName); + } + + // If another call is already creating this discussion, wait for it + if (pendingDiscussions.has(theoremName)) { + return pendingDiscussions.get(theoremName); + } + + const promise = doEnsureDiscussion(theoremName); + pendingDiscussions.set(theoremName, promise); + try { + return await promise; + } finally { + pendingDiscussions.delete(theoremName); + } + } + + async function doEnsureDiscussion(theoremName) { + const { token } = getUser(); + if (!token) throw new Error('Not authenticated'); + + // List discussions directly (avoids search index lag that causes duplicates) + let hasNextPage = true; + let afterCursor = null; + while (hasNextPage) { + const data = await graphql(` + query($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + discussions(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { id number title } + } + } + } + `, { owner: REPO_OWNER, name: REPO_NAME, after: afterCursor }, token); + + const discussions = data.repository.discussions; + const match = discussions.nodes.find(n => n.title === theoremName); + if (match) { + discussionIdCache.set(theoremName, match.id); + updateCacheDiscussionNumber(theoremName, match.number); + return match.id; + } + hasNextPage = discussions.pageInfo.hasNextPage; + afterCursor = discussions.pageInfo.endCursor; + } + + // Create new discussion + const categoryId = await fetchCategoryId(token); + const shortName = theoremName.split('.').pop(); + const body = `Discussion for theorem **${shortName}**.\n\nFull Lean name: \`${theoremName}\``; + + const createData = await graphql(` + mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) { + createDiscussion(input: { repositoryId: $repoId, categoryId: $categoryId, title: $title, body: $body }) { + discussion { id number } + } + } + `, { repoId: REPO_ID, categoryId, title: theoremName, body }, token); + + const created = createData.createDiscussion.discussion; + discussionIdCache.set(theoremName, created.id); + updateCacheDiscussionNumber(theoremName, created.number); + return created.id; + } + + // --------------------------------------------------------------------------- + // Data fetching + // --------------------------------------------------------------------------- + function getDefaults() { + return { count: 0, userVoted: false, thumbsUp: 0, thumbsDown: 0, userTruth: null, avgDifficulty: null, numRatings: 0, userDifficulty: null, discussionId: null, discussionNumber: null }; + } + async function fetchAllVotes() { if (voteCache) return voteCache; const map = new Map(); - const { login } = getUser(); - const userParam = login ? `?user=${encodeURIComponent(login)}` : ''; + // Always fetch aggregates from the proxy (uses GitHub App installation token, cached) try { - const resp = await fetch(`${WORKER_URL}/votes${userParam}`); - if (!resp.ok) throw new Error('Failed to fetch votes'); + const resp = await fetch(`${WORKER_URL}/discussions?owner=${encodeURIComponent(REPO_OWNER)}&repo=${encodeURIComponent(REPO_NAME)}`); + if (!resp.ok) throw new Error('Failed to fetch discussions'); const data = await resp.json(); for (const [name, info] of Object.entries(data)) { + discussionIdCache.set(name, info.discussionId); map.set(name, { count: info.count, - userVoted: info.userVoted, + userVoted: false, + thumbsUp: info.thumbsUp, + thumbsDown: info.thumbsDown, + userTruth: null, avgDifficulty: info.avgDifficulty ?? null, numRatings: info.numRatings || 0, - userDifficulty: info.userDifficulty ?? null, + userDifficulty: null, + discussionId: info.discussionId, + discussionNumber: info.discussionNumber, }); } } catch (e) { console.error('Failed to fetch votes:', e); } + // If logged in, overlay user-specific state + const { token, login } = getUser(); + if (token && login) { + try { + let hasNextPage = true; + let afterCursor = null; + + while (hasNextPage) { + const data = await graphql(` + query($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + discussions(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + id + title + reactions(content: HEART) { viewerHasReacted } + thumbsUpReactions: reactions(content: THUMBS_UP) { viewerHasReacted } + thumbsDownReactions: reactions(content: THUMBS_DOWN) { viewerHasReacted } + comments(first: 100) { + pageInfo { hasNextPage endCursor } + nodes { + id + body + author { login } + } + } + } + } + } + } + `, { owner: REPO_OWNER, name: REPO_NAME, after: afterCursor }, token); + + const discussions = data.repository.discussions; + + for (const disc of discussions.nodes) { + const existing = map.get(disc.title); + if (!existing) continue; + + let allComments = [...disc.comments.nodes]; + let commentPage = disc.comments.pageInfo; + while (commentPage.hasNextPage) { + const cData = await graphql(` + query($discId: ID!, $after: String) { + node(id: $discId) { + ... on Discussion { + comments(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + id + body + author { login } + } + } + } + } + } + `, { discId: disc.id, after: commentPage.endCursor }, token); + const more = cData.node.comments; + allComments = allComments.concat(more.nodes); + commentPage = more.pageInfo; + } + + let userDifficulty = null; + for (const comment of allComments) { + if (!comment.author || comment.author.login !== login) continue; + const lines = comment.body.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (/^difficulty [0-9]$/i.test(trimmed)) { + userDifficulty = parseInt(trimmed.split(' ')[1], 10); + } + } + } + + let userTruth = null; + if (disc.thumbsUpReactions.viewerHasReacted) userTruth = 'up'; + else if (disc.thumbsDownReactions.viewerHasReacted) userTruth = 'down'; + + existing.userVoted = disc.reactions.viewerHasReacted; + existing.userTruth = userTruth; + existing.userDifficulty = userDifficulty; + } + + hasNextPage = discussions.pageInfo.hasNextPage; + afterCursor = discussions.pageInfo.endCursor; + } + } catch (e) { + if (e.message && e.message.includes('401')) { + localStorage.removeItem(LS_TOKEN_KEY); + localStorage.removeItem(LS_USER_KEY); + } else { + console.error('Failed to fetch user-specific vote state:', e); + } + } + } + voteCache = map; return map; } function getVote(theoremName) { - const defaults = { count: 0, userVoted: false, avgDifficulty: null, numRatings: 0, userDifficulty: null }; + const defaults = getDefaults(); if (!voteCache) return defaults; const data = voteCache.get(theoremName); return data ? { ...defaults, ...data } : defaults; } // --------------------------------------------------------------------------- - // Voting actions + // Voting (HEART reaction) // --------------------------------------------------------------------------- async function submitVote(theoremName) { const { token } = getUser(); if (!token) throw new Error('Not authenticated'); - const resp = await fetch(`${WORKER_URL}/vote/${encodeURIComponent(theoremName)}`, { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!resp.ok) throw new Error('Failed to submit vote'); - const result = await resp.json(); + const discussionId = await ensureDiscussion(theoremName); + await graphql(` + mutation($subjectId: ID!) { + addReaction(input: { subjectId: $subjectId, content: HEART }) { + reaction { content } + } + } + `, { subjectId: discussionId }, token); - // Update cache (preserve difficulty fields) if (!voteCache) voteCache = new Map(); - const prev = voteCache.get(theoremName) || {}; - voteCache.set(theoremName, { ...prev, count: result.count, userVoted: true }); + const prev = voteCache.get(theoremName) || getDefaults(); + voteCache.set(theoremName, { ...prev, count: prev.count + 1, userVoted: true, discussionId }); } async function removeVote(theoremName) { const { token } = getUser(); if (!token) throw new Error('Not authenticated'); - const resp = await fetch(`${WORKER_URL}/vote/${encodeURIComponent(theoremName)}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!resp.ok) throw new Error('Failed to remove vote'); - const result = await resp.json(); + const prev = voteCache ? voteCache.get(theoremName) : null; + const discussionId = prev?.discussionId || await ensureDiscussion(theoremName); - // Update cache (preserve difficulty fields) - if (voteCache) { - const prev = voteCache.get(theoremName) || {}; - if (result.count === 0 && !prev.numRatings) { - voteCache.delete(theoremName); - } else { - voteCache.set(theoremName, { ...prev, count: result.count, userVoted: false }); + await graphql(` + mutation($subjectId: ID!) { + removeReaction(input: { subjectId: $subjectId, content: HEART }) { + reaction { content } + } } + `, { subjectId: discussionId }, token); + + if (voteCache && prev) { + voteCache.set(theoremName, { ...prev, count: Math.max(0, prev.count - 1), userVoted: false }); } } // --------------------------------------------------------------------------- - // Difficulty actions + // Truth prediction (THUMBS_UP / THUMBS_DOWN reactions) // --------------------------------------------------------------------------- - async function submitDifficulty(theoremName, value) { + async function submitTruth(theoremName, value) { const { token } = getUser(); if (!token) throw new Error('Not authenticated'); - const resp = await fetch(`${WORKER_URL}/difficulty/${encodeURIComponent(theoremName)}`, { - method: 'POST', - headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ value }), + const discussionId = await ensureDiscussion(theoremName); + if (!voteCache) voteCache = new Map(); + const prev = voteCache.get(theoremName) || getDefaults(); + const content = value === 'up' ? 'THUMBS_UP' : 'THUMBS_DOWN'; + const opposite = value === 'up' ? 'THUMBS_DOWN' : 'THUMBS_UP'; + + const oppositeValue = value === 'up' ? 'down' : 'up'; + if (prev.userTruth === oppositeValue) { + try { + await graphql(` + mutation($subjectId: ID!, $content: ReactionContent!) { + removeReaction(input: { subjectId: $subjectId, content: $content }) { + reaction { content } + } + } + `, { subjectId: discussionId, content: opposite }, token); + } catch (e) { + // May fail if reaction doesn't exist; that's ok + } + } + + await graphql(` + mutation($subjectId: ID!, $content: ReactionContent!) { + addReaction(input: { subjectId: $subjectId, content: $content }) { + reaction { content } + } + } + `, { subjectId: discussionId, content }, token); + + const upDelta = value === 'up' ? 1 : (prev.userTruth === 'up' ? -1 : 0); + const downDelta = value === 'down' ? 1 : (prev.userTruth === 'down' ? -1 : 0); + voteCache.set(theoremName, { + ...prev, + thumbsUp: Math.max(0, prev.thumbsUp + upDelta), + thumbsDown: Math.max(0, prev.thumbsDown + downDelta), + userTruth: value, + discussionId, }); + } + + async function removeTruth(theoremName) { + const { token } = getUser(); + if (!token) throw new Error('Not authenticated'); + + const prev = voteCache ? voteCache.get(theoremName) : null; + const discussionId = prev?.discussionId || await ensureDiscussion(theoremName); + + if (prev && prev.userTruth) { + const content = prev.userTruth === 'up' ? 'THUMBS_UP' : 'THUMBS_DOWN'; + await graphql(` + mutation($subjectId: ID!, $content: ReactionContent!) { + removeReaction(input: { subjectId: $subjectId, content: $content }) { + reaction { content } + } + } + `, { subjectId: discussionId, content }, token); + } - if (!resp.ok) throw new Error('Failed to submit difficulty'); - const result = await resp.json(); + if (voteCache && prev) { + const upDelta = prev.userTruth === 'up' ? -1 : 0; + const downDelta = prev.userTruth === 'down' ? -1 : 0; + voteCache.set(theoremName, { + ...prev, + thumbsUp: Math.max(0, prev.thumbsUp + upDelta), + thumbsDown: Math.max(0, prev.thumbsDown + downDelta), + userTruth: null, + }); + } + } + + // --------------------------------------------------------------------------- + // Difficulty (comments) + // --------------------------------------------------------------------------- + async function submitDifficulty(theoremName, value) { + const { token, login } = getUser(); + if (!token) throw new Error('Not authenticated'); + if (!Number.isInteger(value) || value < 0 || value > 9) { + throw new Error('Difficulty must be an integer 0-9'); + } + + const discussionId = await ensureDiscussion(theoremName); + + await deleteUserDifficultyComments(discussionId, login, token); + + await graphql(` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { + comment { id } + } + } + `, { discussionId, body: `difficulty ${value}` }, token); if (!voteCache) voteCache = new Map(); - const prev = voteCache.get(theoremName) || {}; - voteCache.set(theoremName, { ...prev, avgDifficulty: result.avgDifficulty, numRatings: result.numRatings, userDifficulty: result.userDifficulty }); + const prev = voteCache.get(theoremName) || getDefaults(); + let totalRatings = prev.numRatings; + let sum = (prev.avgDifficulty !== null ? prev.avgDifficulty * prev.numRatings : 0); + if (prev.userDifficulty !== null) { + sum -= prev.userDifficulty; + totalRatings -= 1; + } + sum += value; + totalRatings += 1; + const avgDifficulty = Math.round((sum / totalRatings) * 10) / 10; + voteCache.set(theoremName, { ...prev, avgDifficulty, numRatings: totalRatings, userDifficulty: value, discussionId }); } async function removeDifficulty(theoremName) { - const { token } = getUser(); + const { token, login } = getUser(); if (!token) throw new Error('Not authenticated'); - const resp = await fetch(`${WORKER_URL}/difficulty/${encodeURIComponent(theoremName)}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }); + const prev = voteCache ? voteCache.get(theoremName) : null; + const discussionId = prev?.discussionId || await ensureDiscussion(theoremName); - if (!resp.ok) throw new Error('Failed to remove difficulty'); - const result = await resp.json(); + await deleteUserDifficultyComments(discussionId, login, token); + + if (voteCache && prev) { + let totalRatings = prev.numRatings; + let sum = (prev.avgDifficulty !== null ? prev.avgDifficulty * prev.numRatings : 0); + if (prev.userDifficulty !== null) { + sum -= prev.userDifficulty; + totalRatings -= 1; + } + const avgDifficulty = totalRatings > 0 ? Math.round((sum / totalRatings) * 10) / 10 : null; + voteCache.set(theoremName, { ...prev, avgDifficulty, numRatings: totalRatings, userDifficulty: null }); + } + } + + async function deleteUserDifficultyComments(discussionId, login, token) { + let hasNextPage = true; + let afterCursor = null; + + while (hasNextPage) { + const data = await graphql(` + query($discId: ID!, $after: String) { + node(id: $discId) { + ... on Discussion { + comments(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + id + body + author { login } + } + } + } + } + } + `, { discId: discussionId, after: afterCursor }, token); + + const comments = data.node.comments; + for (const comment of comments.nodes) { + if (!comment.author || comment.author.login !== login) continue; + const body = comment.body.trim(); + if (/^difficulty [0-9]$/i.test(body)) { + await graphql(` + mutation($id: ID!) { + deleteDiscussionComment(input: { id: $id }) { + comment { id } + } + } + `, { id: comment.id }, token); + } + } - if (voteCache) { - const prev = voteCache.get(theoremName) || {}; - voteCache.set(theoremName, { ...prev, avgDifficulty: result.avgDifficulty, numRatings: result.numRatings, userDifficulty: null }); + hasNextPage = comments.pageInfo.hasNextPage; + afterCursor = comments.pageInfo.endCursor; } } @@ -238,10 +715,10 @@ `; container.querySelector('.auth-prompt').addEventListener('click', function (e) { e.preventDefault(); - login(); + loginWithConsent(); }); container.querySelector('.vote-btn').addEventListener('click', function () { - login(); + loginWithConsent(); }); return; } @@ -292,6 +769,135 @@ ${count}`; } + // --------------------------------------------------------------------------- + // Discussion link — lazily creates the discussion if logged in + // --------------------------------------------------------------------------- + function renderDiscussionLink(theoremName, container) { + if (!container) return; + + const { discussionNumber } = getVote(theoremName); + container.innerHTML = ''; + + if (discussionNumber) { + const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/discussions/${discussionNumber}`; + container.innerHTML = `View discussion on GitHub ↗`; + return; + } + + // If logged in but no discussion exists, lazily create one + if (isLoggedIn()) { + container.innerHTML = 'Loading discussion...'; + ensureDiscussion(theoremName).then(function () { + const vote = getVote(theoremName); + if (vote.discussionNumber) { + const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/discussions/${vote.discussionNumber}`; + container.innerHTML = `View discussion on GitHub ↗`; + } + }).catch(function (e) { + console.error('Failed to create discussion:', e); + container.innerHTML = ''; + }); + } + } + + // --------------------------------------------------------------------------- + // Truth prediction UI + // --------------------------------------------------------------------------- + function renderTruthWidget(theoremName, container) { + if (!container) return; + + const { thumbsUp, thumbsDown, userTruth } = getVote(theoremName); + + container.innerHTML = ''; + container.className = 'truth-widget'; + + if (!isLoggedIn()) { + container.innerHTML = ` + + + Sign in to predict + `; + container.querySelector('.auth-prompt').addEventListener('click', function (e) { + e.preventDefault(); + loginWithConsent(); + }); + for (const btn of container.querySelectorAll('.truth-btn')) { + btn.addEventListener('click', function () { loginWithConsent(); }); + } + return; + } + + let busy = false; + + const upBtn = document.createElement('button'); + upBtn.className = 'truth-btn' + (userTruth === 'up' ? ' truth-btn--active-up' : ''); + upBtn.title = userTruth === 'up' ? 'Remove prediction' : 'Predict: conjecture is true'; + upBtn.setAttribute('aria-label', upBtn.title); + upBtn.innerHTML = `True${thumbsUp}`; + upBtn.addEventListener('click', async function () { + if (busy) return; + busy = true; + upBtn.disabled = true; + try { + if (userTruth === 'up') { + await removeTruth(theoremName); + } else { + await submitTruth(theoremName, 'up'); + } + renderTruthWidget(theoremName, container); + } catch (e) { + console.error('Truth prediction failed:', e); + showToast('Prediction failed. Please try again.'); + } finally { + busy = false; + upBtn.disabled = false; + } + }); + + const downBtn = document.createElement('button'); + downBtn.className = 'truth-btn' + (userTruth === 'down' ? ' truth-btn--active-down' : ''); + downBtn.title = userTruth === 'down' ? 'Remove prediction' : 'Predict: conjecture is false'; + downBtn.setAttribute('aria-label', downBtn.title); + downBtn.innerHTML = `False${thumbsDown}`; + downBtn.addEventListener('click', async function () { + if (busy) return; + busy = true; + downBtn.disabled = true; + try { + if (userTruth === 'down') { + await removeTruth(theoremName); + } else { + await submitTruth(theoremName, 'down'); + } + renderTruthWidget(theoremName, container); + } catch (e) { + console.error('Truth prediction failed:', e); + showToast('Prediction failed. Please try again.'); + } finally { + busy = false; + downBtn.disabled = false; + } + }); + + container.appendChild(upBtn); + container.appendChild(downBtn); + } + + function renderCardTruth(theoremName) { + const { thumbsUp, thumbsDown } = getVote(theoremName); + if (thumbsUp === 0 && thumbsDown === 0) return ''; + return `T ${thumbsUp} F ${thumbsDown}`; + } + + // --------------------------------------------------------------------------- + // Difficulty UI + // --------------------------------------------------------------------------- function renderDifficultyWidget(theoremName, container) { if (!container) return; @@ -301,14 +907,14 @@ container.className = 'difficulty-widget'; const avgText = avgDifficulty !== null - ? `Avg difficulty: ${avgDifficulty}/10 (${numRatings} rating${numRatings !== 1 ? 's' : ''})` + ? `Avg difficulty: ${avgDifficulty}/9 (${numRatings} rating${numRatings !== 1 ? 's' : ''})` : 'No ratings yet'; if (!isLoggedIn()) { container.innerHTML = `${avgText}Sign in to rate`; container.querySelector('.auth-prompt').addEventListener('click', function (e) { e.preventDefault(); - login(); + loginWithConsent(); }); return; } @@ -317,15 +923,15 @@ const select = document.createElement('select'); select.className = 'difficulty-select'; - select.setAttribute('aria-label', 'Rate difficulty 0–10'); + select.setAttribute('aria-label', 'Rate difficulty 0\u20139'); const placeholder = document.createElement('option'); placeholder.value = ''; - placeholder.textContent = 'Rate difficulty…'; + placeholder.textContent = 'Rate difficulty\u2026'; placeholder.disabled = true; placeholder.selected = userDifficulty === null; select.appendChild(placeholder); - for (let i = 0; i <= 10; i++) { + for (let i = 0; i <= 9; i++) { const opt = document.createElement('option'); opt.value = i; opt.textContent = i; @@ -381,7 +987,7 @@ function renderCardDifficulty(theoremName) { const { avgDifficulty } = getVote(theoremName); if (avgDifficulty === null) return ''; - return ` + return ` @@ -408,16 +1014,23 @@ isLoggedIn, getUser, login, + loginWithConsent, logout, handleOAuthCallback, fetchAllVotes, getVote, submitVote, removeVote, + submitTruth, + removeTruth, submitDifficulty, removeDifficulty, + ensureDiscussion, renderVoteButton, renderCardVoteCount, + renderDiscussionLink, + renderTruthWidget, + renderCardTruth, renderDifficultyWidget, renderCardDifficulty, }; diff --git a/site/src/templates/browse.html b/site/src/templates/browse.html index 9274b2ac7..843928834 100644 --- a/site/src/templates/browse.html +++ b/site/src/templates/browse.html @@ -79,8 +79,11 @@ - - + + + + + @@ -108,7 +111,7 @@ - + diff --git a/site/src/templates/theorem.html b/site/src/templates/theorem.html index 83b2602a9..013b55cb8 100644 --- a/site/src/templates/theorem.html +++ b/site/src/templates/theorem.html @@ -50,7 +50,7 @@ - + diff --git a/site/worker/README.md b/site/worker/README.md deleted file mode 100644 index 7a1bc8825..000000000 --- a/site/worker/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# OAuth Proxy & Voting Worker - -A Cloudflare Worker that handles GitHub OAuth token exchange and vote storage via Cloudflare KV. - -## Prerequisites - -- [Node.js](https://nodejs.org/) >= 18 -- A [Cloudflare account](https://dash.cloudflare.com/sign-up) (free tier works) -- A GitHub OAuth App - -## Register a GitHub OAuth App - -1. Go to GitHub → Settings → Developer Settings → [OAuth Apps](https://github.com/settings/developers) -2. Click **New OAuth App** -3. Fill in: - - **Application name:** Formal Conjectures Voting - - **Homepage URL:** Your site URL (e.g., `https://formal-conjectures.github.io`) - - **Authorization callback URL:** Your site URL (the client handles the redirect) -4. After creating, note the **Client ID** and generate a **Client Secret** - -## Setup - -```bash -cd worker -npm install -``` - -## Create KV Namespace - -```bash -npx wrangler kv namespace create VOTES -npx wrangler kv namespace create VOTES --preview -``` - -Copy the output IDs into `wrangler.toml`: - -```toml -[[kv_namespaces]] -binding = "VOTES" -id = "" -preview_id = "" -``` - -## Configure Secrets - -Store your GitHub OAuth App credentials as Cloudflare Worker secrets: - -```bash -npx wrangler secret put GH_CLIENT_ID -npx wrangler secret put GH_CLIENT_SECRET -``` - -You'll be prompted to enter each value. - -## API Endpoints - -### `POST /token` -Exchange a GitHub OAuth authorization code for an access token. - -**Request:** `{ "code": "..." }` -**Response:** `{ "access_token": "...", "token_type": "bearer", "scope": "" }` - -### `GET /votes?user=` -Get all vote counts and difficulty data. The `user` parameter is optional — when provided, the response includes whether that user has voted and their difficulty rating for each theorem. - -**Response:** `{ "TheoremName": { "count": 5, "userVoted": true, "avgDifficulty": 6.2, "numRatings": 3, "userDifficulty": 7 }, ... }` - -### `POST /vote/:name` -Cast a vote for a theorem. Requires `Authorization: Bearer ` header. - -**Response:** `{ "count": 6, "userVoted": true }` - -### `DELETE /vote/:name` -Remove a vote from a theorem. Requires `Authorization: Bearer ` header. - -**Response:** `{ "count": 5, "userVoted": false }` - -### `POST /difficulty/:name` -Rate the difficulty of a theorem (0–10 integer). Requires `Authorization: Bearer ` header. - -**Request:** `{ "value": 7 }` -**Response:** `{ "avgDifficulty": 6.2, "numRatings": 3, "userDifficulty": 7 }` - -### `DELETE /difficulty/:name` -Remove your difficulty rating from a theorem. Requires `Authorization: Bearer ` header. - -**Response:** `{ "avgDifficulty": 5.5, "numRatings": 2, "userDifficulty": null }` - -## Local Development - -```bash -npm run dev -``` - -This starts a local dev server (default `http://localhost:8787`). For local dev, create a `.dev.vars` file: - -``` -GH_CLIENT_ID=your_client_id -GH_CLIENT_SECRET=your_client_secret -``` - -## Deploy - -```bash -npm run deploy -``` - -## Configuration - -The `ALLOWED_ORIGIN` variable in `wrangler.toml` controls CORS. Update it to match your deployed site URL. - -## KV Data Model - -- **Namespace:** `VOTES` -- **Key format:** `votes:{theoremName}` -- **Value format:** `{ "count": N, "voters": ["user1", "user2"], "ratings": { "user1": 7, "user3": 4 } }` - -The `voters` array handles deduplication — each GitHub user can only vote once per theorem. The `ratings` object maps GitHub logins to difficulty values (0–10 integers). When the last voter removes their vote and there are no ratings, the KV key is deleted. diff --git a/site/worker/package-lock.json b/site/worker/package-lock.json deleted file mode 100644 index d0ec6889e..000000000 --- a/site/worker/package-lock.json +++ /dev/null @@ -1,1501 +0,0 @@ -{ - "name": "fc-oauth-proxy", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fc-oauth-proxy", - "version": "1.0.0", - "devDependencies": { - "wrangler": "^4" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", - "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz", - "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz", - "integrity": "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz", - "integrity": "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz", - "integrity": "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz", - "integrity": "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz", - "integrity": "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", - "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "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, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/miniflare": { - "version": "4.20260317.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.0.tgz", - "integrity": "sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.24.4", - "workerd": "1.20260317.1", - "ws": "8.18.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?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==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/workerd": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz", - "integrity": "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260317.1", - "@cloudflare/workerd-darwin-arm64": "1.20260317.1", - "@cloudflare/workerd-linux-64": "1.20260317.1", - "@cloudflare/workerd-linux-arm64": "1.20260317.1", - "@cloudflare/workerd-windows-64": "1.20260317.1" - } - }, - "node_modules/wrangler": { - "version": "4.75.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.75.0.tgz", - "integrity": "sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.15.0", - "blake3-wasm": "2.1.5", - "esbuild": "0.27.3", - "miniflare": "4.20260317.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260317.1" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=20.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20260317.1" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } - } - } -} diff --git a/site/worker/package.json b/site/worker/package.json deleted file mode 100644 index 0eb1d14df..000000000 --- a/site/worker/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "fc-oauth-proxy", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy" - }, - "devDependencies": { - "wrangler": "^4" - } -} diff --git a/site/worker/src/index.js b/site/worker/src/index.js deleted file mode 100644 index f7a9b22aa..000000000 --- a/site/worker/src/index.js +++ /dev/null @@ -1,211 +0,0 @@ -'use strict'; - -async function verifyUser(token) { - if (!token) return null; - try { - const resp = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/json', - 'User-Agent': 'fc-oauth-proxy', - }, - }); - if (!resp.ok) return null; - const user = await resp.json(); - return user.login ? { login: user.login } : null; - } catch { - return null; - } -} - -function computeAvgDifficulty(ratings) { - const values = Object.values(ratings || {}); - if (values.length === 0) return { avgDifficulty: null, numRatings: 0 }; - return { - avgDifficulty: Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10, - numRatings: values.length, - }; -} - -function jsonResponse(data, status, corsHeaders) { - return new Response(JSON.stringify(data), { - status, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); -} - -export default { - async fetch(request, env) { - const corsHeaders = { - 'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN || '*', - 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }; - - if (request.method === 'OPTIONS') { - return new Response(null, { status: 204, headers: corsHeaders }); - } - - const url = new URL(request.url); - const path = url.pathname; - - // POST /token — OAuth code exchange - if (path === '/token' && request.method === 'POST') { - const { code } = await request.json().catch(() => ({})); - if (!code) { - return jsonResponse({ error: 'Missing code parameter' }, 400, corsHeaders); - } - - const ghResponse = await fetch('https://github.com/login/oauth/access_token', { - method: 'POST', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: JSON.stringify({ - client_id: env.GH_CLIENT_ID, - client_secret: env.GH_CLIENT_SECRET, - code, - }), - }); - - if (!ghResponse.ok) { - return jsonResponse({ error: 'GitHub token exchange failed' }, 502, corsHeaders); - } - - const data = await ghResponse.json(); - return jsonResponse(data, 200, corsHeaders); - } - - // GET /votes?user= — all vote counts + optional user vote status - if (path === '/votes' && request.method === 'GET') { - const userParam = url.searchParams.get('user') || ''; - const result = {}; - - try { - const list = await env.VOTES.list({ prefix: 'votes:' }); - const fetches = list.keys.map(async (key) => { - const val = await env.VOTES.get(key.name, { type: 'json' }); - if (!val) return; - const theoremName = key.name.slice('votes:'.length); - const ratings = val.ratings || {}; - const { avgDifficulty, numRatings } = computeAvgDifficulty(ratings); - result[theoremName] = { - count: val.count || 0, - userVoted: userParam ? (val.voters || []).includes(userParam) : false, - avgDifficulty, - numRatings, - userDifficulty: userParam ? (ratings[userParam] ?? null) : null, - }; - }); - await Promise.all(fetches); - } catch (e) { - return jsonResponse({ error: 'Failed to read votes' }, 500, corsHeaders); - } - - return jsonResponse(result, 200, corsHeaders); - } - - // POST /vote/:name — cast a vote (auth required) - const voteMatch = path.match(/^\/vote\/(.+)$/); - if (voteMatch && request.method === 'POST') { - const token = (request.headers.get('Authorization') || '').replace('Bearer ', ''); - const user = await verifyUser(token); - if (!user) { - return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders); - } - - const theoremName = decodeURIComponent(voteMatch[1]); - const key = `votes:${theoremName}`; - const val = await env.VOTES.get(key, { type: 'json' }) || { count: 0, voters: [] }; - - if (!val.voters.includes(user.login)) { - val.voters.push(user.login); - val.count = val.voters.length; - await env.VOTES.put(key, JSON.stringify(val)); - } - - return jsonResponse({ count: val.count, userVoted: true }, 200, corsHeaders); - } - - // DELETE /vote/:name — remove a vote (auth required) - if (voteMatch && request.method === 'DELETE') { - const token = (request.headers.get('Authorization') || '').replace('Bearer ', ''); - const user = await verifyUser(token); - if (!user) { - return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders); - } - - const theoremName = decodeURIComponent(voteMatch[1]); - const key = `votes:${theoremName}`; - const val = await env.VOTES.get(key, { type: 'json' }) || { count: 0, voters: [], ratings: {} }; - - const idx = val.voters.indexOf(user.login); - if (idx !== -1) { - val.voters.splice(idx, 1); - val.count = val.voters.length; - const hasRatings = Object.keys(val.ratings || {}).length > 0; - if (val.count === 0 && !hasRatings) { - await env.VOTES.delete(key); - } else { - await env.VOTES.put(key, JSON.stringify(val)); - } - } - - return jsonResponse({ count: val.count, userVoted: false }, 200, corsHeaders); - } - - // POST /difficulty/:name — rate difficulty (auth required) - const diffMatch = path.match(/^\/difficulty\/(.+)$/); - if (diffMatch && request.method === 'POST') { - const token = (request.headers.get('Authorization') || '').replace('Bearer ', ''); - const user = await verifyUser(token); - if (!user) { - return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders); - } - - const body = await request.json().catch(() => ({})); - const value = body.value; - if (!Number.isInteger(value) || value < 0 || value > 10) { - return jsonResponse({ error: 'Value must be an integer 0–10' }, 400, corsHeaders); - } - - const theoremName = decodeURIComponent(diffMatch[1]); - const key = `votes:${theoremName}`; - const val = await env.VOTES.get(key, { type: 'json' }) || { count: 0, voters: [], ratings: {} }; - if (!val.ratings) val.ratings = {}; - - val.ratings[user.login] = value; - await env.VOTES.put(key, JSON.stringify(val)); - - const { avgDifficulty, numRatings } = computeAvgDifficulty(val.ratings); - return jsonResponse({ avgDifficulty, numRatings, userDifficulty: value }, 200, corsHeaders); - } - - // DELETE /difficulty/:name — remove difficulty rating (auth required) - if (diffMatch && request.method === 'DELETE') { - const token = (request.headers.get('Authorization') || '').replace('Bearer ', ''); - const user = await verifyUser(token); - if (!user) { - return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders); - } - - const theoremName = decodeURIComponent(diffMatch[1]); - const key = `votes:${theoremName}`; - const val = await env.VOTES.get(key, { type: 'json' }) || { count: 0, voters: [], ratings: {} }; - if (!val.ratings) val.ratings = {}; - - delete val.ratings[user.login]; - - const hasVoters = (val.voters || []).length > 0; - const hasRatings = Object.keys(val.ratings).length > 0; - if (!hasVoters && !hasRatings) { - await env.VOTES.delete(key); - } else { - await env.VOTES.put(key, JSON.stringify(val)); - } - - const { avgDifficulty, numRatings } = computeAvgDifficulty(val.ratings); - return jsonResponse({ avgDifficulty, numRatings, userDifficulty: null }, 200, corsHeaders); - } - - return new Response('Not found', { status: 404, headers: corsHeaders }); - }, -}; diff --git a/site/worker/wrangler.toml b/site/worker/wrangler.toml deleted file mode 100644 index 4620afec7..000000000 --- a/site/worker/wrangler.toml +++ /dev/null @@ -1,11 +0,0 @@ -name = "fc-oauth-proxy" -main = "src/index.js" -compatibility_date = "2024-01-01" - -[vars] -ALLOWED_ORIGIN = "https://formal-conjectures.github.io" - -[[kv_namespaces]] -binding = "VOTES" -id = "PLACEHOLDER_KV_ID" -preview_id = "PLACEHOLDER_PREVIEW_KV_ID"