diff --git a/.changeset/config.json b/.changeset/config.json index 774abfcc5b..669ea163f1 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -35,4 +35,4 @@ "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } -} \ No newline at end of file +} diff --git a/.changeset/secure-widget-cdn-release.md b/.changeset/secure-widget-cdn-release.md new file mode 100644 index 0000000000..3c8030dfe1 --- /dev/null +++ b/.changeset/secure-widget-cdn-release.md @@ -0,0 +1,6 @@ +--- +'@layerswap/widget-js': patch +'@layerswap/widget-react': patch +--- + +Rotate the widget manifest trust anchor and prepare the public loaders for the protected CDN release workflow. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..0b4996b74f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Frontend release security boundary. Changes to these files require review +# from a maintainer who can approve widget publishing and deployment changes. +/.github/CODEOWNERS @layerswap/frontend-release-maintainers +/.github/workflows/ @layerswap/frontend-release-maintainers +/.github/scripts/ @layerswap/frontend-release-maintainers +/apps/widget-cdn/scripts/ @layerswap/frontend-release-maintainers +/packages/widget/js/src/manifest.ts @layerswap/frontend-release-maintainers +/pnpm-lock.yaml @layerswap/frontend-release-maintainers diff --git a/.github/scripts/resolve-widget-cdn-env.sh b/.github/scripts/resolve-widget-cdn-env.sh new file mode 100755 index 0000000000..b427df6992 --- /dev/null +++ b/.github/scripts/resolve-widget-cdn-env.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Resolve the widget-cdn Azure target for a GitHub environment. +# +# Inputs (env): +# DEPLOY_TARGET GitHub environment name (required) +# RAW_AZURE_CLIENT_ID \ +# RAW_AZURE_TENANT_ID | the environment's `vars.*`, passed through by +# RAW_AZURE_SUBSCRIPTION_ID | the workflow (may be empty) +# RAW_AZURE_STORAGE_ACCOUNT | +# RAW_AZURE_STORAGE_CONTAINER +# RAW_CDN_BASE_URL / +# +# For widget-cdn-azure-sandbox, missing values fall back to the personal +# sandbox identifiers (they are identifiers, not credentials) so the sandbox +# works before its environment variables exist. Any other environment must be +# fully configured in Settings → Environments, or this fails with a clear +# error. Resolved values are appended to $GITHUB_ENV. +set -euo pipefail + +TARGET="${DEPLOY_TARGET:?DEPLOY_TARGET is required}" +CLIENT_ID="${RAW_AZURE_CLIENT_ID:-}" +TENANT_ID="${RAW_AZURE_TENANT_ID:-}" +SUBSCRIPTION_ID="${RAW_AZURE_SUBSCRIPTION_ID:-}" +STORAGE_ACCOUNT="${RAW_AZURE_STORAGE_ACCOUNT:-}" +CONTAINER="${RAW_AZURE_STORAGE_CONTAINER:-}" +BASE_URL="${RAW_CDN_BASE_URL:-}" + +if [ "$TARGET" = "widget-cdn-azure-sandbox" ]; then + CLIENT_ID="${CLIENT_ID:-69ca1ebc-8381-4b6d-864a-497053bdd827}" + TENANT_ID="${TENANT_ID:-8e8e42bf-64ee-4057-99ab-8288126d4ebb}" + SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-f60908b5-2c18-438c-89c5-510a01d2a802}" + STORAGE_ACCOUNT="${STORAGE_ACCOUNT:-layerswapcdntest}" +else + MISSING="" + [ -z "$CLIENT_ID" ] && MISSING="$MISSING AZURE_CLIENT_ID" + [ -z "$TENANT_ID" ] && MISSING="$MISSING AZURE_TENANT_ID" + [ -z "$SUBSCRIPTION_ID" ] && MISSING="$MISSING AZURE_SUBSCRIPTION_ID" + [ -z "$STORAGE_ACCOUNT" ] && MISSING="$MISSING AZURE_STORAGE_ACCOUNT" + if [ -n "$MISSING" ]; then + echo "::error::Environment '$TARGET' is missing variables:$MISSING — configure them in Settings → Environments." + exit 1 + fi +fi + +CONTAINER="${CONTAINER:-widget-cdn}" +BASE_URL="${BASE_URL:-https://${STORAGE_ACCOUNT}.blob.core.windows.net/${CONTAINER}}" + +{ + echo "AZURE_CLIENT_ID=$CLIENT_ID" + echo "AZURE_TENANT_ID=$TENANT_ID" + echo "AZURE_SUBSCRIPTION_ID=$SUBSCRIPTION_ID" + echo "AZURE_STORAGE_ACCOUNT=$STORAGE_ACCOUNT" + echo "AZURE_STORAGE_CONTAINER=$CONTAINER" + echo "CDN_BASE_URL=${BASE_URL%/}" +} >> "$GITHUB_ENV" + +echo "Resolved target $TARGET (account: $STORAGE_ACCOUNT, container: $CONTAINER)" diff --git a/.github/workflows/rebase-main-sandbox.yml b/.github/workflows/rebase-main-sandbox.yml index f2d3646f52..9bc6e80817 100644 --- a/.github/workflows/rebase-main-sandbox.yml +++ b/.github/workflows/rebase-main-sandbox.yml @@ -3,20 +3,24 @@ on: push: branches: [main] permissions: - contents: write + contents: read jobs: rebase-main-sandbox: + permissions: + contents: write timeout-minutes: 2 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 - name: Set Git config run: | git config --local user.email "actions@github.com" git config --local user.name "Github Actions" - name: Merge main to main-sandbox run: | - git fetch --unshallow - git checkout main-sandbox + git fetch origin main-sandbox + git checkout -B main-sandbox origin/main-sandbox git rebase main - git push + git push origin HEAD:main-sandbox diff --git a/.github/workflows/widget-cdn-cleanup.yml b/.github/workflows/widget-cdn-cleanup.yml new file mode 100644 index 0000000000..3ac6bf9b3f --- /dev/null +++ b/.github/workflows/widget-cdn-cleanup.yml @@ -0,0 +1,202 @@ +name: widget-cdn — cleanup old builds + +# Reference-aware retention for the widget CDN storage in Azure Blob Storage +# (policy in apps/widget-cdn/scripts/cleanup-lib.mjs), run as +# plan → approve → apply: +# +# 1. The `plan` job computes what is deletable, prints the keep/delete +# report to the run summary, and uploads the plan as an artifact. It also +# warns when the promoted build's manifest is near expiry (the live +# channel fails closed past it). +# 2. The `apply` job waits on the target ENVIRONMENT's required reviewers — +# the "we can delete these 4 old builds, delete them?" button. Review the +# plan job's summary, then approve. It deletes exactly the approved plan +# (intersected with fresh state, so anything promoted or uploaded since +# planning stays protected). If the plan is empty, apply is skipped and +# no approval is requested. +# +# The target is a GitHub environment carrying the Azure identity as variables +# (see .github/scripts/resolve-widget-cdn-env.sh; the sandbox has inline +# fallbacks, production must be configured in Settings → Environments). Both +# jobs declare the environment — if it has required reviewers, the read-only +# plan needs one approval and the delete a second, informed one. +# +# Note: `schedule` only fires from the repository's default branch (`dev`). + +on: + schedule: + - cron: "17 4 * * 1" # weekly sandbox plan; deletes only if approved + workflow_dispatch: + inputs: + environment: + description: "Storage target (GitHub environment)" + type: choice + options: [widget-cdn-azure-sandbox, widget-cdn-production] + default: widget-cdn-azure-sandbox + keep_recent: + description: "Most recent builds kept per channel regardless of expiry" + type: string + required: false + default: "3" + asset_grace_days: + description: "Grace period before unreferenced shared assets are deleted" + type: string + required: false + default: "2" + +concurrency: + group: widget-cdn-cleanup-${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + cancel-in-progress: false + +permissions: + contents: read + +env: + DEPLOY_TARGET: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + KEEP_RECENT: ${{ inputs.keep_recent || '3' }} + ASSET_GRACE_DAYS: ${{ inputs.asset_grace_days || '2' }} + +jobs: + plan: + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + permissions: + contents: read + id-token: write + outputs: + has-deletions: ${{ steps.plan.outputs.has-deletions }} + + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Resolve storage target + env: + RAW_AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + RAW_AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + RAW_AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + RAW_AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }} + RAW_AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} + RAW_CDN_BASE_URL: ${{ vars.CDN_BASE_URL }} + run: bash .github/scripts/resolve-widget-cdn-env.sh + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build shared widget protocol + run: pnpm --filter @layerswap/widget-types build + + - name: Log in to Azure with OIDC + uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2 + with: + client-id: ${{ env.AZURE_CLIENT_ID }} + tenant-id: ${{ env.AZURE_TENANT_ID }} + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} + + - name: Compute cleanup plan (dry run) + id: plan + working-directory: apps/widget-cdn + run: | + set -euo pipefail + node scripts/cleanup-azure.mjs \ + --keep-recent "$KEEP_RECENT" --asset-grace-days "$ASSET_GRACE_DAYS" \ + --plan-out "$RUNNER_TEMP/plan.json" 2>&1 | tee "$RUNNER_TEMP/cleanup.log" + COUNT=$(node -e "const p=require(process.env.RUNNER_TEMP+'/plan.json'); console.log(p.deleteBuilds.length + p.deleteAssets.length)") + echo "has-deletions=$([ "$COUNT" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + { + echo "## widget-cdn cleanup plan (${DEPLOY_TARGET})" + echo "" + if [ "$COUNT" -gt 0 ]; then + echo "**$COUNT object group(s) are deletable. Approve the \`apply\` job to delete them.**" + else + echo "Nothing to delete — apply will be skipped." + fi + echo "" + echo '```' + cat "$RUNNER_TEMP/cleanup.log" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload plan artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: cleanup-plan + path: ${{ runner.temp }}/plan.json + retention-days: 7 + + apply: + needs: plan + if: ${{ needs.plan.outputs.has-deletions == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + # The approval gate: this environment's required reviewers must approve + # before anything is deleted from the container. + environment: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Resolve storage target + env: + RAW_AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + RAW_AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + RAW_AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + RAW_AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }} + RAW_AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} + RAW_CDN_BASE_URL: ${{ vars.CDN_BASE_URL }} + run: bash .github/scripts/resolve-widget-cdn-env.sh + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build shared widget protocol + run: pnpm --filter @layerswap/widget-types build + + - name: Download approved plan + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: cleanup-plan + path: ${{ runner.temp }} + + - name: Log in to Azure with OIDC + uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2 + with: + client-id: ${{ env.AZURE_CLIENT_ID }} + tenant-id: ${{ env.AZURE_TENANT_ID }} + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} + + - name: Apply approved plan + working-directory: apps/widget-cdn + run: | + set -euo pipefail + node scripts/cleanup-azure.mjs --apply --plan-in "$RUNNER_TEMP/plan.json" \ + --keep-recent "$KEEP_RECENT" --asset-grace-days "$ASSET_GRACE_DAYS" \ + 2>&1 | tee "$RUNNER_TEMP/cleanup.log" + { + echo "## widget-cdn cleanup applied (${DEPLOY_TARGET})" + echo "" + echo '```' + cat "$RUNNER_TEMP/cleanup.log" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/widget-cdn-deploy.yml b/.github/workflows/widget-cdn-deploy.yml index 66ea1c5309..b16c7a27ac 100644 --- a/.github/workflows/widget-cdn-deploy.yml +++ b/.github/workflows/widget-cdn-deploy.yml @@ -1,77 +1,90 @@ -name: widget-cdn — build, sign, deploy +name: widget-cdn — deploy -# Production builds of `apps/widget-cdn` go through CI so the signing key never -# lives outside `secrets.LAYERSWAP_PRIVATE_KEY_PEM`. The build is uploaded to -# Cloudflare R2 under an immutable per-build prefix and the rolling channel -# pointer (`channels.json`) is flipped to it. The Cloudflare Worker in -# `apps/widget-cdn/worker` serves R2 and 302-redirects `/vN/*` to the current -# build. This workflow is the only path to a production CDN deploy. +# Azure Blob Storage is the only CDN deploy path. The target is a GitHub +# ENVIRONMENT (Settings → Environments), which carries the Azure identity as +# environment VARIABLES and the manifest signing key as an environment SECRET: +# +# variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, +# AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_CONTAINER (optional, +# default widget-cdn), CDN_BASE_URL (optional, for a custom +# domain / Front Door; defaults to the Blob endpoint) +# secrets: LAYERSWAP_PRIVATE_KEY_PEM (each environment gets its OWN +# keypair — the production key must never exist at repo level) +# +# - widget-cdn-azure-sandbox: the personal-account sandbox. Its identifiers +# (not credentials) are hardcoded as fallbacks below, so it works before +# the environment has any variables configured. +# - widget-cdn-production: drop-in — fails with a clear error until a repo +# admin creates the production Azure identity/storage account and fills in +# the environment. Dispatches only from main, after dev is merged to main. +# +# A push to dev that touches the widget build graph prepares an unsigned release +# candidate for the sandbox. The job that signs it, obtains Azure credentials, +# and writes to storage targets the protected GitHub environment and therefore +# pauses for required-reviewer approval ("Review deployments"). Production is +# workflow_dispatch with environment=widget-cdn-production and its own required +# reviewers. on: push: - # `babkenmes/widget-cdn-delivery` is temporary — for testing the full - # deploy pipeline on the feature branch before merging. Remove it (leaving - # just `main`) once the CDN is verified end-to-end. - branches: [main, babkenmes/widget-cdn-delivery] + branches: + - dev paths: - 'apps/widget-cdn/**' - - 'packages/widget/**' - - 'packages/wallets/**' + - 'packages/**' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' - '.github/workflows/widget-cdn-deploy.yml' + - '.github/scripts/resolve-widget-cdn-env.sh' workflow_dispatch: inputs: + environment: + description: "Deployment target (GitHub environment)" + type: choice + options: [widget-cdn-azure-sandbox, widget-cdn-production] + default: widget-cdn-azure-sandbox promote: - description: 'Flip the rolling channel to this build (uncheck to upload only)' + description: "Publish this build to the rolling v1 channel" type: boolean required: false default: true - deploy_worker: - description: 'Also deploy the Cloudflare Worker (edge router)' - type: boolean - required: false - default: false concurrency: - group: widget-cdn-${{ github.ref }} + group: widget-cdn-deploy-${{ inputs.environment || 'widget-cdn-azure-sandbox' }} cancel-in-progress: false permissions: contents: read - # Reserved for an OIDC upgrade path if the signing key moves into KMS later. - id-token: write - # Lets the failure alert open/comment a GitHub issue. - issues: write jobs: - deploy: + build: runs-on: ubuntu-latest - environment: widget-cdn-production + timeout-minutes: 30 env: - # R2 credentials (an R2 API token scoped to the bucket) + endpoint. - R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} - R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} - R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} - R2_BUCKET: ${{ vars.R2_BUCKET || 'layerswap-widget-cdn' }} - # Promotion defaults to true on push; honors the dispatch toggle otherwise. - LAYERSWAP_PROMOTE: ${{ (github.event_name == 'workflow_dispatch' && inputs.promote == false) && 'false' || 'true' }} - # Published builds are immutable, but the buildId embeds the commit sha - # (see apps/widget-cdn/scripts/build-id.mjs), so every push gets a fresh - # prefix — only literally re-deploying the SAME commit collides. For that - # rare case, uncomment the next line. Re-comment afterwards. - # ALLOW_OVERWRITE: '1' + DEPLOY_TARGET: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + outputs: + build-id: ${{ steps.metadata.outputs.build-id }} + build-time: ${{ steps.metadata.outputs.build-time }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: fetch-depth: 0 + persist-credentials: false + + - name: Validate deployment target + run: | + set -euo pipefail + # Production must ship code that has completed the dev -> main path. + if [ "$DEPLOY_TARGET" = "widget-cdn-production" ] \ + && [ "$GITHUB_REF_NAME" != "main" ]; then + echo "::error::Refusing to deploy '$GITHUB_REF_NAME' to production — dispatch from 'main'." + exit 1 + fi - # No `version:` — let the action read it from package.json's - # `packageManager` field, so CI tracks the repo's pinned pnpm exactly. - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 20 cache: pnpm @@ -82,138 +95,174 @@ jobs: - name: Build workspace packages run: pnpm build:packages - # Exercise the release-critical build identity, deploy/rollback, SRI, - # public type, and Worker contracts before the signing key is staged or - # any artifact can be uploaded to production. - name: Test widget release tooling run: | pnpm --filter @layerswap/wallet-core test pnpm --filter @layerswap/widget-cdn test pnpm --filter @layerswap/widget-js test pnpm --filter @layerswap/widget-react check:types - pnpm exec tsc -p apps/widget-cdn/worker/tsconfig.json --noEmit - # Build provenance stamped into the signed manifest. - name: Compute build metadata + id: metadata run: | - echo "LAYERSWAP_GIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV" - echo "LAYERSWAP_BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) BUILD_ID=$(node --input-type=module -e "import { resolveBuildIdentity } from './apps/widget-cdn/scripts/build-id.mjs'; process.stdout.write(resolveBuildIdentity('./apps/widget-cdn').buildId)") + echo "build-time=${BUILD_TIME}" >> "$GITHUB_OUTPUT" + echo "build-id=${BUILD_ID}" >> "$GITHUB_OUTPUT" + echo "LAYERSWAP_GIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV" + echo "LAYERSWAP_BUILD_TIME=${BUILD_TIME}" >> "$GITHUB_ENV" echo "LAYERSWAP_BUILD_ID=${BUILD_ID}" >> "$GITHUB_ENV" - # Stage the signing key to a tempfile (not argv/env-dump visible). Wiped - # at job end regardless of outcome. - - name: Stage signing key + # Build before approval so reviewers gate a concrete release candidate. + # It remains unsigned: the signing key is only exposed to the protected + # deploy job after approval. + - name: Build unsigned release candidate + working-directory: apps/widget-cdn + run: pnpm build + + - name: Upload release candidate + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: widget-cdn-${{ steps.metadata.outputs.build-id }} + path: apps/widget-cdn/dist + if-no-files-found: error + retention-days: 7 + + - name: Summary + run: | + { + echo "## widget-cdn release candidate" + echo "" + echo "- Target: \`${DEPLOY_TARGET}\`" + echo "- Build id: \`${LAYERSWAP_BUILD_ID}\`" + echo "- Commit: \`${GITHUB_SHA}\`" + echo "" + echo "Build and tests passed. Approve the protected deployment environment to publish this candidate." + } >> "$GITHUB_STEP_SUMMARY" + + deploy: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + # This is the approval boundary. Configure required reviewers for both + # environments in Settings → Environments; no signing or Azure access is + # possible before this job is approved. + environment: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + permissions: + contents: read + id-token: write + env: + DEPLOY_TARGET: ${{ inputs.environment || 'widget-cdn-azure-sandbox' }} + LAYERSWAP_PROMOTE: ${{ github.event_name != 'workflow_dispatch' || inputs.promote != false }} + LAYERSWAP_GIT_SHA: ${{ github.sha }} + LAYERSWAP_BUILD_TIME: ${{ needs.build.outputs.build-time }} + LAYERSWAP_BUILD_ID: ${{ needs.build.outputs.build-id }} + + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Resolve deployment target + env: + RAW_AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + RAW_AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + RAW_AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + RAW_AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }} + RAW_AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} + RAW_CDN_BASE_URL: ${{ vars.CDN_BASE_URL }} + run: bash .github/scripts/resolve-widget-cdn-env.sh + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build shared widget protocol + run: pnpm --filter @layerswap/widget-types build + + - name: Download approved release candidate + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: widget-cdn-${{ needs.build.outputs.build-id }} + path: apps/widget-cdn/dist + + # The signing key is the target environment's own secret. Rotating a key + # during a normal deploy would break every existing installation pinned + # to its public half. + - name: Materialize manifest signing key id: key env: - PEM: ${{ secrets.LAYERSWAP_PRIVATE_KEY_PEM }} + PRIVATE_KEY_PEM: ${{ secrets.LAYERSWAP_PRIVATE_KEY_PEM }} run: | - if [ -z "$PEM" ]; then - echo "::error::secrets.LAYERSWAP_PRIVATE_KEY_PEM is not set." + if [ -z "$PRIVATE_KEY_PEM" ]; then + echo "::error::LAYERSWAP_PRIVATE_KEY_PEM is not configured for environment '$DEPLOY_TARGET'" exit 1 fi umask 077 KEY_PATH="$RUNNER_TEMP/layerswap-signing.pem" - printf '%s\n' "$PEM" > "$KEY_PATH" + printf '%s' "$PRIVATE_KEY_PEM" > "$KEY_PATH" echo "key-path=$KEY_PATH" >> "$GITHUB_OUTPUT" - - name: Build + sign widget-cdn + - name: Sign and verify approved artifact working-directory: apps/widget-cdn env: LAYERSWAP_PRIVATE_KEY_PEM: ${{ steps.key.outputs.key-path }} - run: pnpm build + run: | + node scripts/build-manifest.mjs + pnpm verify-manifest - # Round-trip the signature against the public key bundled into the loader - # core. Fails the deploy on a key mismatch before anything ships. - - name: Verify signature against bundled public key - working-directory: apps/widget-cdn - run: pnpm verify-manifest + - name: Log in to Azure with OIDC + uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2 + with: + client-id: ${{ env.AZURE_CLIENT_ID }} + tenant-id: ${{ env.AZURE_TENANT_ID }} + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} - # Upload the immutable build to R2 and (unless promote=false) flip the - # rolling channel pointer. Refuses to overwrite an already-published - # buildId, so re-running a deploy for the same commit is a no-op error - # rather than a silent clobber — any new commit gets a fresh buildId. - - name: Upload to R2 + promote channel + - name: Upload to Azure Blob Storage and promote channel working-directory: apps/widget-cdn - run: pnpm deploy:r2 + run: pnpm deploy:azure - name: Wipe signing key from runner if: always() run: rm -f "$RUNNER_TEMP/layerswap-signing.pem" - # Optional: deploy the edge Worker. Only needed when worker code changes; - # off by default on push, opt-in via workflow_dispatch. - - name: Deploy Cloudflare Worker - if: ${{ github.event_name == 'workflow_dispatch' && inputs.deploy_worker == true }} - working-directory: apps/widget-cdn - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} - run: pnpm worker:deploy - - # End-to-end check against the live CDN: the rolling channel resolves, - # the manifest is reachable with CORS, signed, and kill-switch off. - - name: Smoke-test the live channel - if: ${{ env.LAYERSWAP_PROMOTE != 'false' && vars.CDN_BASE_URL != '' }} + - name: Smoke-test the public Blob channel + if: ${{ env.LAYERSWAP_PROMOTE != 'false' }} run: | set -euo pipefail - VERSION=$(node -e "console.log(require('./apps/widget-cdn/node_modules/@layerswap/widget/package.json').version)") - CHANNEL="v${VERSION%%.*}" - MANIFEST_URL="${{ vars.CDN_BASE_URL }}/${CHANNEL}/manifest.json" - echo "Fetching (following redirects) $MANIFEST_URL" - curl -fsSL -L -H "Origin: https://example.com" -D /tmp/headers "$MANIFEST_URL" -o /tmp/manifest.json - grep -i 'access-control-allow-origin' /tmp/headers || { echo "::error::CDN missing CORS header"; exit 1; } + MANIFEST_URL="${CDN_BASE_URL}/v1/manifest.json" + curl -fsSL -H "Origin: https://example.com" -D /tmp/headers \ + "$MANIFEST_URL" -o /tmp/manifest.json + grep -i 'access-control-allow-origin' /tmp/headers \ + || { echo "::error::Azure Blob CORS header is missing"; exit 1; } node -e " const m = JSON.parse(require('fs').readFileSync('/tmp/manifest.json','utf8')); - if (!m.signature) { console.error('live manifest has no signature'); process.exit(1); } - if (m.killSwitch) { console.error('live manifest has killSwitch=true'); process.exit(1); } - if (m.buildId !== '${LAYERSWAP_BUILD_ID}') { console.error('live channel still on build', m.buildId, '— expected ${LAYERSWAP_BUILD_ID}'); process.exit(1); } - if (m.version !== '${VERSION}') { console.error('live channel still on', m.version, '— expected ${VERSION}'); process.exit(1); } - if (m.gitSha !== '${GITHUB_SHA}') { console.error('live channel still on commit', m.gitSha, '— expected ${GITHUB_SHA}'); process.exit(1); } - console.log('live channel OK buildId=', m.buildId, ' version=', m.version, ' gitSha=', m.gitSha); + if (!m.signature) throw new Error('manifest has no signature'); + if (m.buildId !== '${LAYERSWAP_BUILD_ID}') { + throw new Error('channel has ' + m.buildId + ', expected ${LAYERSWAP_BUILD_ID}'); + } + const remote = new URL(m.remoteEntry, '${MANIFEST_URL}').href; + if (!remote.includes('/${LAYERSWAP_BUILD_ID}/remoteEntry.js')) { + throw new Error('remoteEntry is not immutable: ' + remote); + } + console.log('Azure channel OK', { buildId: m.buildId, remote }); " - name: Summary if: always() run: | - VERSION=$(node -e "console.log(require('./apps/widget-cdn/node_modules/@layerswap/widget/package.json').version)" 2>/dev/null || echo "?") - BUILD_ID=${LAYERSWAP_BUILD_ID:-?} { echo "## widget-cdn deploy" echo "" - echo "- Build id: \`${BUILD_ID}\`" - echo "- Version: \`${VERSION}\`" - echo "- Channel promoted: \`${LAYERSWAP_PROMOTE}\`" + echo "- Target: \`${DEPLOY_TARGET}\`" + echo "- Build id: \`${LAYERSWAP_BUILD_ID:-?}\`" + echo "- Promoted: \`${LAYERSWAP_PROMOTE}\`" echo "- Commit: \`${GITHUB_SHA}\`" } >> "$GITHUB_STEP_SUMMARY" - - # A failed deploy means the live channel silently keeps serving the - # previous build — that must never be discovered by noticing stale - # behavior in the browser. Open a GitHub issue @mentioning the pusher - # (triggers a GitHub notification/email); if an open deploy-failure - # issue already exists, comment on it instead of piling up duplicates. - - name: Alert on failure - if: failure() - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - VERSION=$(node -p "require('./packages/widget/core/package.json').version" 2>/dev/null || echo "?") - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - TITLE="widget-cdn deploy failed" - { - echo "@${{ github.actor }} the widget-cdn deploy failed — the live CDN channel is still serving the **previous** build." - echo "" - echo "- Version: \`${VERSION}\`" - echo "- Branch: \`${GITHUB_REF_NAME}\` @ \`${GITHUB_SHA::7}\`" - echo "- Run: ${RUN_URL}" - echo "" - echo "Check the run log for the failing step. (Build ids embed the commit sha, so an immutability collision only happens when re-deploying the same commit — re-run from a new commit or set \`ALLOW_OVERWRITE=1\`.)" - } > "$RUNNER_TEMP/alert-body.md" - EXISTING=$(gh issue list --repo "${{ github.repository }}" --state open \ - --search "\"${TITLE}\" in:title" --json number --jq '.[0].number // empty') - if [ -n "$EXISTING" ]; then - gh issue comment "$EXISTING" --repo "${{ github.repository }}" --body-file "$RUNNER_TEMP/alert-body.md" - else - gh issue create --repo "${{ github.repository }}" --title "$TITLE" --body-file "$RUNNER_TEMP/alert-body.md" - fi diff --git a/.github/workflows/widget-cdn-pr.yml b/.github/workflows/widget-cdn-pr.yml new file mode 100644 index 0000000000..943ebdcbcc --- /dev/null +++ b/.github/workflows/widget-cdn-pr.yml @@ -0,0 +1,57 @@ +name: widget-cdn — PR validation + +on: + pull_request: + branches: [dev, main] + paths: + - "apps/widget-cdn/**" + - "examples/widget-react-host/**" + - "packages/**" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - ".github/workflows/widget-cdn-*.yml" + - ".github/scripts/resolve-widget-cdn-env.sh" + +# PR code is untrusted. This workflow uses a GitHub-hosted runner, has only a +# read-only token, and never references deployment environments or secrets. +permissions: + contents: read + +concurrency: + group: widget-cdn-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace packages + run: pnpm build:packages + + - name: Test widget packages and release tooling + run: | + pnpm --filter @layerswap/widget-cdn test + pnpm --filter @layerswap/widget-js test + pnpm --filter @layerswap/widget-react check:types + pnpm --filter @layerswap/widget-react test + + - name: Build release candidates + run: | + pnpm --filter @layerswap/widget-cdn build + pnpm --filter widget-react-host-example build diff --git a/apps/widget-cdn/.keys/manifest-public.b64.txt b/apps/widget-cdn/.keys/manifest-public.b64.txt index e49d84107b..efd00e8aea 100644 --- a/apps/widget-cdn/.keys/manifest-public.b64.txt +++ b/apps/widget-cdn/.keys/manifest-public.b64.txt @@ -1 +1 @@ -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESuHFHbltz/hfcY+DzIrLq7Ixc4efHE8SLZdNg0pZZDHTfdwbqLpGk4461EgNranHLWnVsoAbyQ4IyHIVnRAVKw== +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY9/zg6ZUU8ZVOAbuS4qqreIR6/U8BDqY+8giwi8xFYl5dllwzE0x//l2iVRgo3fr6nRpplm9RUYRKcXaiJC/Lg== diff --git a/apps/widget-cdn/AZURE_SETUP.md b/apps/widget-cdn/AZURE_SETUP.md new file mode 100644 index 0000000000..07bfcaef99 --- /dev/null +++ b/apps/widget-cdn/AZURE_SETUP.md @@ -0,0 +1,245 @@ +# Azure Blob Storage sandbox setup + +This is the low-cost personal-account setup for validating widget delivery +before Layerswap provisions Azure Front Door and a private production origin. +It uses a dedicated public Blob container because browsers must fetch the +manifest and JavaScript anonymously. + +Do not reuse this account for unrelated private data. + +## 1. Create the storage account + +In the [Azure portal](https://portal.azure.com): + +1. Open **Storage accounts** and select **Create**. +2. Create or choose a resource group, for example + `widget-cdn-sandbox-rg`. +3. Enter a globally unique lowercase account name, for example + `yournamewidgetcdn`. +4. Choose: + - Preferred storage type: **Blob storage or Azure Data Lake Storage**. + - Region: a nearby region. + - Performance: **Standard**. + - Redundancy: **LRS** for this temporary sandbox. +5. On **Advanced**: + - Require secure transfer: **Enabled**. + - Allow enabling anonymous access on individual containers: **Enabled**. + - Enable storage account key access: **Disabled**. + - Default to Microsoft Entra authorization in the Azure portal: **Enabled**. + - Minimum TLS version: **1.2**. + - Hierarchical namespace: **Disabled**. +6. On **Networking**, allow public network access from all networks for the + sandbox. +7. Select **Review + create**, then **Create**. + +## 2. Protect accidental overwrites + +Open the storage account, then **Data management → Data protection**: + +- Enable blob soft delete with a 7-day retention period. +- Enable container soft delete with a 7-day retention period. +- Enable blob versioning. + +The only routinely overwritten blob is `v1/manifest.json`. Versioning gives +the sandbox a recovery history for channel promotions. + +## 3. Grant your user deployment access + +Being the subscription owner does not always grant Blob data access. + +1. Open **Access control (IAM)** on the storage account. +2. Select **Add → Add role assignment**. +3. Choose **Storage Blob Data Contributor**. +4. Assign access to **User, group, or service principal**. +5. Select your personal Azure user and save. + +The role can take several minutes to propagate. Wait for it before continuing +because the portal is configured to use Microsoft Entra authorization instead +of the storage account key. + +## 4. Create the public container + +1. Open **Data storage → Containers**. +2. Select **+ Container**. +3. Name it `widget-cdn`. +4. Set anonymous access to **Blob (anonymous read access for blobs only)**. + Do not choose container-level listing access. +5. Select **Create**. + +If the anonymous-access option is disabled, open +**Settings → Configuration**, enable **Allow Blob anonymous access**, save, +and retry. + +The public base URL will be: + +```text +https://.blob.core.windows.net/widget-cdn +``` + +## 5. Configure browser CORS + +Open **Settings → Resource sharing (CORS)**. In the **Blob service** row add: + +| Field | Value | +| --------------- | ------------------------------------------------ | +| Allowed origins | `*` | +| Allowed methods | `GET`, `HEAD`, `OPTIONS` | +| Allowed headers | `*` | +| Exposed headers | `ETag,Content-Length,Content-Type,Cache-Control` | +| Max age | `86400` | + +Save the rule. This is required for the cross-origin manifest fetch and +`crossorigin="anonymous"` JavaScript loads. + +## 6. Authenticate locally + +Install Azure CLI on macOS if `az` is not already available: + +```bash +brew install azure-cli +az login +az account set --subscription "" +az account show --output table +``` + +The deploy script asks Azure CLI for a short-lived Microsoft Entra access +token. It does not use a storage account key or connection string. + +## 7. Build a sandbox artifact + +Create a temporary signing key. It is ignored by git and is intentionally not +the production trust key: + +```bash +openssl ecparam -name prime256v1 -genkey -noout \ + -out apps/widget-cdn/.keys/azure-test-private.pem + +LAYERSWAP_PRIVATE_KEY_PEM=.keys/azure-test-private.pem \ + pnpm --filter @layerswap/widget-cdn build +``` + +The sandbox key will not verify against the public key compiled into the +production loader. Use verification-off only in the internal example harness. + +## 8. Deploy and promote `v1` + +```bash +AZURE_STORAGE_ACCOUNT="" \ +AZURE_STORAGE_CONTAINER="widget-cdn" \ +pnpm --filter @layerswap/widget-cdn deploy:azure +``` + +The command uploads immutable objects first, publishes the immutable manifest +last, then copies the signed manifest to `v1/manifest.json`. The channel blob +is written with `Cache-Control: no-store, max-age=0`; immutable build files and +shared assets retain their one-year immutable cache policy. If Front Door or +another CDN is later added, configure the manifest route to honor the origin +header or disable caching for `*/manifest.json`. + +Upload without changing `v1`: + +```bash +LAYERSWAP_PROMOTE=false \ +AZURE_STORAGE_ACCOUNT="" \ +AZURE_STORAGE_CONTAINER="widget-cdn" \ +pnpm --filter @layerswap/widget-cdn deploy:azure +``` + +Rollback/promotion to an already-uploaded build: + +```bash +AZURE_STORAGE_ACCOUNT="" \ +AZURE_STORAGE_CONTAINER="widget-cdn" \ +pnpm --filter @layerswap/widget-cdn rollback:azure -- \ + v1 1.7.0- +``` + +## 9. Smoke-test Blob delivery + +```bash +export WIDGET_AZURE_BASE="https://.blob.core.windows.net/widget-cdn" + +curl -fsSL -H "Origin: http://127.0.0.1:3001" \ + -D /tmp/widget-azure-headers \ + "$WIDGET_AZURE_BASE/v1/manifest.json" \ + -o /tmp/widget-azure-manifest.json + +grep -i "access-control-allow-origin" /tmp/widget-azure-headers +node -e " + const m = require('/tmp/widget-azure-manifest.json'); + console.log({ + buildId: m.buildId, + remoteEntry: new URL(m.remoteEntry, process.env.WIDGET_AZURE_BASE + '/v1/manifest.json').href + }); +" +``` + +The published React/JS loaders cannot be repointed at this sandbox: their +manifest URL and signature verification policy are fixed inside +`@layerswap/widget-js`. Browser integration testing requires a loader build +whose internal `WIDGET_MANIFEST_URL` and trust anchor target the sandbox; do not add +host environment variables or globals to bypass that boundary. + +## 10. GitHub Actions environments + +After local delivery works, `.github/workflows/widget-cdn-deploy.yml` deploys +merges to `dev` to the personal account through OIDC. Its automatic target is +the `widget-cdn-azure-sandbox` GitHub environment; the sandbox identifiers are +inline fallbacks, so only the `LAYERSWAP_PRIVATE_KEY_PEM` signing secret is +required. Production is a manual dispatch from `main` to the +`widget-cdn-production` environment. + +In Azure Portal: + +1. Open **Microsoft Entra ID → App registrations → New registration**. +2. Name it `layerswap-widget-cdn-github-test`, use the current tenant only, + and leave the redirect URI empty. +3. Open **Certificates & secrets → Federated credentials → Add credential**. +4. Select the GitHub Actions scenario and configure: + - Organization: `layerswap` + - Repository: `layerswapapp` + - Entity type: **Environment** + - Environment: `widget-cdn-azure-sandbox` +5. Assign this application **Storage Blob Data Contributor** on only the + `widget-cdn` container. +6. Create a separate production identity and environment-based federated + credential for `widget-cdn-production`, scoped only to the production + container. + +In GitHub, a repository administrator must then: + +1. Open **Settings → Environments** and create + `widget-cdn-azure-sandbox`. +2. Add one or more **Required reviewers** under deployment protection rules. + Enable **Prevent self-review** when the person who starts a deployment must + not approve it. +3. Add `LAYERSWAP_PRIVATE_KEY_PEM` as an environment secret and add the Azure + identifiers described in the [CI deploy configuration](./README.md#ci-deploy) + as environment variables. +4. Repeat for `widget-cdn-production`, using the production approvers, + identity, storage account, and signing key. + +Do not place the signing key or Azure deployment configuration at repository +scope. Environment-scoped values remain unavailable to the deploy job until a +required reviewer approves it. + +Because each privileged job declares a GitHub Environment, Azure accepts its +OIDC token only when the corresponding subject is exactly: + +```text +repo:layerswap/layerswapapp:environment:widget-cdn-azure-sandbox +repo:layerswap/layerswapapp:environment:widget-cdn-production +``` + +The Azure client, tenant, subscription, storage account, and container +identifiers in the sandbox workflow are not credentials. The workflow builds +and tests an unsigned candidate first; only the approved environment job can +access its signing key or request the Azure OIDC token. + +## Production follow-up + +The Layerswap account should replace this public sandbox origin with Azure +Front Door and preferably a private Blob origin. The final rollout also needs: + +- The production manifest signing key/KMS. +- A stable custom domain such as `cdn.layerswap.io`. diff --git a/apps/widget-cdn/README.md b/apps/widget-cdn/README.md index ce5204ac18..0901bf43be 100644 --- a/apps/widget-cdn/README.md +++ b/apps/widget-cdn/README.md @@ -1,65 +1,69 @@ # @layerswap/widget-cdn Rspack Module-Federation **remote** that exposes `@layerswap/widget` for -runtime delivery, plus the Cloudflare **Worker + R2** edge that serves it. +runtime delivery from **Azure Blob Storage**. - `name`: `layerswap_widget` - `filename`: `remoteEntry.js` -- `exposes`: `./Widget`, `./mount` +- `exposes`: `./Widget`, `./mount` (swap widget), `./DepositWidget`, + `./mountDeposit` (deposit widget — fixed-destination funding flow) - Shared singletons: `react`, `react-dom`, `wagmi`, `viem`, `@tanstack/react-query`, `zustand` Consumed by `@layerswap/widget-react` (React hosts) and `@layerswap/widget-js` (framework-agnostic hosts). +See [AZURE_SETUP.md](./AZURE_SETUP.md) for the storage-account setup and the +production follow-up checklist. + ## Versioning model -Every build is published to an **immutable, buildId-named prefix** in R2 and -never overwritten. A single mutable pointer (`channels.json`) maps each rolling -major channel to its current build, and the Worker turns that into a redirect. +Every build is published to an **immutable, buildId-named prefix** and never +overwritten. Each rolling major channel is a single mutable blob — the signed +manifest copied to `//manifest.json`: ``` -R2 bucket (layerswap-widget-cdn) +Blob container (widget-cdn) ├── 1.5.0-abc123def456/ ← immutable build, write-once │ ├── manifest.json ← signed; describes this exact build │ └── remoteEntry.js ├── 1.5.0-fedcba654321/ ← next build, also immutable ├── assets/ ← shared content-addressed namespace │ └── ..js ← byte-identical chunks reuse one URL/object -└── channels.json ← the ONLY mutable object: { "v1": "1.5.0-abc123def456" } +└── v1/manifest.json ← the ONLY mutable object: the rolling channel + pointer (a copy of the promoted build's manifest) ``` -The Worker serves two kinds of URL: - | URL | Behavior | |---|---| -| `…/v1/manifest.json` | **Rolling** — Worker 302-redirects to the current `v1` build. Auto-updates within ~60s of a channel flip. | -| `…/1.5.0-abc123def456/manifest.json` | **Pinned** — frozen forever at that exact build. | +| `…/v1/manifest.json` | **Rolling** — `Cache-Control: no-store`, so new page loads observe a promotion immediately. | +| `…/1.5.0-abc123def456/manifest.json` | **Immutable** — exact build used for staging, rollback, and debugging. Its signed validity window still expires. | Integrators don't choose between them: the manifest URL is not a public knob. `@layerswap/widget-js` bakes in the rolling channel URL -(`DEFAULT_MANIFEST_URL` in `src/manifest.ts`) and `resolveSource()` takes no +(`WIDGET_MANIFEST_URL` in `src/manifest.ts`) and `resolveSource()` takes no arguments, so every integrator rides the channel and picks up pointer flips -automatically. Pinned URLs exist for Layerswap's own release mechanics -(staged releases, rollback targets, debugging a specific build) and are only -reachable from a loader via the internal `__LAYERSWAP_WIDGET_MANIFEST__` -override global — a build/test seam, not a supported integrator option. - -The loader follows the redirect and resolves the relative `remoteEntry` against -the **final** URL, so the remote anchors at the immutable build path. The -remote loads its content-hashed chunks from the stable `/assets/` namespace, -and the signed manifest registers SRI for both locations. **Rollback / -roll-forward is a pointer flip** — no rebuild, no re-upload (see -`scripts/rollback-r2.mjs`). +automatically. Immutable URLs exist for Layerswap's own release mechanics +(staged releases, rollback targets, and debugging a specific build); hosts +cannot point the loader at them. + +The manifest uses a build-addressed relative `remoteEntry` (for example +`../1.5.0-abc123def456/remoteEntry.js`), so it resolves to the same immutable +build path whether it is fetched from the immutable location or from the +rolling channel blob. The remote loads its content-hashed chunks from the +stable `/assets/` namespace, and the signed manifest registers SRI for both +locations. **Rollback / roll-forward is a pointer copy of an already-signed +manifest** — no rebuild, no re-upload (see `scripts/rollback-azure.mjs`). A build's immutable identity is its **buildId** — the `@layerswap/widget` version plus the git sha (`1.5.0-abc123def456`, see `scripts/build-id.mjs`) — because the deployed bytes also change with widget-cdn/wallets/widget-js/ -widget-react, none of which bump the widget version. The **version** remains -the host-facing compatibility number. A breaking change to -the embed/mount API or a required host singleton major (react/wagmi/viem) is -what warrants cutting a new major channel (`v2`); anything backward-compatible -ships within the existing channel. +widget-react, none of which bump the widget version. The manifest's +**protocolMajor** is the host-facing compatibility boundary and selects the +channel independently from the core implementation version. A breaking change +to the manifest, embed/mount API, exposed modules, or a required host runtime is +what warrants protocol `2`, loader package major `2`, and `/v2/`; compatible +changes continue rolling within `/v1/`. ## Dev @@ -68,16 +72,11 @@ pnpm dev ``` Serves the remote on `http://127.0.0.1:3100/remoteEntry.js`, plus an unsigned -`http://127.0.0.1:3100/manifest.json` pointing at it — so the loader's manifest -path works in dev exactly as in prod. Point a dev host at it via the internal -loader globals (set before the widget mounts; see `examples/widget-react-host`): +`http://127.0.0.1:3100/manifest.json` pointing at it. The published loader is +intentionally fixed to the signed production channel and cannot load this +unsigned development manifest. -```js -globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'http://127.0.0.1:3100/manifest.json'; -globalThis.__LAYERSWAP_WIDGET_VERIFY__ = false; // dev manifest is unsigned -``` - -Dev output stays flat in `dist/` (no version directory, no redirect). +Dev output stays flat in `dist/` (no version directory). ## Production build @@ -93,8 +92,7 @@ verifying loaders refuse a manifest that is expired or missing `expiresAt`), the shared `assetBase`, per-chunk SHA-384 SRI hashes, the kill switch, and the signature. Without `LAYERSWAP_PRIVATE_KEY_PEM` the manifest is emitted unsigned — fine for local builds, rejected by the deploy script and by the -loader (integrators always verify; only the internal -`__LAYERSWAP_WIDGET_VERIFY__ = false` dev seam accepts unsigned manifests). +loader (integrators always verify). `LAYERSWAP_RELEASE_VERSION` overrides the version label and `LAYERSWAP_RELEASE_ID` the buildId (and therefore the output directory) for a @@ -104,81 +102,149 @@ one-off build. pnpm verify-manifest # round-trip the signature against the bundled public key ``` -## The edge: Cloudflare Worker + R2 - -The Worker (`worker/`) serves R2 and does the rolling-channel redirect: - -- `GET /vN/` → reads `channels.json`, 302-redirects to `//` - (short cache so flips propagate fast). -- `GET //` → serves from R2 with `immutable` caching + permissive - CORS (chunks load `crossorigin="anonymous"` for SRI). -- `GET /assets/` → serves the shared immutable chunk used - by every build that emitted the same bytes. -- Security headers (HSTS, nosniff, frame-deny) on every response. - -```bash -pnpm worker:dev # local Worker dev -pnpm worker:deploy # wrangler deploy -``` - ## Deploy +Requires `az login` (or CI OIDC) plus `AZURE_STORAGE_ACCOUNT` / +`AZURE_STORAGE_CONTAINER` — see `scripts/azure-lib.mjs`. No account key or +connection string is used. + ```bash -pnpm deploy:r2 # upload build controls + shared assets, then flip channel -LAYERSWAP_PROMOTE=false pnpm deploy:r2 # upload only (staged release) -ALLOW_OVERWRITE=1 pnpm deploy:r2 # re-upload an existing build (escape hatch) +pnpm deploy:azure # local default: upload and promote +LAYERSWAP_PROMOTE=false pnpm deploy:azure # upload only (staged release) +ALLOW_OVERWRITE=1 pnpm deploy:azure # re-upload an existing build (escape hatch) # roll a channel to any already-published build (instant; no rebuild): -node scripts/rollback-r2.mjs v1 1.4.0-abc123def456 +node scripts/rollback-azure.mjs v1 1.4.0-abc123def456 ``` -`deploy:r2` refuses to overwrite an already-published buildId — published +`deploy:azure` refuses to overwrite an already-published buildId — published builds are immutable. It reuses content-hashed objects already present under `/assets/`, so unchanged chunks keep their browser cache and do not consume -duplicate R2 storage. The buildId embeds the commit sha, so deploying from any +duplicate storage. The buildId embeds the commit sha, so deploying from any new commit gets a fresh control-file prefix. -### CI deploy (production) - -Production deploys go through `.github/workflows/widget-cdn-deploy.yml`. The -signing key never leaves CI. The workflow: builds + signs → verifies the -signature against the bundled public key → uploads the immutable build to R2 → -flips the channel pointer → smoke-tests the live channel. The Worker is -deployed separately (manually, or via the `deploy_worker` dispatch input — it -rarely changes). +### CI deploy + +Deploys go through `.github/workflows/widget-cdn-deploy.yml` as two jobs. The +`build` job tests the release and uploads an unsigned release candidate. The +`deploy` job waits for approval of the target **GitHub environment**, then +signs that candidate (the key never leaves CI), verifies it, uploads it, +promotes the channel, and smoke-tests the public endpoint: + +- **`widget-cdn-azure-sandbox`** — the personal-account sandbox. A push + deploys here; its Azure identifiers are hardcoded fallbacks in + `.github/scripts/resolve-widget-cdn-env.sh`, so it works before the + environment has any variables configured. +- **`widget-cdn-production`** — drop-in: selectable via `workflow_dispatch` + today, fails with a clear "missing variables" error until a repo admin + creates the production Azure identity/storage account and fills in the + environment. Production dispatches are refused from non-default branches. + +Each environment carries its Azure identity as environment **variables** +(`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, +`AZURE_STORAGE_ACCOUNT`, optional `AZURE_STORAGE_CONTAINER` and +`CDN_BASE_URL` for a custom domain) and its **own** signing keypair as the +environment **secret** `LAYERSWAP_PRIVATE_KEY_PEM` — the production key must +never exist at repo level. + +Approval protection is required configuration; workflow YAML can reference an +environment but cannot define its reviewers. A repo admin must configure both +deployment environments in **Settings → Environments**: + +1. Open `widget-cdn-azure-sandbox`, add one or more **Required reviewers**, and + enable **Prevent self-review** if the deploy initiator must not approve it. +2. Repeat for `widget-cdn-production` with the production approver group. +3. Keep the Azure variables and `LAYERSWAP_PRIVATE_KEY_PEM` scoped to the + environment, not the repository. This ensures they are unavailable until + the approval is granted. + +With those protection rules enabled, every CDN deploy pauses at the `deploy` +job with "Review deployments". The job cannot sign a manifest, request an +Azure OIDC token, or write to Blob Storage before approval. Cleanup `apply` +jobs use the same gate. + +### Blob cache and retention policy + +Promotion overwrites only `//manifest.json`. It is published with +`Cache-Control: no-store, max-age=0` so new page loads observe a pointer +change without a CDN purge. Immutable build controls and content-addressed +assets remain `public, max-age=31536000, immutable`. + +If Azure Front Door or another CDN sits in front of Blob Storage, its route +for `*/manifest.json` must honor the origin header or have caching disabled. +Build and asset routes should keep long-lived caching. + +### Cleanup of old builds + +Do not apply an age-only delete policy to `/assets/`: chunks are shared between +builds. Reference-aware cleanup lives in `scripts/cleanup-azure.mjs` (policy in +`scripts/cleanup-lib.mjs`): -#### Required GitHub secrets +```bash +pnpm cleanup:azure # dry run — report only +pnpm cleanup:azure --plan-out plan.json # dry run + reviewable plan file +pnpm cleanup:azure --apply # delete everything deletable +pnpm cleanup:azure --apply --plan-in plan.json # delete only the approved plan +pnpm cleanup:azure --keep-recent 5 --asset-grace-days 2 --build-grace-days 2 +``` -| Secret | Value | -|---|---| -| `LAYERSWAP_PRIVATE_KEY_PEM` | ECDSA P-256 private key (PEM). Its public half is baked into `packages/widget/js/src/manifest.ts`. | -| `R2_ACCOUNT_ID` | Cloudflare account id. | -| `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` | R2 **Account** API token (Object Read & Write) credentials. | -| `CLOUDFLARE_API_TOKEN` | *(optional)* Only if CI deploys the Worker. | +A build prefix is **retained** when it is the current target of any rolling +channel, its manifest is not yet expired (unexpired builds are valid rollback +targets and in-flight sessions may still lazy-load their chunks), it is one of +the `--keep-recent` most recent builds of its channel, or it has no manifest +but was uploaded within `--build-grace-days` (an in-flight deploy publishes +its manifest last). Everything else is deleted — manifest first, so an +interrupted deletion can never leave a promotable half-build. Shared assets +are then mark-and-swept: chunks referenced by any retained manifest survive; +unreferenced ones are deleted after `--asset-grace-days`. Channel pointers and +unrecognized keys are never touched, anomalies are kept and warned about, and +apply aborts if a channel pointer changes between planning and deletion. + +The cleanup run also warns when a promoted build's manifest is expired or +expires within 7 days — the early signal that the live channel is about to +fail closed. + +CI runs (`.github/workflows/widget-cdn-cleanup.yml`) are **plan → approve → +apply**: a `plan` job posts the keep/delete report to the run summary and +saves it with `--plan-out`; the `apply` job waits on the +`widget-cdn-azure-sandbox` environment approval ("Review deployments") and +then deletes exactly the reviewed plan via `--apply --plan-in` — recomputed +against fresh state, so anything promoted or uploaded since planning stays +protected, and items that became deletable after approval wait for the next +plan. When nothing is deletable, apply is skipped and no approval is +requested. **Approvals only exist once a repo admin adds required reviewers** +in Settings → Environments → `widget-cdn-azure-sandbox`. + +#### Required GitHub configuration (per environment) + +| Kind | Name | Value | +|---|---|---| +| secret | `LAYERSWAP_PRIVATE_KEY_PEM` | ECDSA P-256 private key (PEM), one keypair per environment. The production public half is baked into `packages/widget/js/src/manifest.ts`. | +| variable | `AZURE_CLIENT_ID` / `AZURE_TENANT_ID` / `AZURE_SUBSCRIPTION_ID` | OIDC-federated identity for `azure/login`. | +| variable | `AZURE_STORAGE_ACCOUNT` | Target storage account. | +| variable | `AZURE_STORAGE_CONTAINER` | Optional; default `widget-cdn`. | +| variable | `CDN_BASE_URL` | Optional; custom domain / Front Door origin for smoke tests. Defaults to the Blob endpoint. | + +Each environment must also have a **Required reviewers** deployment protection +rule. This rule is what enforces approval; creating an unprotected environment +with the same name is not sufficient. + +Azure authentication uses OIDC federation (`azure/login`) — no storage keys or +connection strings are stored as secrets. -#### Required GitHub variables +### First-time infrastructure setup -| Variable | Value | -|---|---| -| `R2_BUCKET` | Bucket name (default `layerswap-widget-cdn`). | -| `CDN_BASE_URL` | Public CDN origin, e.g. the Worker's `*.workers.dev` URL or custom domain. Used by the smoke test. | +Follow [AZURE_SETUP.md](./AZURE_SETUP.md) (storage account, container, CORS, +RBAC, OIDC federation). The signing keypair is generated once: -### First-time infrastructure setup +```bash +openssl ecparam -name prime256v1 -genkey -noout -out .keys/manifest-private.pem +openssl ec -in .keys/manifest-private.pem -pubout -outform DER | base64 | tr -d '\n' +``` -1. **Enable R2** in the Cloudflare dashboard (Storage & databases → R2). -2. **Create the bucket**: `wrangler r2 bucket create layerswap-widget-cdn`. -3. **Deploy the Worker**: `pnpm worker:deploy` (registers a `*.workers.dev` - subdomain on first run, or wire a custom domain in `worker/wrangler.toml`). -4. **Create an R2 Account API token** (Object Read & Write) → set - `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` / `R2_ACCOUNT_ID`. -5. **Generate the signing keypair** (one-time): - ```bash - openssl ecparam -name prime256v1 -genkey -noout -out .keys/manifest-private.pem - openssl ec -in .keys/manifest-private.pem -pubout -outform DER | base64 | tr -d '\n' - ``` - Put the base64 SPKI into `packages/widget/js/src/manifest.ts` - (`MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64`) and `.keys/manifest-public.b64.txt`, - and the private PEM into the `LAYERSWAP_PRIVATE_KEY_PEM` secret. +Put the base64 SPKI into `packages/widget/js/src/manifest.ts` +(`MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64`) and `.keys/manifest-public.b64.txt`, +and the private PEM into the `LAYERSWAP_PRIVATE_KEY_PEM` secret. ### Key rotation @@ -194,6 +260,7 @@ The public key constant in `@layerswap/widget-js` is the trust anchor. Rotate: ### Upgrading to KMS The current workflow trusts a GitHub Secret to hold the private key. The secure -upgrade keeps the key non-extractable in a KMS/HSM and signs via its API. The -workflow already requests `id-token: write` for OIDC federation. The loader -never changes — it only sees signed bytes, never the key. +upgrade keeps the key non-extractable in a KMS/HSM (e.g. Azure Key Vault) and +signs via its API. The workflow already requests `id-token: write` for OIDC +federation. The loader never changes — it only sees signed bytes, never the +key. diff --git a/apps/widget-cdn/package.json b/apps/widget-cdn/package.json index 5c59419c37..e8ee82ee56 100644 --- a/apps/widget-cdn/package.json +++ b/apps/widget-cdn/package.json @@ -7,11 +7,10 @@ "dev": "rspack serve --config rspack.config.mjs", "build": "node scripts/prepare-build.mjs && rspack build --config rspack.config.mjs --mode production && node scripts/build-manifest.mjs && node scripts/check-bundle-budget.mjs", "verify-manifest": "node scripts/verify-manifest.mjs", - "deploy:r2": "node scripts/deploy-r2.mjs", - "rollback:r2": "node scripts/rollback-r2.mjs", - "test": "node --test scripts/*.test.mjs", - "worker:dev": "wrangler dev --config worker/wrangler.toml", - "worker:deploy": "wrangler deploy --config worker/wrangler.toml", + "deploy:azure": "node scripts/deploy-azure.mjs", + "rollback:azure": "node scripts/rollback-azure.mjs", + "cleanup:azure": "node scripts/cleanup-azure.mjs", + "test": "pnpm --filter @layerswap/widget-types build && node --test scripts/*.test.mjs", "start": "node serve.mjs", "check:bundle-budget": "node scripts/check-bundle-budget.mjs" }, @@ -41,8 +40,6 @@ "zustand": "catalog:" }, "devDependencies": { - "@aws-sdk/client-s3": "^3.700.0", - "@cloudflare/workers-types": "^4.20241127.0", "@module-federation/enhanced": "^0.9.0", "@rspack/cli": "^1.1.5", "@rspack/core": "^1.1.5", @@ -51,7 +48,6 @@ "css-loader": "^7.1.2", "mini-css-extract-plugin": "^2.9.2", "style-loader": "^4.0.0", - "typescript": "^5.9.2", - "wrangler": "^4.0.0" + "typescript": "^5.9.2" } } diff --git a/apps/widget-cdn/rspack.config.mjs b/apps/widget-cdn/rspack.config.mjs index 1d851fa39b..32673bc9f3 100644 --- a/apps/widget-cdn/rspack.config.mjs +++ b/apps/widget-cdn/rspack.config.mjs @@ -5,6 +5,7 @@ import rspack from '@rspack/core'; import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; import { resolveBuildIdentity } from './scripts/build-id.mjs'; import { ASSET_BASE, CHUNK_HASH_LENGTH } from './scripts/cdn-layout.mjs'; +import { WIDGET_PROTOCOL_MAJOR } from '@layerswap/widget-types'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); @@ -43,12 +44,12 @@ const SHARED_SINGLETONS = { // Every build publishes its control files to an IMMUTABLE, buildId-named // directory (`dist/1.7.0-abc123def456/`). Content-hashed chunks are uploaded // separately under the stable `/assets/` namespace so byte-identical chunks -// keep the same URL across builds. The rolling major channel (`/v1/`) is a 302 -// redirect served by the Cloudflare Worker (see `worker/`). +// keep the same URL across builds. The rolling major channel (`/v1/`) is the +// signed manifest copied to `//manifest.json` at promotion. // // The buildId (widget version + git sha — see `scripts/build-id.mjs`) is // resolved identically by `build-manifest.mjs`, `verify-manifest.mjs`, and -// `deploy-r2.mjs`, so all four agree on the output directory. +// `deploy-azure.mjs`, so all four agree on the output directory. const { buildId: BUILD_ID } = resolveBuildIdentity(__dirname); // Dev-only: emit a minimal `manifest.json` next to `remoteEntry.js` so the @@ -66,6 +67,7 @@ const devManifestPlugin = { }, () => { const manifest = { + protocolMajor: WIDGET_PROTOCOL_MAJOR, version: '0.0.0-dev', remoteEntry: './remoteEntry.js', chunks: {}, @@ -89,8 +91,8 @@ export default (env, argv) => { devtool: isProd ? 'source-map' : 'eval-cheap-module-source-map', entry: {}, // Pure remote — no app entry. output: { - // Production: dist//* — deploy-r2.mjs keeps control files under - // that prefix and publishes content-hashed files under /assets/. + // Production: dist//* — deploy-azure.mjs keeps control files + // under that prefix and publishes content-hashed files under /assets/. // Dev: keep dist/ flat (the dev-server serves whatever publicPath says). path: path.resolve(__dirname, isProd ? `dist/${BUILD_ID}` : 'dist'), publicPath: 'auto', @@ -201,6 +203,12 @@ export default (env, argv) => { // Imperative mount entry for framework-agnostic hosts. Owns its own // React root so non-React pages can embed it without a host framework. './mount': './src/mount.tsx', + // Deposit widget: fixed-destination funding flow. Same pairing — + // React hosts consume the component, vanilla hosts the mount. + // Additive expose: older loaders never request it, so this rolls + // within protocol v1. + './DepositWidget': './src/DepositWidget.tsx', + './mountDeposit': './src/mountDeposit.tsx', }, shared: SHARED_SINGLETONS, // Disable MF's dev-only live-reload bridge. With the remote consumed diff --git a/apps/widget-cdn/scripts/azure-lib.mjs b/apps/widget-cdn/scripts/azure-lib.mjs new file mode 100644 index 0000000000..a86d29e3b2 --- /dev/null +++ b/apps/widget-cdn/scripts/azure-lib.mjs @@ -0,0 +1,201 @@ +// Shared Azure Blob Storage helpers for deploy + rollback. +// +// Authentication uses a short-lived Microsoft Entra token from Azure CLI: +// - locally: run `az login` +// - GitHub Actions: run `azure/login` with OIDC first +// +// Required environment: +// AZURE_STORAGE_ACCOUNT +// AZURE_STORAGE_CONTAINER (default: widget-cdn) +// +// AZURE_STORAGE_BLOB_ENDPOINT is optional and supports sovereign clouds or a +// custom Blob endpoint. No account key or connection string is required. + +import { execFileSync } from "node:child_process"; + +const CONTENT_TYPES = { + js: "text/javascript; charset=utf-8", + mjs: "text/javascript; charset=utf-8", + css: "text/css; charset=utf-8", + json: "application/json; charset=utf-8", + map: "application/json; charset=utf-8", +}; + +export function contentTypeFor(name) { + const ext = name.split(".").pop()?.toLowerCase() ?? ""; + return CONTENT_TYPES[ext] ?? "application/octet-stream"; +} + +function requireEnv(env, name) { + const value = env[name]; + if (!value) throw new Error(`[azure] missing required env var ${name}`); + return value; +} + +export function makeClient(env = process.env) { + const account = requireEnv(env, "AZURE_STORAGE_ACCOUNT"); + const containerName = env.AZURE_STORAGE_CONTAINER || "widget-cdn"; + const endpoint = + env.AZURE_STORAGE_BLOB_ENDPOINT || + `https://${account}.blob.core.windows.net`; + let accessToken = env.AZURE_STORAGE_ACCESS_TOKEN; + if (!accessToken) { + try { + accessToken = execFileSync( + "az", + [ + "account", + "get-access-token", + "--resource", + env.AZURE_STORAGE_TOKEN_RESOURCE || "https://storage.azure.com/", + "--query", + "accessToken", + "--output", + "tsv", + ], + { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ).trim(); + } catch (err) { + const detail = err?.stderr?.toString().trim(); + throw new Error( + "[azure] could not obtain an Azure CLI access token. Run `az login` first" + + `${detail ? `: ${detail}` : "."}`, + ); + } + } + if (!accessToken) + throw new Error("[azure] Azure CLI returned an empty access token"); + return { + account, + containerName, + endpoint: endpoint.replace(/\/+$/, ""), + accessToken, + }; +} + +function objectUrl(ctx, key) { + const container = encodeURIComponent(ctx.containerName); + const encodedKey = key.split("/").map(encodeURIComponent).join("/"); + return `${ctx.endpoint}/${container}/${encodedKey}`; +} + +async function authorizedFetch(ctx, url, init = {}) { + const headers = new Headers(init.headers); + headers.set("Authorization", `Bearer ${ctx.accessToken}`); + headers.set("x-ms-date", new Date().toUTCString()); + headers.set("x-ms-version", "2023-11-03"); + return fetch(url, { ...init, headers }); +} + +async function blobRequest(ctx, key, init = {}) { + return authorizedFetch(ctx, objectUrl(ctx, key), init); +} + +async function throwResponseError(operation, key, response) { + const message = (await response.text()).trim(); + throw new Error( + `[azure] ${operation} ${key} failed with ${response.status} ${response.statusText}` + + `${message ? `: ${message}` : ""}`, + ); +} + +// The List Blobs response is XML; only and are needed, +// so a scoped regex parse avoids pulling in an XML dependency. +function decodeXml(text) { + return text + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, "&"); +} + +export async function listObjects(ctx, { prefix } = {}) { + const results = []; + let marker; + do { + const params = new URLSearchParams({ restype: "container", comp: "list" }); + if (prefix) params.set("prefix", prefix); + if (marker) params.set("marker", marker); + const url = `${ctx.endpoint}/${encodeURIComponent(ctx.containerName)}?${params}`; + const response = await authorizedFetch(ctx, url); + if (!response.ok) { + await throwResponseError("LIST", prefix ?? "(container)", response); + } + const xml = await response.text(); + for (const blob of xml.match(/[\s\S]*?<\/Blob>/g) ?? []) { + const name = blob.match(/([\s\S]*?)<\/Name>/)?.[1]; + if (!name) continue; + const lastModified = blob.match( + /([\s\S]*?)<\/Last-Modified>/, + )?.[1]; + results.push({ + key: decodeXml(name), + lastModified: lastModified ? new Date(lastModified) : undefined, + }); + } + const nextMarker = xml.match(/([\s\S]*?)<\/NextMarker>/)?.[1]; + marker = nextMarker ? decodeXml(nextMarker) : undefined; + } while (marker); + return results; +} + +export async function deleteObject(ctx, key) { + const response = await blobRequest(ctx, key, { method: "DELETE" }); + // Idempotent: an already-deleted blob is not an error for cleanup. + if (response.status === 404) return false; + if (!response.ok) await throwResponseError("DELETE", key, response); + return true; +} + +export async function objectExists(ctx, key) { + const response = await blobRequest(ctx, key, { method: "HEAD" }); + if (response.status === 404) return false; + if (!response.ok) await throwResponseError("HEAD", key, response); + return true; +} + +export async function putObject( + ctx, + key, + body, + { contentType, cacheControl } = {}, +) { + const headers = { + "Content-Type": "application/octet-stream", + "x-ms-blob-type": "BlockBlob", + "x-ms-blob-content-type": contentType ?? contentTypeFor(key), + }; + if (cacheControl) headers["x-ms-blob-cache-control"] = cacheControl; + const response = await blobRequest(ctx, key, { + method: "PUT", + headers, + body, + }); + if (!response.ok) await throwResponseError("PUT", key, response); +} + +export async function readJsonObject(ctx, key) { + const response = await blobRequest(ctx, key); + if (response.status === 404) return undefined; + if (!response.ok) await throwResponseError("GET", key, response); + const value = await response.json(); + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`[azure] ${key} does not contain a JSON object`); + } + return value; +} + +export async function writeChannelManifest(ctx, channel, manifest) { + await putObject( + ctx, + `${channel}/manifest.json`, + Buffer.from(JSON.stringify(manifest, null, 2)), + { + contentType: "application/json; charset=utf-8", + // This is the only mutable public object. New page loads must observe a + // promotion immediately; immutable build files carry the long cache. + cacheControl: "no-store, max-age=0", + }, + ); +} diff --git a/apps/widget-cdn/scripts/azure-release-scripts.test.mjs b/apps/widget-cdn/scripts/azure-release-scripts.test.mjs new file mode 100644 index 0000000000..b5dfe17847 --- /dev/null +++ b/apps/widget-cdn/scripts/azure-release-scripts.test.mjs @@ -0,0 +1,270 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { putObject, writeChannelManifest } from "./azure-lib.mjs"; +import { deployAzureBuild } from "./deploy-azure.mjs"; +import { rollbackAzureChannel } from "./rollback-azure.mjs"; +import { + ASSET_BASE, + deploymentKey, + remoteEntryForBuild, +} from "./cdn-layout.mjs"; + +const silentLogger = { log() {}, warn() {} }; + +test("content-hashed chunks use the shared asset namespace", () => { + assert.equal( + deploymentKey("1.7.0-0123456789ab", "837.0123456789abcdef.js"), + "assets/837.0123456789abcdef.js", + ); + assert.equal( + deploymentKey("1.7.0-0123456789ab", "remoteEntry.js"), + "1.7.0-0123456789ab/remoteEntry.js", + ); +}); + +test("Azure Blob PUT uses Entra auth, encoded keys, and blob HTTP properties", async (t) => { + const originalFetch = globalThis.fetch; + t.after(() => { + globalThis.fetch = originalFetch; + }); + let request; + globalThis.fetch = async (url, init) => { + request = { url, init }; + return new Response(null, { status: 201 }); + }; + + await putObject( + { + endpoint: "https://test.blob.core.windows.net", + containerName: "widget-cdn", + accessToken: "short-lived-token", + }, + "build id/remoteEntry.js", + Buffer.from("remote"), + { + contentType: "text/javascript; charset=utf-8", + cacheControl: "public, max-age=31536000, immutable", + }, + ); + + assert.equal( + request.url, + "https://test.blob.core.windows.net/widget-cdn/build%20id/remoteEntry.js", + ); + const headers = new Headers(request.init.headers); + assert.equal(headers.get("authorization"), "Bearer short-lived-token"); + assert.equal(headers.get("x-ms-blob-type"), "BlockBlob"); + assert.equal( + headers.get("x-ms-blob-content-type"), + "text/javascript; charset=utf-8", + ); + assert.equal( + headers.get("x-ms-blob-cache-control"), + "public, max-age=31536000, immutable", + ); +}); + +test("Azure channel manifests disable browser and edge caching", async (t) => { + const originalFetch = globalThis.fetch; + t.after(() => { + globalThis.fetch = originalFetch; + }); + let request; + globalThis.fetch = async (url, init) => { + request = { url, init }; + return new Response(null, { status: 201 }); + }; + + await writeChannelManifest( + { + endpoint: "https://test.blob.core.windows.net", + containerName: "widget-cdn", + accessToken: "short-lived-token", + }, + "v1", + { protocolMajor: 1, buildId: "1.7.0-0123456789ab" }, + ); + + assert.match(request.url, /\/v1\/manifest\.json$/); + const headers = new Headers(request.init.headers); + assert.equal(headers.get("x-ms-blob-cache-control"), "no-store, max-age=0"); +}); + +function deployFixture(t, identity, manifestOverrides = {}) { + const root = mkdtempSync(join(tmpdir(), "layerswap-deploy-azure-")); + const dist = join(root, "dist", identity.buildId); + mkdirSync(dist, { recursive: true }); + writeFileSync( + join(dist, "manifest.json"), + JSON.stringify({ + ...identity, + protocolMajor: 1, + remoteEntry: remoteEntryForBuild(identity.buildId), + assetBase: ASSET_BASE, + chunks: {}, + signature: "signed", + ...manifestOverrides, + }), + ); + t.after(() => rmSync(root, { recursive: true, force: true })); + return root; +} + +test("Azure deploy rejects a channel-relative remote before creating a client", async (t) => { + const identity = { + buildId: "1.7.0-0123456789ab", + version: "1.7.0", + channel: "v1", + gitSha: "0123456789abcdef", + }; + const root = deployFixture(t, identity, { remoteEntry: "./remoteEntry.js" }); + mkdirSync(join(root, "dist", "assets")); + let clientsCreated = 0; + + await assert.rejects( + deployAzureBuild({ + root, + identity, + logger: silentLogger, + createClient() { + clientsCreated += 1; + return {}; + }, + }), + /remoteEntry/, + ); + assert.equal(clientsCreated, 0); +}); + +test("Azure deploy reuses shared assets and promotes the signed manifest", async (t) => { + const identity = { + buildId: "1.7.0-0123456789ab", + version: "1.7.0", + channel: "v1", + gitSha: "0123456789abcdef", + }; + const root = deployFixture(t, identity); + const buildDir = join(root, "dist", identity.buildId); + const assetDir = join(root, "dist", "assets"); + const assetName = "837.0123456789abcdef.js"; + mkdirSync(assetDir); + writeFileSync(join(buildDir, "remoteEntry.js"), "remote"); + writeFileSync(join(assetDir, assetName), "chunk"); + + const uploadedKeys = []; + const promotions = []; + const result = await deployAzureBuild({ + root, + identity, + logger: silentLogger, + createClient: () => ({ account: "test", containerName: "widget-cdn" }), + objectExists: async (_ctx, key) => key === `assets/${assetName}`, + putObject: async (_ctx, key) => uploadedKeys.push(key), + readJsonObject: async () => ({ buildId: "1.6.0-aaaaaaaaaaaa" }), + writeChannelManifest: async (_ctx, channel, manifest) => { + promotions.push({ channel, buildId: manifest.buildId }); + }, + }); + + assert.deepEqual(result, { uploaded: 2, reused: 1, promoted: true }); + assert.deepEqual(uploadedKeys, [ + `${identity.buildId}/remoteEntry.js`, + `${identity.buildId}/manifest.json`, + ]); + assert.deepEqual(promotions, [{ channel: "v1", buildId: identity.buildId }]); +}); + +test("Azure deploy publishes the immutable manifest after all payloads", async (t) => { + const identity = { + buildId: "1.7.0-0123456789ab", + version: "1.7.0", + channel: "v1", + gitSha: "0123456789abcdef", + }; + const root = deployFixture(t, identity); + const buildDir = join(root, "dist", identity.buildId); + const assetDir = join(root, "dist", "assets"); + mkdirSync(assetDir); + writeFileSync(join(buildDir, "remoteEntry.js"), "remote"); + writeFileSync(join(assetDir, "one.0123456789abcdef.js"), "one"); + writeFileSync(join(assetDir, "two.fedcba9876543210.js"), "two"); + + let activePayloadUploads = 0; + const completedKeys = []; + await deployAzureBuild({ + root, + identity, + env: { LAYERSWAP_PROMOTE: "false" }, + logger: silentLogger, + uploadConcurrency: 2, + createClient: () => ({ account: "test", containerName: "widget-cdn" }), + objectExists: async () => false, + putObject: async (_ctx, key) => { + if (key.endsWith("/manifest.json")) { + assert.equal(activePayloadUploads, 0); + } else { + activePayloadUploads += 1; + await new Promise((resolve) => setTimeout(resolve, 10)); + activePayloadUploads -= 1; + } + completedKeys.push(key); + }, + }); + + assert.equal(completedKeys.at(-1), `${identity.buildId}/manifest.json`); +}); + +test("Azure rollback promotes a valid immutable manifest", async () => { + const buildId = "1.7.0-0123456789ab"; + const writes = []; + const result = await rollbackAzureChannel({ + channel: "v1", + buildId, + ctx: {}, + logger: silentLogger, + readManifest: async (_ctx, key) => + key.startsWith(buildId) + ? { + buildId, + channel: "v1", + remoteEntry: remoteEntryForBuild(buildId), + expiresAt: new Date( + Date.now() + 7 * 24 * 60 * 60 * 1000, + ).toISOString(), + } + : { buildId: "1.6.0-aaaaaaaaaaaa" }, + writeManifest: async (_ctx, channel, manifest) => { + writes.push({ channel, buildId: manifest.buildId }); + }, + }); + + assert.deepEqual(result, { changed: true, previous: "1.6.0-aaaaaaaaaaaa" }); + assert.deepEqual(writes, [{ channel: "v1", buildId }]); +}); + +test("Azure rollback rejects a manifest that cannot resolve from the channel path", async () => { + const buildId = "1.7.0-0123456789ab"; + let writes = 0; + await assert.rejects( + rollbackAzureChannel({ + channel: "v1", + buildId, + ctx: {}, + logger: silentLogger, + readManifest: async () => ({ + buildId, + channel: "v1", + protocolMajor: 1, + remoteEntry: "./remoteEntry.js", + }), + writeManifest: async () => { + writes += 1; + }, + }), + /valid Azure manifest/, + ); + assert.equal(writes, 0); +}); diff --git a/apps/widget-cdn/scripts/build-id.mjs b/apps/widget-cdn/scripts/build-id.mjs index 43941f9e2a..1855625a23 100644 --- a/apps/widget-cdn/scripts/build-id.mjs +++ b/apps/widget-cdn/scripts/build-id.mjs @@ -1,29 +1,30 @@ // Single source of truth for the build's identity, shared by rspack.config.mjs, -// build-manifest.mjs, verify-manifest.mjs, and deploy-r2.mjs so the output -// directory, R2 prefix, and manifest fields can never disagree. +// build-manifest.mjs, verify-manifest.mjs, and deploy-azure.mjs so the output +// directory, storage prefix, and manifest fields can never disagree. // // Two distinct concepts, deliberately kept separate: // // - `buildId` — the IMMUTABLE identity of one build: `-` // (or `LAYERSWAP_RELEASE_ID` verbatim). Names the dist directory and the -// write-once R2 prefix. The deploy workflow also triggers on changes to -// widget-cdn/wallets/widget-js/widget-react, all of which change the +// write-once storage prefix. The deploy workflow also triggers on changes +// to widget-cdn/wallets/widget-js/widget-react, all of which change the // deployed bytes WITHOUT bumping `@layerswap/widget` — keying immutability // on the version alone made every such deploy collide with the previously // published prefix. -// - `channel` — the rolling COMPATIBILITY pointer (`v` of the widget -// version). The Worker 302-redirects `/v1/*` to whatever buildId -// `channels.json` currently maps it to. +// - `channel` — the rolling COMPATIBILITY pointer (`v`). +// Promotion copies the signed manifest of the current buildId to +// `//manifest.json`. // -// `version` stays the `@layerswap/widget` version — the host-facing -// compatibility number stamped into the manifest. +// `version` stays the `@layerswap/widget` implementation version stamped into +// the manifest. It does not select a CDN channel. import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +import { WIDGET_PROTOCOL_MAJOR } from '@layerswap/widget-types'; const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; // `assets` is the shared content-addressed namespace and cannot also name a -// build directory. Rolling channel names are reserved for Worker redirects. +// build directory. Rolling channel names are reserved for channel pointers. export const BUILD_ID_RE = /^(?!v\d+$)(?!assets$)[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/; export function isValidBuildId(value) { @@ -41,7 +42,7 @@ export function resolveBuildIdentity(root) { if (!VERSION_RE.test(version)) { throw new Error(`[build-id] invalid release version: ${JSON.stringify(version)}`); } - const channel = `v${version.split('.')[0]}`; + const channel = `v${WIDGET_PROTOCOL_MAJOR}`; const gitSha = process.env.LAYERSWAP_GIT_SHA || process.env.GITHUB_SHA || 'local'; const buildId = process.env.LAYERSWAP_RELEASE_ID || `${version}-${gitSha.slice(0, 12)}`; if (!isValidBuildId(buildId)) { diff --git a/apps/widget-cdn/scripts/build-id.test.mjs b/apps/widget-cdn/scripts/build-id.test.mjs index 8431d83bd3..599651416c 100644 --- a/apps/widget-cdn/scripts/build-id.test.mjs +++ b/apps/widget-cdn/scripts/build-id.test.mjs @@ -72,7 +72,7 @@ test('rejects malformed release versions', (t) => { } }); -test('accepts semver prerelease and build metadata', (t) => { +test('core semver does not select the compatibility channel', (t) => { const root = fixtureRoot(t); const identity = withEnv({ LAYERSWAP_RELEASE_VERSION: '2.0.0-beta.1+build.2', @@ -80,6 +80,6 @@ test('accepts semver prerelease and build metadata', (t) => { LAYERSWAP_GIT_SHA: 'abcdef0123456789', }, () => resolveBuildIdentity(root)); - assert.equal(identity.channel, 'v2'); + assert.equal(identity.channel, 'v1'); assert.equal(identity.buildId, '2.0.0-beta.1+build.2-abcdef012345'); }); diff --git a/apps/widget-cdn/scripts/build-manifest.mjs b/apps/widget-cdn/scripts/build-manifest.mjs index b50f20c590..ff6873191a 100644 --- a/apps/widget-cdn/scripts/build-manifest.mjs +++ b/apps/widget-cdn/scripts/build-manifest.mjs @@ -17,8 +17,9 @@ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createSign, createPrivateKey, createHash } from 'node:crypto'; +import { WIDGET_PROTOCOL_MAJOR } from '@layerswap/widget-types'; import { resolveBuildIdentity } from './build-id.mjs'; -import { ASSET_BASE, ASSET_DIRECTORY } from './cdn-layout.mjs'; +import { ASSET_BASE, ASSET_DIRECTORY, remoteEntryForBuild } from './cdn-layout.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..'); @@ -54,10 +55,11 @@ if (!existsSync(ASSET_DIST)) { process.exit(1); } -// remoteEntry.js sits at the build-directory root by Rspack config. Kept -// origin-relative so the same signed manifest works whether served at its -// version path or reached via the rolling-channel redirect. -const remoteEntry = './remoteEntry.js'; +// This resolves to the immutable build prefix from BOTH publication paths: +// //manifest.json (immutable) and //manifest.json (rolling). +// Promotion copies this signed manifest directly to the rolling path, and the +// relative remoteEntry anchors at the immutable build either way. +const remoteEntry = remoteEntryForBuild(buildId); // Hash every JS file in the build directory and record under the // filename. The browser will use these via SRI when MF loads the scripts. @@ -107,6 +109,7 @@ function canonicalJSON(value) { const chunks = collectChunks([DIST, ASSET_DIST]); const manifest = { + protocolMajor: WIDGET_PROTOCOL_MAJOR, version, channel, buildId, diff --git a/apps/widget-cdn/scripts/cdn-layout.mjs b/apps/widget-cdn/scripts/cdn-layout.mjs index 1249dc8051..b8630efe51 100644 --- a/apps/widget-cdn/scripts/cdn-layout.mjs +++ b/apps/widget-cdn/scripts/cdn-layout.mjs @@ -22,3 +22,16 @@ export function deploymentKey(buildId, relativePath) { ? `${ASSET_DIRECTORY}/${relativePath}` : `${buildId}/${relativePath}`; } + +/** + * A signed production manifest is published twice on Azure: + * - //manifest.json (immutable) + * - //manifest.json (rolling pointer) + * + * This relative URL resolves to the same immutable remote from either + * location, so promotion can atomically copy the already-signed manifest + * bytes without an edge redirect or a second signature. + */ +export function remoteEntryForBuild(buildId) { + return `../${buildId}/remoteEntry.js`; +} diff --git a/apps/widget-cdn/scripts/check-bundle-budget.mjs b/apps/widget-cdn/scripts/check-bundle-budget.mjs index c5a7fd6cc2..f5ffeb4927 100644 --- a/apps/widget-cdn/scripts/check-bundle-budget.mjs +++ b/apps/widget-cdn/scripts/check-bundle-budget.mjs @@ -4,6 +4,13 @@ // or QR/canvas library slipping back into the root import graph) landed us at // 842 KB gzip once; this guard keeps the fix from silently eroding. // +// The budget applies PER EXPOSE, not to the union of all exposes: a page +// mounts exactly one widget (`./Widget`/`./mount` or `./DepositWidget`/ +// `./mountDeposit`), so the bytes a host actually downloads before first +// paint are one expose's sync set. Their graphs overlap almost entirely; +// summing the union would count each sibling widget's private chunk against +// every other widget's budget. +// // Budget override (bytes): WIDGET_SYNC_GZIP_BUDGET env var. Raise it only for // a deliberate, reviewed increase — not to make a red build green. import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; @@ -31,32 +38,42 @@ const statsPath = statsFiles[0]; const stats = JSON.parse(readFileSync(statsPath, 'utf8')); const buildDir = dirname(statsPath); -const syncAssets = new Set(); -for (const expose of stats.exposes ?? []) { - for (const asset of expose.assets?.js?.sync ?? []) syncAssets.add(asset); -} -if (syncAssets.size === 0) { +const exposes = (stats.exposes ?? []).filter((e) => (e.assets?.js?.sync ?? []).length > 0); +if (exposes.length === 0) { console.error(`[bundle-budget] ${statsPath} lists no synchronous expose assets — stats format changed?`); process.exit(1); } -let totalGzip = 0; -const rows = []; -for (const asset of syncAssets) { - const assetPath = normalize(join(buildDir, asset)); - const gz = gzipSync(readFileSync(assetPath), { level: 6 }).length; - totalGzip += gz; - rows.push({ asset, gz }); -} -rows.sort((a, b) => b.gz - a.gz); +// gzip each distinct asset once — the exposes' sync sets overlap heavily. +const gzCache = new Map(); +const gzipOf = (asset) => { + if (!gzCache.has(asset)) { + const assetPath = normalize(join(buildDir, asset)); + gzCache.set(asset, gzipSync(readFileSync(assetPath), { level: 6 }).length); + } + return gzCache.get(asset); +}; const kib = (n) => `${(n / 1024).toFixed(1)} KiB`; -for (const { asset, gz } of rows) console.log(`[bundle-budget] ${kib(gz).padStart(10)} ${asset}`); -console.log(`[bundle-budget] ${syncAssets.size} sync assets, ${kib(totalGzip)} gzip total (budget ${kib(budget)})`); +const perExpose = exposes.map((expose) => { + const assets = [...new Set(expose.assets.js.sync)]; + const rows = assets + .map((asset) => ({ asset, gz: gzipOf(asset) })) + .sort((a, b) => b.gz - a.gz); + return { name: expose.path ?? expose.name, rows, totalGzip: rows.reduce((sum, r) => sum + r.gz, 0) }; +}).sort((a, b) => b.totalGzip - a.totalGzip); + +// Detail the heaviest expose; one summary line for each of the rest. +const heaviest = perExpose[0]; +for (const { asset, gz } of heaviest.rows) console.log(`[bundle-budget] ${kib(gz).padStart(10)} ${asset}`); +for (const { name, rows, totalGzip } of perExpose) { + console.log(`[bundle-budget] ${name}: ${rows.length} sync assets, ${kib(totalGzip)} gzip (budget ${kib(budget)})`); +} -if (totalGzip > budget) { +const over = perExpose.filter((e) => e.totalGzip > budget); +if (over.length > 0) { console.error( - `[bundle-budget] FAIL: synchronous JS is ${kib(totalGzip)} gzip, over the ${kib(budget)} budget. ` + `[bundle-budget] FAIL: ${over.map((e) => `${e.name} is ${kib(e.totalGzip)} gzip`).join(', ')}, over the ${kib(budget)} per-expose budget. ` + 'Find what joined the sync graph (compare mf-stats.json sync lists against the previous build) and lazy-load it; ' + 'raise WIDGET_SYNC_GZIP_BUDGET only for a deliberate, reviewed increase.', ); diff --git a/apps/widget-cdn/scripts/cleanup-azure.mjs b/apps/widget-cdn/scripts/cleanup-azure.mjs new file mode 100644 index 0000000000..6eadb8e3e0 --- /dev/null +++ b/apps/widget-cdn/scripts/cleanup-azure.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Reference-aware cleanup of old widget builds in Azure Blob Storage. +// +// node scripts/cleanup-azure.mjs # dry run (report only) +// node scripts/cleanup-azure.mjs --plan-out plan.json # dry run + reviewable plan file +// node scripts/cleanup-azure.mjs --apply # delete everything deletable +// node scripts/cleanup-azure.mjs --apply --plan-in plan.json # delete only the approved plan +// node scripts/cleanup-azure.mjs --keep-recent 5 --asset-grace-days 2 --build-grace-days 2 +// +// Retention rules live in cleanup-lib.mjs. Requires `az login` (or the CI +// OIDC login) and AZURE_STORAGE_ACCOUNT / AZURE_STORAGE_CONTAINER. + +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { + deleteObject, + listObjects, + makeClient, + readJsonObject, +} from "./azure-lib.mjs"; +import { isValidBuildId } from "./build-id.mjs"; +import { runCleanupCli } from "./cleanup-lib.mjs"; + +const CHANNEL_MANIFEST_RE = /^v\d+\/manifest\.json$/; + +// Azure has no channels.json — each rolling pointer is the signed manifest +// copied to //manifest.json. Its buildId IS the pointer. +export async function readAzureChannelPointers( + ctx, + { list = listObjects, readJson = readJsonObject } = {}, +) { + const pointers = {}; + for (const { key } of await list(ctx, { prefix: "v" })) { + if (!CHANNEL_MANIFEST_RE.test(key)) continue; + const manifest = await readJson(ctx, key); + if (manifest && isValidBuildId(manifest.buildId)) { + pointers[key.split("/")[0]] = manifest.buildId; + } + } + return pointers; +} + +export const azureCleanupOps = { + listObjects, + readJson: readJsonObject, + readChannelPointers: readAzureChannelPointers, + deleteObject, +}; + +const entryUrl = process.argv[1] + ? pathToFileURL(resolve(process.argv[1])).href + : undefined; +if (entryUrl === import.meta.url) { + try { + await runCleanupCli({ + argv: process.argv.slice(2), + ctx: makeClient(), + ops: azureCleanupOps, + }); + } catch (err) { + console.error(err instanceof Error ? err.message : err); + process.exitCode = 1; + } +} diff --git a/apps/widget-cdn/scripts/cleanup-lib.mjs b/apps/widget-cdn/scripts/cleanup-lib.mjs new file mode 100644 index 0000000000..0c77c6adcd --- /dev/null +++ b/apps/widget-cdn/scripts/cleanup-lib.mjs @@ -0,0 +1,458 @@ +// Reference-aware cleanup for the widget CDN storage in Azure Blob Storage +// (immutable // prefixes, shared content-hashed /assets/, and the +// mutable rolling //manifest.json pointer). +// +// Deletion policy — a build prefix is RETAINED when any of these hold: +// - it is the current target of any rolling channel pointer; +// - its signed manifest has not expired (unexpired builds are valid rollback +// targets, and in-flight browser sessions may still lazy-load their +// chunks); +// - it is one of the N most recent builds of its channel (rollback depth +// even when everything has expired); +// - it has no readable manifest but was uploaded recently (a deploy in +// flight publishes the manifest LAST — the grace period keeps cleanup +// from racing it). +// Everything else is deleted, manifest.json first, so a half-deleted build +// can never be promoted by rollback scripts (they require the manifest). +// +// Shared assets are mark-and-swept: the union of `chunks` referenced by every +// RETAINED manifest is protected; unreferenced assets are deleted only after +// their own grace period (again to avoid racing an in-flight deploy that +// uploads assets before its manifest). NEVER apply an age-only policy to +// /assets/ — chunks are shared between builds. +// +// Anything anomalous (unknown top-level keys, a manifest whose buildId does +// not match its prefix, a channel pointing at a missing build) is kept and +// surfaced as a warning — cleanup only deletes what it fully understands. + +import { readFileSync, writeFileSync } from 'node:fs'; +import { isValidBuildId } from './build-id.mjs'; +import { ASSET_DIRECTORY, isSharedAsset } from './cdn-layout.mjs'; + +const DAY_MS = 24 * 60 * 60 * 1000; +// Must cover the loader's clock-skew allowance (5 min) with margin. +const EXPIRY_SKEW_MS = 60 * 60 * 1000; +const CHANNEL_EXPIRY_WARN_MS = 7 * DAY_MS; +const CHANNEL_RE = /^v\d+$/; + +export const DEFAULT_MIN_RECENT_PER_CHANNEL = 3; +export const DEFAULT_ASSET_GRACE_MS = 2 * DAY_MS; +export const DEFAULT_ORPHAN_GRACE_MS = 2 * DAY_MS; + +function toMs(value) { + if (value == null) return undefined; + const ms = value instanceof Date ? value.getTime() : Date.parse(value); + return Number.isNaN(ms) ? undefined : ms; +} + +/** + * Split a full object listing into build prefixes, shared assets, rolling + * channel objects, and everything else. Channel objects and unknown keys are + * never deletion candidates. + */ +export function partitionObjects(objects) { + const builds = new Map(); + const assets = []; + const channelObjects = []; + const unknown = []; + for (const object of objects) { + const segments = object.key.split('/'); + const [first, ...rest] = segments; + if (CHANNEL_RE.test(first)) { + channelObjects.push(object); + continue; + } + if (first === ASSET_DIRECTORY) { + if (rest.length === 1 && isSharedAsset(rest[0])) { + assets.push({ ...object, name: rest[0] }); + } else { + unknown.push(object); + } + continue; + } + if (rest.length > 0 && isValidBuildId(first)) { + if (!builds.has(first)) builds.set(first, []); + builds.get(first).push(object); + continue; + } + unknown.push(object); + } + return { builds, assets, channelObjects, unknown }; +} + +/** + * Pure retention planner. Inputs: + * now epoch ms + * channels { v1: '', … } — current rolling pointers + * builds [{ buildId, keys: [{ key, lastModified }], manifest|null }] + * assets [{ key, name, lastModified }] + */ +export function planCleanup({ + now, + channels, + builds, + assets, + minRecentPerChannel = DEFAULT_MIN_RECENT_PER_CHANNEL, + assetGraceMs = DEFAULT_ASSET_GRACE_MS, + orphanGraceMs = DEFAULT_ORPHAN_GRACE_MS, +}) { + if (!Number.isFinite(now)) throw new Error('[cleanup] planCleanup requires `now`'); + const warnings = []; + + const promotedChannels = new Map(); + for (const [channel, buildId] of Object.entries(channels)) { + if (!builds.some((b) => b.buildId === buildId)) { + warnings.push( + `channel ${channel} points at ${buildId}, which has no published build prefix`, + ); + } + const list = promotedChannels.get(buildId) ?? []; + list.push(channel); + promotedChannels.set(buildId, list); + } + + const entries = builds.map((build) => { + const newestUpload = Math.max( + 0, + ...build.keys.map((k) => toMs(k.lastModified) ?? 0), + ); + return { + ...build, + expiresMs: toMs(build.manifest?.expiresAt), + recency: toMs(build.manifest?.issuedAt) ?? newestUpload, + newestUpload, + reasons: [], + }; + }); + + // N most recent builds per channel survive even past manifest expiry. + const byChannel = new Map(); + for (const entry of entries) { + if (!entry.manifest) continue; + const channel = + typeof entry.manifest.channel === 'string' ? entry.manifest.channel : '(unknown)'; + const list = byChannel.get(channel) ?? []; + list.push(entry); + byChannel.set(channel, list); + } + for (const list of byChannel.values()) { + list.sort((a, b) => b.recency - a.recency); + for (const entry of list.slice(0, minRecentPerChannel)) { + entry.reasons.push('recent'); + } + } + + for (const entry of entries) { + const liveChannels = promotedChannels.get(entry.buildId); + if (liveChannels) { + entry.reasons.push(...liveChannels.map((c) => `promoted:${c}`)); + if (!entry.manifest) { + warnings.push(`promoted build ${entry.buildId} has no readable manifest`); + } else if (entry.expiresMs === undefined) { + warnings.push( + `promoted build ${entry.buildId} has no valid expiresAt — verifying loaders reject it`, + ); + } else if (entry.expiresMs <= now) { + warnings.push( + `promoted build ${entry.buildId} EXPIRED at ${entry.manifest.expiresAt} — ` + + 'verifying loaders are failing closed; re-publish now', + ); + } else if (entry.expiresMs - now < CHANNEL_EXPIRY_WARN_MS) { + warnings.push( + `promoted build ${entry.buildId} expires ${entry.manifest.expiresAt} ` + + '(<7 days) — deploy or re-sign before the channel goes dark', + ); + } + } + + if (entry.manifest) { + if (entry.manifest.buildId !== entry.buildId) { + warnings.push( + `build prefix ${entry.buildId} contains a manifest claiming to be ` + + `${JSON.stringify(entry.manifest.buildId)} — kept, investigate manually`, + ); + entry.reasons.push('manifest-mismatch'); + } + if (entry.expiresMs !== undefined && entry.expiresMs + EXPIRY_SKEW_MS > now) { + entry.reasons.push('unexpired'); + } + } else if (now - entry.newestUpload < orphanGraceMs) { + // Possibly a deploy in flight: its manifest publishes last. + entry.reasons.push('upload-grace'); + } + } + + const keepBuilds = entries.filter((e) => e.reasons.length > 0); + const deleteBuilds = entries + .filter((e) => e.reasons.length === 0) + .map((e) => ({ + buildId: e.buildId, + keys: e.keys.map((k) => k.key), + reason: e.manifest + ? 'expired, not promoted, not recent' + : 'no manifest (incomplete upload) past grace period', + })); + + // Mark-and-sweep the shared asset namespace against retained manifests. + const referenced = new Set(); + for (const entry of keepBuilds) { + for (const name of Object.keys(entry.manifest?.chunks ?? {})) { + if (isSharedAsset(name)) referenced.add(name); + } + } + const deleteAssets = []; + let referencedAssets = 0; + let gracedAssets = 0; + for (const asset of assets) { + if (referenced.has(asset.name)) { + referencedAssets += 1; + continue; + } + const age = now - (toMs(asset.lastModified) ?? now); + if (age < assetGraceMs) { + gracedAssets += 1; + continue; + } + deleteAssets.push(asset.key); + } + + return { + keepBuilds: keepBuilds.map((e) => ({ buildId: e.buildId, reasons: e.reasons })), + deleteBuilds, + deleteAssets, + referencedAssets, + gracedAssets, + warnings, + }; +} + +async function runWithConcurrency(items, limit, task) { + if (items.length === 0) return; + const workerCount = Math.min(Math.max(1, Math.floor(limit)), items.length); + let cursor = 0; + await Promise.all( + Array.from({ length: workerCount }, async () => { + while (cursor < items.length) { + const item = items[cursor]; + cursor += 1; + await task(item); + } + }), + ); +} + +function normalizedPointers(channels) { + return JSON.stringify( + Object.fromEntries(Object.entries(channels).sort(([a], [b]) => a.localeCompare(b))), + ); +} + +/** + * Full cleanup pass over one storage backend. Dry-run unless `apply` is true. + * + * ops = { + * listObjects(ctx) -> [{ key, lastModified }] + * readJson(ctx, key) -> object | undefined + * readChannelPointers(ctx) -> { [channel]: buildId } + * deleteObject(ctx, key) + * } + * + * `approvedPlan` (a previously written plan file) restricts deletion to the + * items a human approved: the fresh plan is recomputed from live state, and + * only its intersection with the approved plan is deleted. Items that became + * deletable after approval are deferred; approved items that are no longer + * deletable stay protected. + */ +export async function runCleanup({ + ctx, + ops, + apply = false, + approvedPlan, + now = Date.now(), + logger = console, + minRecentPerChannel, + assetGraceMs, + orphanGraceMs, + deleteConcurrency = 8, +}) { + const objects = await ops.listObjects(ctx); + const { builds: buildMap, assets, unknown } = partitionObjects(objects); + const channels = await ops.readChannelPointers(ctx); + + const builds = []; + for (const [buildId, keys] of buildMap) { + const manifestKey = `${buildId}/manifest.json`; + const hasManifest = keys.some((k) => k.key === manifestKey); + const manifest = hasManifest ? await ops.readJson(ctx, manifestKey) : undefined; + builds.push({ buildId, keys, manifest: manifest ?? null }); + } + + const plan = planCleanup({ + now, + channels, + builds, + assets, + ...(minRecentPerChannel !== undefined ? { minRecentPerChannel } : {}), + ...(assetGraceMs !== undefined ? { assetGraceMs } : {}), + ...(orphanGraceMs !== undefined ? { orphanGraceMs } : {}), + }); + + if (approvedPlan) { + const approvedBuildIds = new Set( + (approvedPlan.deleteBuilds ?? []) + .map((b) => (typeof b === 'string' ? b : b?.buildId)) + .filter(Boolean), + ); + const approvedAssetKeys = new Set(approvedPlan.deleteAssets ?? []); + const freshBuilds = plan.deleteBuilds; + const freshAssets = plan.deleteAssets; + plan.deleteBuilds = freshBuilds.filter((b) => approvedBuildIds.has(b.buildId)); + plan.deleteAssets = freshAssets.filter((k) => approvedAssetKeys.has(k)); + const deferredBuilds = freshBuilds.length - plan.deleteBuilds.length; + const deferredAssets = freshAssets.length - plan.deleteAssets.length; + const staleBuilds = approvedBuildIds.size - plan.deleteBuilds.length; + const staleAssets = approvedAssetKeys.size - plan.deleteAssets.length; + if (deferredBuilds || deferredAssets) { + logger.log( + `[cleanup] approval filter: ${deferredBuilds} build(s) and ${deferredAssets} ` + + 'asset(s) became deletable after the plan was approved — deferred to a future run.', + ); + } + if (staleBuilds || staleAssets) { + logger.warn( + `[cleanup] WARNING: ${staleBuilds} approved build(s) and ${staleAssets} approved ` + + 'asset(s) are no longer deletable — they stay protected.', + ); + } + } + + logger.log( + `[cleanup] ${builds.length} build(s), ${assets.length} shared asset(s), ` + + `channels: ${normalizedPointers(channels)}`, + ); + for (const key of unknown) { + logger.warn(`[cleanup] WARNING: unrecognized key left untouched: ${key.key}`); + } + for (const warning of plan.warnings) { + logger.warn(`[cleanup] WARNING: ${warning}`); + } + for (const build of plan.keepBuilds) { + logger.log(` keep ${build.buildId} (${build.reasons.join(', ')})`); + } + for (const build of plan.deleteBuilds) { + logger.log(` delete ${build.buildId} — ${build.reason} (${build.keys.length} object(s))`); + } + logger.log( + `[cleanup] assets: ${plan.referencedAssets} referenced, ` + + `${plan.gracedAssets} in grace period, ${plan.deleteAssets.length} to delete`, + ); + + if (!apply) { + logger.log('[cleanup] dry run — nothing deleted. Re-run with --apply to delete.'); + return { ...plan, unknown: unknown.map((k) => k.key), applied: false, deletedObjects: 0 }; + } + + // The pointer set is the ground truth for what must survive. If a promote + // or rollback landed while we were planning, the plan is stale — abort. + const pointersNow = await ops.readChannelPointers(ctx); + if (normalizedPointers(pointersNow) !== normalizedPointers(channels)) { + throw new Error( + '[cleanup] channel pointers changed while planning ' + + `(${normalizedPointers(channels)} → ${normalizedPointers(pointersNow)}) — re-run.`, + ); + } + + let deletedObjects = 0; + for (const build of plan.deleteBuilds) { + const manifestKey = `${build.buildId}/manifest.json`; + // Manifest first: without it the prefix can no longer be promoted, so + // an interrupted deletion never leaves a promotable half-build behind. + const ordered = [ + ...build.keys.filter((k) => k === manifestKey), + ...build.keys.filter((k) => k !== manifestKey), + ]; + for (const key of ordered) { + await ops.deleteObject(ctx, key); + deletedObjects += 1; + logger.log(` ✕ ${key}`); + } + } + await runWithConcurrency(plan.deleteAssets, deleteConcurrency, async (key) => { + await ops.deleteObject(ctx, key); + deletedObjects += 1; + logger.log(` ✕ ${key}`); + }); + + logger.log( + `[cleanup] deleted ${plan.deleteBuilds.length} build(s) and ` + + `${plan.deleteAssets.length} asset(s) (${deletedObjects} object(s) total).`, + ); + return { ...plan, unknown: unknown.map((k) => k.key), applied: true, deletedObjects }; +} + +export function parseCleanupArgs(argv) { + const options = { apply: false }; + const requireNumber = (raw, flag) => { + const value = Number(raw); + if (!Number.isFinite(value) || value < 0) { + throw new Error(`[cleanup] ${flag} expects a non-negative number, got ${JSON.stringify(raw)}`); + } + return value; + }; + const requirePath = (raw, flag) => { + if (typeof raw !== 'string' || raw.length === 0) { + throw new Error(`[cleanup] ${flag} expects a file path`); + } + return raw; + }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--apply') options.apply = true; + else if (arg === '--keep-recent') { + options.minRecentPerChannel = requireNumber(argv[(i += 1)], arg); + } else if (arg === '--asset-grace-days') { + options.assetGraceMs = requireNumber(argv[(i += 1)], arg) * DAY_MS; + } else if (arg === '--build-grace-days') { + options.orphanGraceMs = requireNumber(argv[(i += 1)], arg) * DAY_MS; + } else if (arg === '--plan-out') { + options.planOut = requirePath(argv[(i += 1)], arg); + } else if (arg === '--plan-in') { + options.planIn = requirePath(argv[(i += 1)], arg); + } else { + throw new Error(`[cleanup] unknown argument: ${arg}`); + } + } + return options; +} + +/** + * CLI wrapper for cleanup-azure.mjs. Adds the approval round-trip: + * `--plan-out ` captures the dry-run plan for a human to review, and + * `--apply --plan-in ` deletes only what that reviewed plan approved. + */ +export async function runCleanupCli({ argv, ctx, ops, logger = console, now }) { + const { planOut, planIn, ...options } = parseCleanupArgs(argv); + if (planOut && options.apply) { + throw new Error('[cleanup] --plan-out captures a dry-run plan; drop --apply.'); + } + if (planIn && !options.apply) { + throw new Error('[cleanup] --plan-in only applies an approved plan; add --apply.'); + } + const approvedPlan = planIn ? JSON.parse(readFileSync(planIn, 'utf8')) : undefined; + const result = await runCleanup({ + ctx, + ops, + logger, + approvedPlan, + ...(now !== undefined ? { now } : {}), + ...options, + }); + if (planOut) { + writeFileSync( + planOut, + JSON.stringify({ generatedAt: new Date(now ?? Date.now()).toISOString(), ...result }, null, 2), + ); + logger.log(`[cleanup] plan written to ${planOut}`); + } + return result; +} diff --git a/apps/widget-cdn/scripts/cleanup.test.mjs b/apps/widget-cdn/scripts/cleanup.test.mjs new file mode 100644 index 0000000000..47b0dfc462 --- /dev/null +++ b/apps/widget-cdn/scripts/cleanup.test.mjs @@ -0,0 +1,434 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { listObjects } from "./azure-lib.mjs"; +import { readAzureChannelPointers } from "./cleanup-azure.mjs"; +import { + parseCleanupArgs, + partitionObjects, + planCleanup, + runCleanup, + runCleanupCli, +} from "./cleanup-lib.mjs"; + +const silentLogger = { log() {}, warn() {} }; + +const DAY_MS = 24 * 60 * 60 * 1000; +const NOW = Date.parse("2026-08-01T00:00:00Z"); + +function iso(offsetDays) { + return new Date(NOW + offsetDays * DAY_MS).toISOString(); +} + +function build(buildId, { manifest, ageDays = 10, extraKeys = [] } = {}) { + const lastModified = new Date(NOW - ageDays * DAY_MS); + const keys = [ + ...(manifest !== null + ? [{ key: `${buildId}/manifest.json`, lastModified }] + : []), + { key: `${buildId}/remoteEntry.js`, lastModified }, + ...extraKeys.map((key) => ({ key, lastModified })), + ]; + return { buildId, keys, manifest: manifest ?? null }; +} + +function manifestFor(buildId, { channel = "v1", issuedDays = -10, expiresDays = 20, chunks = {} } = {}) { + return { + buildId, + channel, + issuedAt: iso(issuedDays), + expiresAt: iso(expiresDays), + chunks, + }; +} + +test("partitionObjects separates builds, assets, channels, and unknowns", () => { + const { builds, assets, channelObjects, unknown } = partitionObjects([ + { key: "1.7.0-abc/manifest.json" }, + { key: "1.7.0-abc/remoteEntry.js" }, + { key: "assets/837.0123456789abcdef.js" }, + { key: "assets/README.txt" }, + { key: "v1/manifest.json" }, + { key: "stray-file" }, + ]); + assert.deepEqual([...builds.keys()], ["1.7.0-abc"]); + assert.equal(builds.get("1.7.0-abc").length, 2); + assert.deepEqual(assets.map((a) => a.name), ["837.0123456789abcdef.js"]); + assert.deepEqual(channelObjects.map((o) => o.key), ["v1/manifest.json"]); + assert.deepEqual(unknown.map((o) => o.key), ["assets/README.txt", "stray-file"]); +}); + +test("planCleanup keeps promoted and unexpired builds, deletes expired ones", () => { + const chunk = "837.0123456789abcdef.js"; + const promoted = build("1.7.0-promoted", { + manifest: manifestFor("1.7.0-promoted", { expiresDays: -5, chunks: { [chunk]: "sha384-x" } }), + ageDays: 60, + }); + const fresh = build("1.7.0-fresh", { + manifest: manifestFor("1.7.0-fresh", { issuedDays: -1, expiresDays: 29 }), + }); + const stale = [1, 2, 3, 4].map((n) => + build(`1.6.${n}-stale`, { + manifest: manifestFor(`1.6.${n}-stale`, { issuedDays: -100 - n, expiresDays: -70 - n }), + ageDays: 100 + n, + }), + ); + + const plan = planCleanup({ + now: NOW, + channels: { v1: "1.7.0-promoted" }, + builds: [promoted, fresh, ...stale], + assets: [], + minRecentPerChannel: 2, + }); + + const kept = Object.fromEntries(plan.keepBuilds.map((b) => [b.buildId, b.reasons])); + // Promoted survives even though expired — and that expiry is loudly warned. + assert.deepEqual(kept["1.7.0-promoted"], ["recent", "promoted:v1"]); + assert.ok(kept["1.7.0-fresh"].includes("unexpired")); + // minRecentPerChannel=2 → promoted + fresh are the 2 most recent; all four + // stale expired builds go. + assert.deepEqual( + plan.deleteBuilds.map((b) => b.buildId).sort(), + ["1.6.1-stale", "1.6.2-stale", "1.6.3-stale", "1.6.4-stale"], + ); + assert.ok(plan.warnings.some((w) => w.includes("EXPIRED"))); +}); + +test("planCleanup keeps the N most recent builds per channel even when all expired", () => { + const builds = [1, 2, 3].map((n) => + build(`1.5.${n}-old`, { + manifest: manifestFor(`1.5.${n}-old`, { issuedDays: -100 + n, expiresDays: -70 + n }), + }), + ); + const plan = planCleanup({ + now: NOW, + channels: {}, + builds, + assets: [], + minRecentPerChannel: 2, + }); + assert.deepEqual( + plan.keepBuilds.map((b) => b.buildId).sort(), + ["1.5.2-old", "1.5.3-old"], + ); + assert.deepEqual(plan.deleteBuilds.map((b) => b.buildId), ["1.5.1-old"]); +}); + +test("planCleanup grace-protects manifest-less uploads, deletes old orphans", () => { + const inFlight = build("1.8.0-inflight", { manifest: null, ageDays: 0.5 }); + const abandoned = build("1.8.0-abandoned", { manifest: null, ageDays: 30 }); + const plan = planCleanup({ + now: NOW, + channels: {}, + builds: [inFlight, abandoned], + assets: [], + minRecentPerChannel: 0, + }); + assert.deepEqual(plan.keepBuilds, [ + { buildId: "1.8.0-inflight", reasons: ["upload-grace"] }, + ]); + assert.deepEqual(plan.deleteBuilds.map((b) => b.buildId), ["1.8.0-abandoned"]); +}); + +test("planCleanup mark-and-sweeps shared assets against retained manifests", () => { + const referencedChunk = "aaa.0123456789abcdef.js"; + const kept = build("1.7.0-live", { + manifest: manifestFor("1.7.0-live", { + chunks: { [referencedChunk]: "sha384-a", "remoteEntry.js": "sha384-r" }, + }), + }); + const plan = planCleanup({ + now: NOW, + channels: { v1: "1.7.0-live" }, + builds: [kept], + assets: [ + { key: `assets/${referencedChunk}`, name: referencedChunk, lastModified: new Date(NOW - 90 * DAY_MS) }, + { key: "assets/bbb.fedcba9876543210.js", name: "bbb.fedcba9876543210.js", lastModified: new Date(NOW - 90 * DAY_MS) }, + { key: "assets/ccc.1111111111111111.js", name: "ccc.1111111111111111.js", lastModified: new Date(NOW - 1 * 60 * 60 * 1000) }, + ], + }); + assert.deepEqual(plan.deleteAssets, ["assets/bbb.fedcba9876543210.js"]); + assert.equal(plan.referencedAssets, 1); + assert.equal(plan.gracedAssets, 1); +}); + +test("planCleanup keeps and warns about anomalies", () => { + const mismatched = build("1.7.0-prefix", { + manifest: { buildId: "1.7.0-OTHER", channel: "v1", expiresAt: iso(-50), issuedAt: iso(-80), chunks: {} }, + }); + const plan = planCleanup({ + now: NOW, + channels: { v1: "1.9.9-missing" }, + builds: [mismatched], + assets: [], + minRecentPerChannel: 0, + }); + assert.deepEqual(plan.deleteBuilds, []); + assert.ok(plan.warnings.some((w) => w.includes("no published build prefix"))); + assert.ok(plan.warnings.some((w) => w.includes("manifest claiming to be"))); +}); + +function fakeStorage() { + const objects = new Map([ + [ + "1.7.0-live/manifest.json", + JSON.stringify(manifestFor("1.7.0-live", { chunks: { "aaa.0123456789abcdef.js": "sha384-a" } })), + ], + ["1.7.0-live/remoteEntry.js", "remote"], + [ + "1.6.0-dead/manifest.json", + JSON.stringify(manifestFor("1.6.0-dead", { issuedDays: -100, expiresDays: -70 })), + ], + ["1.6.0-dead/remoteEntry.js", "remote"], + ["assets/aaa.0123456789abcdef.js", "chunk"], + ["assets/bbb.fedcba9876543210.js", "chunk"], + ]); + const deletions = []; + return { + objects, + deletions, + ops: { + listObjects: async () => + [...objects.keys()].map((key) => ({ key, lastModified: new Date(NOW - 90 * DAY_MS) })), + readJson: async (_ctx, key) => + objects.has(key) ? JSON.parse(objects.get(key)) : undefined, + readChannelPointers: async () => ({ v1: "1.7.0-live" }), + deleteObject: async (_ctx, key) => { + deletions.push(key); + objects.delete(key); + }, + }, + }; +} + +test("runCleanup dry run reports the plan and deletes nothing", async () => { + const storage = fakeStorage(); + const result = await runCleanup({ + ctx: {}, + ops: storage.ops, + now: NOW, + logger: silentLogger, + minRecentPerChannel: 1, + }); + assert.equal(result.applied, false); + assert.deepEqual(result.deleteBuilds.map((b) => b.buildId), ["1.6.0-dead"]); + assert.deepEqual(result.deleteAssets, ["assets/bbb.fedcba9876543210.js"]); + assert.deepEqual(storage.deletions, []); +}); + +test("runCleanup --apply deletes the build manifest before its payload", async () => { + const storage = fakeStorage(); + const result = await runCleanup({ + ctx: {}, + ops: storage.ops, + apply: true, + now: NOW, + logger: silentLogger, + minRecentPerChannel: 1, + }); + assert.equal(result.applied, true); + assert.deepEqual(storage.deletions, [ + "1.6.0-dead/manifest.json", + "1.6.0-dead/remoteEntry.js", + "assets/bbb.fedcba9876543210.js", + ]); + assert.ok(storage.objects.has("1.7.0-live/manifest.json")); + assert.ok(storage.objects.has("assets/aaa.0123456789abcdef.js")); +}); + +test("runCleanup aborts apply when a channel pointer changes mid-plan", async () => { + const storage = fakeStorage(); + let reads = 0; + storage.ops.readChannelPointers = async () => + (reads += 1) === 1 ? { v1: "1.7.0-live" } : { v1: "1.6.0-dead" }; + await assert.rejects( + runCleanup({ + ctx: {}, + ops: storage.ops, + apply: true, + now: NOW, + logger: silentLogger, + minRecentPerChannel: 1, + }), + /channel pointers changed/, + ); + assert.deepEqual(storage.deletions, []); +}); + +test("Azure listObjects paginates and decodes the XML listing", async (t) => { + const originalFetch = globalThis.fetch; + t.after(() => { + globalThis.fetch = originalFetch; + }); + const pages = [ + ` + + 1.7.0-abc/manifest.jsonFri, 01 Aug 2026 00:00:00 GMT + assets/a&b.0123456789abcdef.jsFri, 01 Aug 2026 00:00:00 GMT + + page-2 + `, + ` + + v1/manifest.jsonFri, 01 Aug 2026 00:00:00 GMT + + + `, + ]; + const requestedUrls = []; + globalThis.fetch = async (url) => { + requestedUrls.push(String(url)); + return new Response(pages.shift(), { status: 200 }); + }; + + const results = await listObjects({ + endpoint: "https://test.blob.core.windows.net", + containerName: "widget-cdn", + accessToken: "token", + }); + + assert.deepEqual( + results.map((r) => r.key), + ["1.7.0-abc/manifest.json", "assets/a&b.0123456789abcdef.js", "v1/manifest.json"], + ); + assert.equal(results[0].lastModified.toISOString(), "2026-08-01T00:00:00.000Z"); + assert.equal(requestedUrls.length, 2); + assert.match(requestedUrls[1], /marker=page-2/); +}); + +test("Azure channel pointers come from vN/manifest.json blobs", async () => { + const pointers = await readAzureChannelPointers( + {}, + { + list: async () => [ + { key: "v1/manifest.json" }, + { key: "v2/manifest.json" }, + { key: "v1-not-a-channel/manifest.json" }, + ], + readJson: async (_ctx, key) => + key === "v1/manifest.json" + ? { buildId: "1.7.0-abc" } + : { buildId: "!!invalid!!" }, + }, + ); + assert.deepEqual(pointers, { v1: "1.7.0-abc" }); +}); + +test("runCleanup --apply with an approved plan deletes only its intersection with fresh state", async () => { + const storage = fakeStorage(); + // The approved plan covers the dead build but NOT the orphaned asset — the + // asset became deletable after approval and must be deferred. It also + // approves a build that no longer exists — skipped, not an error. + const result = await runCleanup({ + ctx: {}, + ops: storage.ops, + apply: true, + approvedPlan: { + deleteBuilds: [{ buildId: "1.6.0-dead" }, { buildId: "1.0.0-gone" }], + deleteAssets: [], + }, + now: NOW, + logger: silentLogger, + minRecentPerChannel: 1, + }); + assert.equal(result.applied, true); + assert.deepEqual(storage.deletions, [ + "1.6.0-dead/manifest.json", + "1.6.0-dead/remoteEntry.js", + ]); + assert.ok(storage.objects.has("assets/bbb.fedcba9876543210.js")); +}); + +test("runCleanupCli round-trips a plan file from --plan-out to --apply --plan-in", async (t) => { + const dir = mkdtempSync(join(tmpdir(), "layerswap-cleanup-plan-")); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const planPath = join(dir, "plan.json"); + + const storage = fakeStorage(); + const planned = await runCleanupCli({ + argv: ["--plan-out", planPath, "--keep-recent", "1"], + ctx: {}, + ops: storage.ops, + logger: silentLogger, + now: NOW, + }); + assert.equal(planned.applied, false); + assert.deepEqual(storage.deletions, []); + const written = JSON.parse(readFileSync(planPath, "utf8")); + assert.deepEqual(written.deleteBuilds.map((b) => b.buildId), ["1.6.0-dead"]); + assert.deepEqual(written.deleteAssets, ["assets/bbb.fedcba9876543210.js"]); + + const applied = await runCleanupCli({ + argv: ["--apply", "--plan-in", planPath, "--keep-recent", "1"], + ctx: {}, + ops: storage.ops, + logger: silentLogger, + now: NOW, + }); + assert.equal(applied.applied, true); + assert.deepEqual(storage.deletions, [ + "1.6.0-dead/manifest.json", + "1.6.0-dead/remoteEntry.js", + "assets/bbb.fedcba9876543210.js", + ]); +}); + +test("runCleanupCli rejects contradictory plan flags", async (t) => { + const dir = mkdtempSync(join(tmpdir(), "layerswap-cleanup-flags-")); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const planPath = join(dir, "plan.json"); + writeFileSync(planPath, "{}"); + const storage = fakeStorage(); + await assert.rejects( + runCleanupCli({ + argv: ["--apply", "--plan-out", planPath], + ctx: {}, + ops: storage.ops, + logger: silentLogger, + now: NOW, + }), + /--plan-out captures a dry-run plan/, + ); + await assert.rejects( + runCleanupCli({ + argv: ["--plan-in", planPath], + ctx: {}, + ops: storage.ops, + logger: silentLogger, + now: NOW, + }), + /--plan-in only applies an approved plan/, + ); + assert.deepEqual(storage.deletions, []); +}); + +test("parseCleanupArgs parses flags and rejects unknown ones", () => { + assert.deepEqual(parseCleanupArgs([]), { apply: false }); + const parsed = parseCleanupArgs([ + "--apply", + "--keep-recent", + "5", + "--asset-grace-days", + "3", + "--build-grace-days", + "1", + ]); + assert.equal(parsed.apply, true); + assert.equal(parsed.minRecentPerChannel, 5); + assert.equal(parsed.assetGraceMs, 3 * 24 * 60 * 60 * 1000); + assert.equal(parsed.orphanGraceMs, 1 * 24 * 60 * 60 * 1000); + assert.deepEqual(parseCleanupArgs(["--plan-out", "p.json"]), { + apply: false, + planOut: "p.json", + }); + assert.deepEqual(parseCleanupArgs(["--apply", "--plan-in", "p.json"]), { + apply: true, + planIn: "p.json", + }); + assert.throws(() => parseCleanupArgs(["--nope"]), /unknown argument/); + assert.throws(() => parseCleanupArgs(["--keep-recent", "x"]), /non-negative number/); + assert.throws(() => parseCleanupArgs(["--plan-out"]), /expects a file path/); +}); diff --git a/apps/widget-cdn/scripts/deploy-azure.mjs b/apps/widget-cdn/scripts/deploy-azure.mjs new file mode 100644 index 0000000000..6ed96d23a9 --- /dev/null +++ b/apps/widget-cdn/scripts/deploy-azure.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +// Publish a signed build to Azure Blob Storage. +// +// Immutable build controls live at // and content-hashed chunks live +// in /assets/. Promotion atomically copies the same signed manifest to +// //manifest.json; its build-addressed remoteEntry keeps all code on +// immutable URLs without needing any edge redirect service. + +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { + contentTypeFor, + makeClient, + objectExists, + putObject, + readJsonObject, + writeChannelManifest, +} from "./azure-lib.mjs"; +import { resolveBuildIdentity } from "./build-id.mjs"; +import { WIDGET_PROTOCOL_MAJOR } from "@layerswap/widget-types"; +import { + ASSET_BASE, + ASSET_DIRECTORY, + deploymentKey, + isSharedAsset, + remoteEntryForBuild, +} from "./cdn-layout.mjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +export function validateAzureManifest(manifest, identity) { + const mismatches = [ + ["buildId", manifest.buildId, identity.buildId], + ["protocolMajor", manifest.protocolMajor, WIDGET_PROTOCOL_MAJOR], + ["version", manifest.version, identity.version], + ["channel", manifest.channel, identity.channel], + ["gitSha", manifest.gitSha, identity.gitSha], + ["assetBase", manifest.assetBase, ASSET_BASE], + [ + "remoteEntry", + manifest.remoteEntry, + remoteEntryForBuild(identity.buildId), + ], + ].filter(([, actual, expected]) => actual !== expected); + + if (mismatches.length > 0) { + const details = mismatches + .map( + ([field, actual, expected]) => + `${field}: ${JSON.stringify(actual)} !== ${JSON.stringify(expected)}`, + ) + .join(", "); + throw new Error( + `[deploy-azure] manifest identity does not match this build: ${details}`, + ); + } +} + +function listFiles(dir) { + const out = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) out.push(...listFiles(full)); + else if (entry.isFile()) out.push(full); + } + return out; +} + +async function runWithConcurrency(items, limit, task) { + if (items.length === 0) return; + const workerCount = Math.min(Math.max(1, Math.floor(limit)), items.length); + let cursor = 0; + await Promise.all( + Array.from({ length: workerCount }, async () => { + while (cursor < items.length) { + const item = items[cursor]; + cursor += 1; + await task(item); + } + }), + ); +} + +export async function deployAzureBuild(options = {}) { + const root = options.root ?? ROOT; + const identity = options.identity ?? resolveBuildIdentity(root); + const env = options.env ?? process.env; + const logger = options.logger ?? console; + const createClient = options.createClient ?? (() => makeClient(env)); + const exists = options.objectExists ?? objectExists; + const upload = options.putObject ?? putObject; + const readJson = options.readJsonObject ?? readJsonObject; + const publishChannel = options.writeChannelManifest ?? writeChannelManifest; + const uploadConcurrency = options.uploadConcurrency ?? 8; + const dist = join(root, "dist", identity.buildId); + const assetDist = join(root, "dist", ASSET_DIRECTORY); + const manifestPath = join(dist, "manifest.json"); + + if (!existsSync(manifestPath)) { + throw new Error( + `[deploy-azure] missing ${manifestPath} — run \`pnpm build\` first.`, + ); + } + + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch (err) { + throw new Error( + `[deploy-azure] failed to read manifest: ${err instanceof Error ? err.message : String(err)}`, + ); + } + if (!manifest.signature) { + throw new Error( + "[deploy-azure] refusing to deploy an UNSIGNED manifest. " + + "Build with LAYERSWAP_PRIVATE_KEY_PEM set.", + ); + } + + // Validate before Azure credentials are read or a client is created. + validateAzureManifest(manifest, identity); + if (!existsSync(assetDist)) { + throw new Error( + `[deploy-azure] missing shared asset output ${assetDist} — run \`pnpm build\` first.`, + ); + } + + const ctx = createClient(); + if (await exists(ctx, `${identity.buildId}/manifest.json`)) { + if (env.ALLOW_OVERWRITE !== "1") { + throw new Error( + `[deploy-azure] build ${identity.buildId} is already published (immutable). ` + + "Deploy from a new commit, or set ALLOW_OVERWRITE=1 to force.", + ); + } + } + + const files = [ + ...listFiles(dist).map((file) => ({ file, root: dist, shared: false })), + ...listFiles(assetDist).map((file) => ({ + file, + root: assetDist, + shared: true, + })), + ].sort((a, b) => a.file.localeCompare(b.file)); + + const publishableFiles = files.map(({ file, root: fileRoot, shared }) => { + const rel = relative(fileRoot, file).split(/[\\/]/).join("/"); + if (shared && !isSharedAsset(rel)) { + throw new Error( + `[deploy-azure] refusing non-content-hashed file in ${ASSET_DIRECTORY}/: ${rel}`, + ); + } + return { + file, + rel, + shared, + key: shared + ? deploymentKey(identity.buildId, rel) + : `${identity.buildId}/${rel}`, + }; + }); + const manifestFile = publishableFiles.find( + ({ file }) => file === manifestPath, + ); + const payloadFiles = publishableFiles.filter( + ({ file }) => file !== manifestPath, + ); + if (!manifestFile) { + throw new Error( + `[deploy-azure] missing ${manifestPath} from publishable files.`, + ); + } + + let uploaded = 0; + let reused = 0; + logger.log( + `[deploy-azure] publishing ${files.length} file(s) to ` + + `${ctx.account ?? "(account)"}/${ctx.containerName ?? "(container)"} …`, + ); + + const publishFile = async ({ file, rel, shared, key }) => { + if (shared && env.ALLOW_OVERWRITE !== "1" && (await exists(ctx, key))) { + reused += 1; + logger.log(` = ${key} (already published)`); + return; + } + + const body = readFileSync(file); + await upload(ctx, key, body, { + contentType: contentTypeFor(rel), + cacheControl: "public, max-age=31536000, immutable", + }); + uploaded += 1; + logger.log(` ↑ ${key} (${body.length} bytes)`); + }; + + // Publishing the manifest last ensures an interrupted deployment can + // never expose a build whose payload is incomplete. + await runWithConcurrency(payloadFiles, uploadConcurrency, publishFile); + await publishFile(manifestFile); + + logger.log( + `[deploy-azure] published build ${identity.buildId} ` + + `(${uploaded} uploaded, ${reused} reused).`, + ); + + if (env.LAYERSWAP_PROMOTE === "false") { + logger.log( + `[deploy-azure] LAYERSWAP_PROMOTE=false — channel ${identity.channel} NOT changed. ` + + "Promote later with:", + ); + logger.log( + ` node scripts/rollback-azure.mjs ` + + `${identity.channel} ${identity.buildId}`, + ); + return { uploaded, reused, promoted: false }; + } + + const channelKey = `${identity.channel}/manifest.json`; + const previous = await readJson(ctx, channelKey); + await publishChannel(ctx, identity.channel, manifest); + logger.log( + `[deploy-azure] channel ${identity.channel}: ` + + `${previous?.buildId ?? "(none)"} → ${identity.buildId}`, + ); + logger.log(`[deploy-azure] live at /${identity.channel}/manifest.json`); + return { uploaded, reused, promoted: true }; +} + +const entryUrl = process.argv[1] + ? pathToFileURL(resolve(process.argv[1])).href + : undefined; +if (entryUrl === import.meta.url) { + try { + await deployAzureBuild(); + } catch (err) { + console.error(err instanceof Error ? err.message : err); + process.exitCode = 1; + } +} diff --git a/apps/widget-cdn/scripts/deploy-r2.mjs b/apps/widget-cdn/scripts/deploy-r2.mjs deleted file mode 100644 index 221051cfb6..0000000000 --- a/apps/widget-cdn/scripts/deploy-r2.mjs +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env node -// Upload signed build control files from `dist//` to the immutable -// `/` prefix and content-hashed chunks to the shared `/assets/` -// namespace. Unless LAYERSWAP_PROMOTE=false, the rolling channel is then -// updated in `channels.json`. - -import { readFileSync, existsSync, readdirSync } from 'node:fs'; -import { dirname, join, resolve, relative } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { - makeClient, - objectExists, - putObject, - readChannels, - writeChannels, - contentTypeFor, -} from './r2-lib.mjs'; -import { resolveBuildIdentity } from './build-id.mjs'; -import { - ASSET_BASE, - ASSET_DIRECTORY, - deploymentKey, - isSharedAsset, -} from './cdn-layout.mjs'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const ROOT = resolve(__dirname, '..'); - -export function validateManifestIdentity(manifest, identity) { - const mismatches = [ - ['buildId', manifest.buildId, identity.buildId], - ['version', manifest.version, identity.version], - ['channel', manifest.channel, identity.channel], - ['gitSha', manifest.gitSha, identity.gitSha], - ['assetBase', manifest.assetBase, ASSET_BASE], - ].filter(([, actual, expected]) => actual !== expected); - - if (mismatches.length > 0) { - const details = mismatches - .map( - ([field, actual, expected]) => ( - `${field}: ${JSON.stringify(actual)} !== ${JSON.stringify(expected)}` - ), - ) - .join(', '); - throw new Error(`[deploy-r2] manifest identity does not match this build: ${details}`); - } -} - -function listFiles(dir) { - const out = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) out.push(...listFiles(full)); - else if (entry.isFile()) out.push(full); - } - return out; -} - -async function runWithConcurrency(items, limit, task) { - if (items.length === 0) return; - const workerCount = Math.min(Math.max(1, Math.floor(limit)), items.length); - let cursor = 0; - await Promise.all(Array.from({ length: workerCount }, async () => { - while (cursor < items.length) { - const item = items[cursor]; - cursor += 1; - await task(item); - } - })); -} - -export async function deployBuild(options = {}) { - const root = options.root ?? ROOT; - const identity = options.identity ?? resolveBuildIdentity(root); - const env = options.env ?? process.env; - const logger = options.logger ?? console; - const createClient = options.createClient ?? makeClient; - const exists = options.objectExists ?? objectExists; - const upload = options.putObject ?? putObject; - const readChannelMap = options.readChannels ?? readChannels; - const writeChannelMap = options.writeChannels ?? writeChannels; - const uploadConcurrency = options.uploadConcurrency ?? 8; - const dist = join(root, 'dist', identity.buildId); - const assetDist = join(root, 'dist', ASSET_DIRECTORY); - const manifestPath = join(dist, 'manifest.json'); - - if (!existsSync(manifestPath)) { - throw new Error(`[deploy-r2] missing ${manifestPath} — run \`pnpm build\` first.`); - } - - let manifest; - try { - manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); - } catch (err) { - throw new Error( - `[deploy-r2] failed to read manifest: ${err instanceof Error ? err.message : String(err)}`, - ); - } - if (!manifest.signature) { - throw new Error( - '[deploy-r2] refusing to deploy an UNSIGNED manifest. ' - + 'Build with LAYERSWAP_PRIVATE_KEY_PEM set.', - ); - } - - // This must run before credentials are read or an R2 client is created: - // stale/copied artifacts must never upload under the wrong immutable key. - validateManifestIdentity(manifest, identity); - if (!existsSync(assetDist)) { - throw new Error(`[deploy-r2] missing shared asset output ${assetDist} — run \`pnpm build\` first.`); - } - - const ctx = createClient(); - if (await exists(ctx, `${identity.buildId}/manifest.json`)) { - if (env.ALLOW_OVERWRITE !== '1') { - throw new Error( - `[deploy-r2] build ${identity.buildId} is already published (immutable). ` - + 'Deploy from a new commit, or set ALLOW_OVERWRITE=1 to force.', - ); - } - } - - // Prepare and validate every key before uploading anything. The manifest - // is held aside and published only after every payload worker succeeds, so - // an interrupted upload can never make an incomplete build rollback-ready. - const files = [ - ...listFiles(dist).map((file) => ({ file, root: dist, shared: false })), - ...(existsSync(assetDist) - ? listFiles(assetDist).map((file) => ({ file, root: assetDist, shared: true })) - : []), - ].sort((a, b) => a.file.localeCompare(b.file)); - - const publishableFiles = files.map(({ file, root: fileRoot, shared }) => { - const rel = relative(fileRoot, file).split(/[\\/]/).join('/'); - if (shared && !isSharedAsset(rel)) { - throw new Error(`[deploy-r2] refusing non-content-hashed file in ${ASSET_DIRECTORY}/: ${rel}`); - } - return { - file, - rel, - shared, - key: shared ? deploymentKey(identity.buildId, rel) : `${identity.buildId}/${rel}`, - }; - }); - const manifestFile = publishableFiles.find(({ file }) => file === manifestPath); - const payloadFiles = publishableFiles.filter(({ file }) => file !== manifestPath); - if (!manifestFile) { - throw new Error(`[deploy-r2] missing ${manifestPath} from publishable files.`); - } - - let uploaded = 0; - let reused = 0; - logger.log(`[deploy-r2] publishing ${files.length} file(s) to ${ctx.bucket} …`); - - const publishFile = async ({ file, rel, shared, key }) => { - // A content hash is the immutable asset key. Reuse an existing object - // instead of storing or transferring the same chunk for every build. - if (shared && env.ALLOW_OVERWRITE !== '1' && await exists(ctx, key)) { - reused += 1; - logger.log(` = ${key} (already published)`); - return; - } - - const body = readFileSync(file); - await upload(ctx, key, body, { - contentType: contentTypeFor(rel), - cacheControl: 'public, max-age=31536000, immutable', - }); - uploaded += 1; - logger.log(` ↑ ${key} (${body.length} bytes)`); - }; - - await runWithConcurrency(payloadFiles, uploadConcurrency, publishFile); - await publishFile(manifestFile); - - logger.log( - `[deploy-r2] published build ${identity.buildId} (${uploaded} uploaded, ${reused} reused).`, - ); - - if (env.LAYERSWAP_PROMOTE === 'false') { - logger.log( - `[deploy-r2] LAYERSWAP_PROMOTE=false — channel ${identity.channel} NOT changed. ` - + 'Promote later with:', - ); - logger.log(` node scripts/rollback-r2.mjs ${identity.channel} ${identity.buildId}`); - return { uploaded, reused, promoted: false }; - } - - const channels = await readChannelMap(ctx); - const previous = channels[identity.channel]; - channels[identity.channel] = identity.buildId; - await writeChannelMap(ctx, channels); - logger.log( - `[deploy-r2] channel ${identity.channel}: ${previous ?? '(none)'} → ${identity.buildId}`, - ); - logger.log(`[deploy-r2] live at /${identity.channel}/manifest.json`); - return { uploaded, reused, promoted: true }; -} - -const entryUrl = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; -if (entryUrl === import.meta.url) { - try { - await deployBuild(); - } catch (err) { - console.error(err instanceof Error ? err.message : err); - process.exitCode = 1; - } -} diff --git a/apps/widget-cdn/scripts/protocol-contract.test.mjs b/apps/widget-cdn/scripts/protocol-contract.test.mjs new file mode 100644 index 0000000000..93977e94de --- /dev/null +++ b/apps/widget-cdn/scripts/protocol-contract.test.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; +import { WIDGET_PROTOCOL_MAJOR } from '@layerswap/widget-types'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); + +function packageMajor(relativePath) { + const pkg = JSON.parse(readFileSync(join(root, relativePath), 'utf8')); + return Number(pkg.version.split('.')[0]); +} + +test('public loader package majors match the CDN protocol major', () => { + assert.equal(packageMajor('packages/widget/types/package.json'), WIDGET_PROTOCOL_MAJOR); + assert.equal(packageMajor('packages/widget/js/package.json'), WIDGET_PROTOCOL_MAJOR); + assert.equal(packageMajor('packages/widget/react/package.json'), WIDGET_PROTOCOL_MAJOR); +}); + diff --git a/apps/widget-cdn/scripts/r2-lib.mjs b/apps/widget-cdn/scripts/r2-lib.mjs deleted file mode 100644 index 55f8e31a2e..0000000000 --- a/apps/widget-cdn/scripts/r2-lib.mjs +++ /dev/null @@ -1,114 +0,0 @@ -// Shared R2 helpers for the deploy + rollback scripts. -// -// R2 is S3-compatible, so we drive it with the AWS S3 SDK pointed at the -// account's R2 endpoint. Credentials come from env (an R2 API token with -// Object Read & Write on the target bucket): -// -// R2_ACCOUNT_ID Cloudflare account id -// R2_ACCESS_KEY_ID R2 token access key id -// R2_SECRET_ACCESS_KEY R2 token secret -// R2_BUCKET bucket name (default: layerswap-widget-cdn) - -import { - S3Client, - PutObjectCommand, - GetObjectCommand, - HeadObjectCommand, -} from '@aws-sdk/client-s3'; - -export const CHANNELS_KEY = 'channels.json'; - -const CONTENT_TYPES = { - js: 'text/javascript; charset=utf-8', - mjs: 'text/javascript; charset=utf-8', - css: 'text/css; charset=utf-8', - json: 'application/json; charset=utf-8', - map: 'application/json; charset=utf-8', -}; - -export function contentTypeFor(name) { - const ext = name.split('.').pop()?.toLowerCase() ?? ''; - return CONTENT_TYPES[ext] ?? 'application/octet-stream'; -} - -function requireEnv(name) { - const v = process.env[name]; - if (!v) { - console.error(`[r2] missing required env var ${name}`); - process.exit(1); - } - return v; -} - -export function makeClient() { - const accountId = requireEnv('R2_ACCOUNT_ID'); - return { - bucket: process.env.R2_BUCKET || 'layerswap-widget-cdn', - client: new S3Client({ - region: 'auto', - endpoint: `https://${accountId}.r2.cloudflarestorage.com`, - credentials: { - accessKeyId: requireEnv('R2_ACCESS_KEY_ID'), - secretAccessKey: requireEnv('R2_SECRET_ACCESS_KEY'), - }, - }), - }; -} - -export async function objectExists({ client, bucket }, key) { - try { - await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); - return true; - } catch (err) { - if (err?.$metadata?.httpStatusCode === 404 || err?.name === 'NotFound') return false; - throw err; - } -} - -export async function putObject({ client, bucket }, key, body, { contentType, cacheControl } = {}) { - await client.send( - new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: body, - ContentType: contentType ?? contentTypeFor(key), - CacheControl: cacheControl, - }), - ); -} - -export async function readJsonObject(ctx, key) { - try { - const res = await ctx.client.send( - new GetObjectCommand({ Bucket: ctx.bucket, Key: key }), - ); - const text = await res.Body.transformToString(); - const value = JSON.parse(text); - if (!value || typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`[r2] ${key} does not contain a JSON object`); - } - return value; - } catch (err) { - if ( - err?.$metadata?.httpStatusCode === 404 - || err?.name === 'NoSuchKey' - || err?.name === 'NotFound' - ) { - return undefined; - } - throw err; - } -} - -export async function readChannels(ctx) { - return (await readJsonObject(ctx, CHANNELS_KEY)) ?? {}; -} - -export async function writeChannels(ctx, channels) { - await putObject(ctx, CHANNELS_KEY, JSON.stringify(channels, null, 2), { - contentType: 'application/json; charset=utf-8', - // The pointer must never be cached hard — the Worker reads the freshest - // copy so channel flips (roll-forward / rollback) take effect promptly. - cacheControl: 'no-store', - }); -} diff --git a/apps/widget-cdn/scripts/release-scripts.test.mjs b/apps/widget-cdn/scripts/release-scripts.test.mjs deleted file mode 100644 index b74fc1350c..0000000000 --- a/apps/widget-cdn/scripts/release-scripts.test.mjs +++ /dev/null @@ -1,231 +0,0 @@ -import assert from 'node:assert/strict'; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import test from 'node:test'; -import { deployBuild } from './deploy-r2.mjs'; -import { ASSET_BASE, deploymentKey } from './cdn-layout.mjs'; -import { rollbackChannel } from './rollback-r2.mjs'; - -const silentLogger = { log() {}, warn() {} }; - -function deployFixture(t, identity, manifestOverrides = {}) { - const root = mkdtempSync(join(tmpdir(), 'layerswap-deploy-r2-')); - const dist = join(root, 'dist', identity.buildId); - mkdirSync(dist, { recursive: true }); - writeFileSync(join(dist, 'manifest.json'), JSON.stringify({ - ...identity, - remoteEntry: './remoteEntry.js', - assetBase: ASSET_BASE, - chunks: {}, - signature: 'signed', - ...manifestOverrides, - })); - t.after(() => rmSync(root, { recursive: true, force: true })); - return root; -} - -test('deploy rejects a mismatched manifest before creating an R2 client', async (t) => { - const identity = { - buildId: '1.7.0-0123456789ab', - version: '1.7.0', - channel: 'v1', - gitSha: '0123456789abcdef', - }; - const root = deployFixture(t, identity, { buildId: '1.7.0-fedcba987654' }); - let clientsCreated = 0; - - await assert.rejects( - deployBuild({ - root, - identity, - logger: silentLogger, - createClient() { - clientsCreated += 1; - return {}; - }, - }), - /manifest identity does not match this build/, - ); - assert.equal(clientsCreated, 0); -}); - -test('content-hashed chunks use the shared asset namespace', () => { - assert.equal( - deploymentKey('1.7.0-0123456789ab', '837.0123456789abcdef.js'), - 'assets/837.0123456789abcdef.js', - ); - assert.equal( - deploymentKey('1.7.0-0123456789ab', 'remoteEntry.js'), - '1.7.0-0123456789ab/remoteEntry.js', - ); -}); - -test('deploy reuses an existing shared asset and publishes build controls', async (t) => { - const identity = { - buildId: '1.7.0-0123456789ab', - version: '1.7.0', - channel: 'v1', - gitSha: '0123456789abcdef', - }; - const root = deployFixture(t, identity); - const buildDir = join(root, 'dist', identity.buildId); - const assetDir = join(root, 'dist', 'assets'); - const assetName = '837.0123456789abcdef.js'; - mkdirSync(assetDir, { recursive: true }); - writeFileSync(join(buildDir, 'remoteEntry.js'), 'remote'); - writeFileSync(join(assetDir, assetName), 'chunk'); - - const uploadedKeys = []; - const result = await deployBuild({ - root, - identity, - env: { LAYERSWAP_PROMOTE: 'false' }, - logger: silentLogger, - createClient: () => ({ bucket: 'test' }), - objectExists: async (_ctx, key) => key === `assets/${assetName}`, - putObject: async (_ctx, key) => uploadedKeys.push(key), - }); - - assert.deepEqual(result, { uploaded: 2, reused: 1, promoted: false }); - assert.deepEqual(uploadedKeys, [ - `${identity.buildId}/remoteEntry.js`, - `${identity.buildId}/manifest.json`, - ]); -}); - -test('deploy uploads payloads concurrently and publishes the manifest last', async (t) => { - const identity = { - buildId: '1.7.0-0123456789ab', - version: '1.7.0', - channel: 'v1', - gitSha: '0123456789abcdef', - }; - const root = deployFixture(t, identity); - const buildDir = join(root, 'dist', identity.buildId); - const assetDir = join(root, 'dist', 'assets'); - mkdirSync(assetDir, { recursive: true }); - writeFileSync(join(buildDir, 'remoteEntry.js'), 'remote'); - writeFileSync(join(assetDir, 'one.0123456789abcdef.js'), 'one'); - writeFileSync(join(assetDir, 'two.fedcba9876543210.js'), 'two'); - - let activePayloadUploads = 0; - let maxActivePayloadUploads = 0; - const completedKeys = []; - await deployBuild({ - root, - identity, - env: { LAYERSWAP_PROMOTE: 'false' }, - logger: silentLogger, - uploadConcurrency: 2, - createClient: () => ({ bucket: 'test' }), - objectExists: async () => false, - putObject: async (_ctx, key) => { - if (key.endsWith('/manifest.json')) { - assert.equal(activePayloadUploads, 0, 'manifest started before payloads finished'); - } else { - activePayloadUploads += 1; - maxActivePayloadUploads = Math.max(maxActivePayloadUploads, activePayloadUploads); - await new Promise(resolve => setTimeout(resolve, 10)); - activePayloadUploads -= 1; - } - completedKeys.push(key); - }, - }); - - assert.equal(maxActivePayloadUploads, 2); - assert.equal(completedKeys.at(-1), `${identity.buildId}/manifest.json`); -}); - -test('rollback updates a channel only for a matching published manifest', async () => { - const writes = []; - const result = await rollbackChannel({ - channel: 'v1', - buildId: '1.7.0-0123456789ab', - ctx: {}, - logger: silentLogger, - readManifest: async () => ({ buildId: '1.7.0-0123456789ab', channel: 'v1' }), - readChannelMap: async () => ({ v1: '1.6.0-aaaaaaaaaaaa' }), - writeChannelMap: async (_ctx, channels) => writes.push({ ...channels }), - }); - - assert.equal(result.changed, true); - assert.deepEqual(writes, [{ v1: '1.7.0-0123456789ab' }]); -}); - -test('rollback rejects a missing target without changing the channel', async () => { - let writes = 0; - await assert.rejects( - rollbackChannel({ - channel: 'v1', - buildId: '1.7.0-0123456789ab', - ctx: {}, - logger: silentLogger, - readManifest: async () => undefined, - readChannelMap: async () => ({}), - writeChannelMap: async () => { writes += 1; }, - }), - /is not published/, - ); - assert.equal(writes, 0); -}); - -test('rollback rejects an expired target build', async () => { - let writes = 0; - await assert.rejects( - rollbackChannel({ - channel: 'v1', - buildId: '1.7.0-0123456789ab', - ctx: {}, - logger: silentLogger, - readManifest: async () => ({ - buildId: '1.7.0-0123456789ab', - channel: 'v1', - expiresAt: new Date(Date.now() - 1000).toISOString(), - }), - readChannelMap: async () => ({}), - writeChannelMap: async () => { writes += 1; }, - }), - /expired at/, - ); - assert.equal(writes, 0); -}); - -test('rollback warns but proceeds for a near-expiry target build', async () => { - const warnings = []; - const writes = []; - const result = await rollbackChannel({ - channel: 'v1', - buildId: '1.7.0-0123456789ab', - ctx: {}, - logger: { log() {}, warn(msg) { warnings.push(msg); } }, - readManifest: async () => ({ - buildId: '1.7.0-0123456789ab', - channel: 'v1', - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }), - readChannelMap: async () => ({ v1: '1.6.0-aaaaaaaaaaaa' }), - writeChannelMap: async (_ctx, channels) => writes.push({ ...channels }), - }); - assert.equal(result.changed, true); - assert.equal(writes.length, 1); - assert.equal(warnings.length, 1); - assert.match(warnings[0], /expires/); -}); - -test('rollback rejects a build from another major channel', async () => { - let writes = 0; - await assert.rejects( - rollbackChannel({ - channel: 'v1', - buildId: '2.0.0-0123456789ab', - ctx: {}, - logger: silentLogger, - readManifest: async () => ({ buildId: '2.0.0-0123456789ab', channel: 'v2' }), - readChannelMap: async () => ({}), - writeChannelMap: async () => { writes += 1; }, - }), - /on v2, not v1/, - ); - assert.equal(writes, 0); -}); diff --git a/apps/widget-cdn/scripts/rollback-azure.mjs b/apps/widget-cdn/scripts/rollback-azure.mjs new file mode 100644 index 0000000000..63461956d3 --- /dev/null +++ b/apps/widget-cdn/scripts/rollback-azure.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// Promote a previously-published immutable Azure build by copying its signed +// manifest to the rolling //manifest.json path. + +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { + makeClient, + readJsonObject, + writeChannelManifest, +} from "./azure-lib.mjs"; +import { isValidBuildId } from "./build-id.mjs"; +import { remoteEntryForBuild } from "./cdn-layout.mjs"; +import { + WIDGET_PROTOCOL_MAJOR, + widgetProtocolMajorOf, +} from "@layerswap/widget-types"; + +export async function rollbackAzureChannel(options) { + const { + channel, + buildId, + logger = console, + readManifest = readJsonObject, + writeManifest = writeChannelManifest, + } = options; + + if (!channel || !/^v\d+$/.test(channel) || !isValidBuildId(buildId)) { + throw new Error("[rollback-azure] invalid channel or buildId"); + } + + const ctx = options.ctx ?? makeClient(options.env); + const manifestKey = `${buildId}/manifest.json`; + const manifest = await readManifest(ctx, manifestKey); + if (!manifest) { + throw new Error( + `[rollback-azure] build ${buildId} is not published ` + + `(no ${manifestKey} in container).`, + ); + } + const protocolMajor = widgetProtocolMajorOf(manifest); + if ( + manifest.buildId !== buildId || + manifest.channel !== channel || + protocolMajor !== WIDGET_PROTOCOL_MAJOR || + manifest.remoteEntry !== remoteEntryForBuild(buildId) + ) { + throw new Error( + `[rollback-azure] ${buildId} does not contain a valid Azure manifest ` + + `for ${channel}.`, + ); + } + + if (typeof manifest.expiresAt === "string") { + const expiresMs = Date.parse(manifest.expiresAt); + const dayMs = 24 * 60 * 60 * 1000; + if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) { + throw new Error( + `[rollback-azure] build ${buildId} expired at ${manifest.expiresAt} — ` + + "loaders will refuse it. Re-publish and re-sign the build.", + ); + } + if (!Number.isNaN(expiresMs) && expiresMs - Date.now() < 3 * dayMs) { + logger.warn( + `[rollback-azure] warning: build ${buildId} expires ` + + `${manifest.expiresAt} (<3 days).`, + ); + } + } else { + logger.warn( + `[rollback-azure] warning: build ${buildId} has no expiresAt; ` + + "verifying loaders will reject it.", + ); + } + + const current = await readManifest(ctx, `${channel}/manifest.json`); + if (current?.buildId === buildId) { + logger.log( + `[rollback-azure] channel ${channel} already points at ${buildId} — nothing to do.`, + ); + return { changed: false, previous: buildId }; + } + + await writeManifest(ctx, channel, manifest); + logger.log( + `[rollback-azure] channel ${channel}: ` + + `${current?.buildId ?? "(none)"} → ${buildId}`, + ); + logger.log( + "[rollback-azure] channel manifest is no-store; any upstream CDN must honor that header.", + ); + return { changed: true, previous: current?.buildId }; +} + +const entryUrl = process.argv[1] + ? pathToFileURL(resolve(process.argv[1])).href + : undefined; +if (entryUrl === import.meta.url) { + const [, , channel, buildId] = process.argv; + if (!channel || !/^v\d+$/.test(channel) || !isValidBuildId(buildId)) { + console.error("usage: node scripts/rollback-azure.mjs "); + console.error( + " e.g. node scripts/rollback-azure.mjs v1 1.7.0-abc123def456", + ); + process.exitCode = 1; + } else { + try { + await rollbackAzureChannel({ channel, buildId }); + } catch (err) { + console.error(err instanceof Error ? err.message : err); + process.exitCode = 1; + } + } +} diff --git a/apps/widget-cdn/scripts/rollback-r2.mjs b/apps/widget-cdn/scripts/rollback-r2.mjs deleted file mode 100644 index 21d4a5f891..0000000000 --- a/apps/widget-cdn/scripts/rollback-r2.mjs +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node -// Point a rolling channel at a previously-published build. The target's -// signed manifest must identify both the requested build and channel before -// the mutable channels.json pointer is changed. - -import { resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { - makeClient, - readJsonObject, - readChannels, - writeChannels, -} from './r2-lib.mjs'; -import { isValidBuildId } from './build-id.mjs'; - -export async function rollbackChannel(options) { - const { - channel, - buildId, - logger = console, - readManifest = readJsonObject, - readChannelMap = readChannels, - writeChannelMap = writeChannels, - } = options; - - if (!channel || !/^v\d+$/.test(channel) || !isValidBuildId(buildId)) { - throw new Error('[rollback-r2] invalid channel or buildId'); - } - - const ctx = options.ctx ?? makeClient(); - const manifestKey = `${buildId}/manifest.json`; - const manifest = await readManifest(ctx, manifestKey); - if (!manifest) { - throw new Error( - `[rollback-r2] build ${buildId} is not published (no ${manifestKey} in bucket).`, - ); - } - if (manifest.buildId !== buildId || manifest.channel !== channel) { - throw new Error( - `[rollback-r2] ${buildId} identifies build ${manifest.buildId ?? '(unknown)'} ` - + `on ${manifest.channel ?? '(unknown)'}, not ${channel}.`, - ); - } - - // Verifying loaders fail closed on manifests past `expiresAt` (replay - // protection), so pointing the channel at an expired build would brick - // every new mount. Re-publish (re-sign) the target build instead. - if (typeof manifest.expiresAt === 'string') { - const expiresMs = Date.parse(manifest.expiresAt); - const dayMs = 24 * 60 * 60 * 1000; - if (!Number.isNaN(expiresMs) && expiresMs <= Date.now()) { - throw new Error( - `[rollback-r2] build ${buildId} expired at ${manifest.expiresAt} — loaders will refuse it. ` - + 'Re-publish that build (re-signing refreshes its validity window) and retry.', - ); - } - if (!Number.isNaN(expiresMs) && expiresMs - Date.now() < 3 * dayMs) { - logger.warn( - `[rollback-r2] warning: build ${buildId} expires ${manifest.expiresAt} (<3 days). ` - + 'Plan a re-publish before then or new mounts will start failing.', - ); - } - } else { - logger.warn( - `[rollback-r2] warning: build ${buildId} has no expiresAt — verifying loaders reject ` - + 'manifests without a validity window. Re-publish it with a current pipeline.', - ); - } - - const channels = await readChannelMap(ctx); - const previous = channels[channel]; - if (previous === buildId) { - logger.log(`[rollback-r2] channel ${channel} already points at ${buildId} — nothing to do.`); - return { changed: false, previous }; - } - channels[channel] = buildId; - await writeChannelMap(ctx, channels); - logger.log(`[rollback-r2] channel ${channel}: ${previous ?? '(none)'} → ${buildId}`); - logger.log('[rollback-r2] propagates within ~60s (channel redirect cache).'); - return { changed: true, previous }; -} - -const entryUrl = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; -if (entryUrl === import.meta.url) { - const [, , channel, buildId] = process.argv; - if (!channel || !/^v\d+$/.test(channel) || !isValidBuildId(buildId)) { - console.error('usage: node scripts/rollback-r2.mjs '); - console.error(' e.g. node scripts/rollback-r2.mjs v1 1.4.0-abc123def456'); - process.exitCode = 1; - } else { - try { - await rollbackChannel({ channel, buildId }); - } catch (err) { - console.error(err instanceof Error ? err.message : err); - process.exitCode = 1; - } - } -} diff --git a/apps/widget-cdn/src/DepositWidget.tsx b/apps/widget-cdn/src/DepositWidget.tsx new file mode 100644 index 0000000000..a8016b3b98 --- /dev/null +++ b/apps/widget-cdn/src/DepositWidget.tsx @@ -0,0 +1,50 @@ +import { FC, ReactNode } from 'react'; +import { + type LayerswapWidgetConfig, + type CallbacksContextType, +} from '@layerswap/widget'; +import { Deposit } from '@layerswap/widget/deposit'; +import { type DefaultWalletConfig } from '@layerswap/wallets'; +import type { DepositWidgetProps as SharedDepositWidgetProps } from '@layerswap/widget-types'; +import type { Config as WagmiConfig } from 'wagmi'; +import { useWalletProviders } from './useWalletProviders'; +import '@layerswap/widget/index.css'; + +/** + * Props the `./DepositWidget` expose accepts: the shared public contract from + * `@layerswap/widget-types` (the same shape `mountDepositWidget` and the React + * `LayerswapDepositWidget` forward) with its open slots bound to this app's + * precise internal types — same derivation scheme as `./Widget`. + * + * The deposit-specific fields (`destination`, `methods`, …) are structurally + * typed in the shared package; spreading them into `Deposit` below is what + * keeps the two declarations in lockstep — a divergence fails this file's + * typecheck. + */ +export type DepositWidgetProps = SharedDepositWidgetProps< + WagmiConfig, + ReactNode, + LayerswapWidgetConfig, + DefaultWalletConfig, + CallbacksContextType +>; + +const DepositWidget: FC = ({ + config, + walletDefaults, + walletProvidersConfig, + callbacks, + wagmiConfig, + ...depositProps +}) => { + const walletProviders = useWalletProviders(walletDefaults, walletProvidersConfig, wagmiConfig); + + // `Deposit` wraps `LayerswapProvider` itself (and installs its own + // deposit-shaped loading skeleton), so unlike `./Widget` there is no + // provider wrapper here. + return ( + + ); +}; + +export default DepositWidget; diff --git a/apps/widget-cdn/src/Widget.tsx b/apps/widget-cdn/src/Widget.tsx index f5b41337ae..7d47166eaf 100644 --- a/apps/widget-cdn/src/Widget.tsx +++ b/apps/widget-cdn/src/Widget.tsx @@ -1,17 +1,14 @@ -import { FC, ReactNode, useMemo } from 'react'; +import { FC, ReactNode } from 'react'; import { LayerswapProvider, Swap, type LayerswapWidgetConfig, type CallbacksContextType, } from '@layerswap/widget'; -import { - getDefaultProviders, - createEVMProvider, - type DefaultWalletConfig, -} from '@layerswap/wallets'; +import { type DefaultWalletConfig } from '@layerswap/wallets'; import type { WidgetProps as SharedWidgetProps } from '@layerswap/widget-types'; import type { Config as WagmiConfig } from 'wagmi'; +import { useWalletProviders } from './useWalletProviders'; import '@layerswap/widget/index.css'; /** Wallet provider ids matching what `getDefaultProviders()` emits. */ @@ -35,42 +32,7 @@ export type WidgetProps = SharedWidgetProps< export type WalletProvidersConfig = NonNullable; const Widget: FC = ({ config, walletDefaults, walletProvidersConfig, callbacks, wagmiConfig }) => { - const walletProviders = useMemo(() => { - let providers = getDefaultProviders(walletDefaults ?? {}); - - // Allowlist — keep only the requested ids. Filtering here (before any - // `loadProvider()` call) means dropped chains never dynamic-import their - // SDK, so an `include` list gets the same lazy-loading win as `exclude`. - const included = walletProvidersConfig?.include; - if (included && included.length > 0) { - const keep = new Set(included); - providers = providers.filter((p) => keep.has(p.id)); - } - - // Blocklist — drop the requested ids. Applied after `include` so the two - // can be combined. - const excluded = walletProvidersConfig?.exclude; - if (excluded && excluded.length > 0) { - const drop = new Set(excluded); - providers = providers.filter((p) => !drop.has(p.id)); - } - - if (wagmiConfig) { - // Replace the eager EVM provider with one that adopts the host's wagmi - // config so the widget tracks the host's account/chain. Only do this - // when EVM is actually in the resolved set — respect the include/exclude - // lists literally rather than force-injecting EVM. - const evmIndex = providers.findIndex((p) => p.id === 'evm'); - if (evmIndex >= 0) { - providers[evmIndex] = createEVMProvider({ - walletConnectConfigs: walletDefaults?.walletConnect, - wagmiConfig, - }); - } - } - - return providers; - }, [wagmiConfig, walletDefaults, walletProvidersConfig]); + const walletProviders = useWalletProviders(walletDefaults, walletProvidersConfig, wagmiConfig); return ( diff --git a/apps/widget-cdn/src/mount.tsx b/apps/widget-cdn/src/mount.tsx index df5359f05a..35a3ddb0a5 100644 --- a/apps/widget-cdn/src/mount.tsx +++ b/apps/widget-cdn/src/mount.tsx @@ -1,54 +1,19 @@ -import { createRoot } from 'react-dom/client'; import Widget, { type WidgetProps } from './Widget'; +import { mountRoot, type MountHandle } from './mountRoot'; /** Handle returned by `mount` for updating props or tearing down. */ -export type WidgetHandle = { - update(props: WidgetProps): void; - destroy(): void; -}; - -// The widget keeps process-global state (API client key, app settings, the -// extended-route registry), so two live roots would cross-contaminate. Track -// live mounts and fail the second one synchronously — clearer than the -// in-tree fallback that LayerswapProvider renders as a backstop. -let liveMounts = 0; +export type WidgetHandle = MountHandle; /** - * Imperative mount entry for non-React hosts. + * Imperative mount entry for non-React hosts — the swap widget. * * Owns its own React root so the host needs no React at all. * Framework-agnostic loaders load this expose (`./mount`) and call it; React * hosts use the `./Widget` expose instead and share the host's React. * - * Only one widget may be live per page: a second `mount()` before the first - * handle's `destroy()` throws. + * Only one widget may be live per page (shared with `./mountDeposit` — see + * `mountRoot`): a second mount before the first handle's `destroy()` throws. */ export default function mount(target: HTMLElement, props: WidgetProps): WidgetHandle { - if (liveMounts > 0) { - throw new Error( - '[layerswap/widget] mount() was called while another widget is live. ' - + 'The widget keeps process-global state, so only one widget root may be mounted per page. ' - + 'Call destroy() on the existing handle first.', - ); - } - // Claim the slot only after createRoot succeeds — a bad `target` throwing here - // must not leave the counter stuck and lock out every future mount. - const root = createRoot(target); - liveMounts++; - root.render(); - let destroyed = false; - return { - update(next: WidgetProps) { - if (destroyed) { - throw new Error('[layerswap/widget] update() called on a destroyed widget handle'); - } - root.render(); - }, - destroy() { - if (destroyed) return; - destroyed = true; - liveMounts--; - root.unmount(); - }, - }; + return mountRoot(Widget, target, props); } diff --git a/apps/widget-cdn/src/mountDeposit.tsx b/apps/widget-cdn/src/mountDeposit.tsx new file mode 100644 index 0000000000..23d9ba7c94 --- /dev/null +++ b/apps/widget-cdn/src/mountDeposit.tsx @@ -0,0 +1,20 @@ +import DepositWidget, { type DepositWidgetProps } from './DepositWidget'; +import { mountRoot, type MountHandle } from './mountRoot'; + +/** Handle returned by `mountDeposit` for updating props or tearing down. */ +export type DepositWidgetHandle = MountHandle; + +/** + * Imperative mount entry for non-React hosts — the deposit widget. + * + * Owns its own React root so the host needs no React at all. + * Framework-agnostic loaders load this expose (`./mountDeposit`) and call it; + * React hosts use the `./DepositWidget` expose instead and share the host's + * React. + * + * Only one widget may be live per page (shared with `./mount` — see + * `mountRoot`): a second mount before the first handle's `destroy()` throws. + */ +export default function mountDeposit(target: HTMLElement, props: DepositWidgetProps): DepositWidgetHandle { + return mountRoot(DepositWidget, target, props); +} diff --git a/apps/widget-cdn/src/mountRoot.tsx b/apps/widget-cdn/src/mountRoot.tsx new file mode 100644 index 0000000000..9ff2f8ba01 --- /dev/null +++ b/apps/widget-cdn/src/mountRoot.tsx @@ -0,0 +1,89 @@ +import { Component, ComponentType, ReactNode, createElement } from 'react'; +import { createRoot } from 'react-dom/client'; + +// The React loader path wraps the widget in remoteWidgetHost's +// WidgetErrorBoundary; this is the equivalent containment for the imperative +// `mount`/`mountDeposit` exposes, where the host has no React tree of its own +// to catch a render-phase crash. On error the widget unrenders (the handle +// stays valid — destroy() still releases the mount slot). +class MountErrorBoundary extends Component<{ children: ReactNode }, { error: unknown }> { + state = { error: null as unknown }; + static getDerivedStateFromError(error: unknown) { + return { error }; + } + componentDidCatch(error: unknown) { + console.error('[layerswap/widget] widget crashed:', error); + } + render() { + return this.state.error ? null : this.props.children; + } +} + +/** Handle returned by the mount exposes for updating props or tearing down. */ +export type MountHandle

= { + update(props: P): void; + destroy(): void; +}; + +// The widget keeps process-global state (API client key, app settings, the +// extended-route registry), so two live roots would cross-contaminate. Track +// live mounts and fail the second one synchronously — clearer than the +// in-tree fallback that LayerswapProvider renders as a backstop. +// +// The counter is shared across ALL mount exposes (`./mount`, `./mountDeposit`): +// a swap widget and a deposit widget sit on the same global state, so only one +// of either kind may be live per page. +let liveMounts = 0; + +/** + * Imperative mount core for non-React hosts, shared by the `./mount` (swap) + * and `./mountDeposit` exposes. Owns its own React root so the host needs no + * React at all; React hosts use the component exposes instead and share the + * host's React. + * + * Only one widget may be live per page: a second mount before the first + * handle's `destroy()` throws. + */ +export function mountRoot

( + Component: ComponentType

, + target: HTMLElement, + props: P, +): MountHandle

{ + if (liveMounts > 0) { + throw new Error( + '[layerswap/widget] mount() was called while another widget is live. ' + + 'The widget keeps process-global state, so only one widget root may be mounted per page. ' + + 'Call destroy() on the existing handle first.', + ); + } + // Claim the slot only after createRoot succeeds — a bad `target` throwing here + // must not leave the counter stuck and lock out every future mount. Same for + // a throwing initial render: release the slot and tear down the root, or the + // caller never gets a handle to destroy() and every future mount is locked out. + const root = createRoot(target); + liveMounts++; + const renderWidget = (p: P) => + root.render(createElement(MountErrorBoundary, null, createElement(Component, p))); + try { + renderWidget(props); + } catch (error) { + liveMounts--; + root.unmount(); + throw error; + } + let destroyed = false; + return { + update(next: P) { + if (destroyed) { + throw new Error('[layerswap/widget] update() called on a destroyed widget handle'); + } + renderWidget(next); + }, + destroy() { + if (destroyed) return; + destroyed = true; + liveMounts--; + root.unmount(); + }, + }; +} diff --git a/apps/widget-cdn/src/useWalletProviders.ts b/apps/widget-cdn/src/useWalletProviders.ts new file mode 100644 index 0000000000..8d3ede5d9b --- /dev/null +++ b/apps/widget-cdn/src/useWalletProviders.ts @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import { + getDefaultProviders, + createEVMProvider, + type DefaultWalletConfig, +} from '@layerswap/wallets'; +import type { WalletProviderId } from '@layerswap/widget-types'; +import type { Config as WagmiConfig } from 'wagmi'; + +export type WalletProvidersConfig = { + include?: Array; + exclude?: Array; +}; + +/** + * Resolve the wallet provider set for a remote widget mount from the shared + * loader-facing props: defaults → include/exclude filtering → host wagmi + * adoption. Shared by the `./Widget` (swap) and `./DepositWidget` exposes so + * the two surfaces resolve providers identically. + */ +export function useWalletProviders( + walletDefaults: DefaultWalletConfig | undefined, + walletProvidersConfig: WalletProvidersConfig | undefined, + wagmiConfig: WagmiConfig | undefined, +): ReturnType { + return useMemo(() => { + let providers = getDefaultProviders(walletDefaults ?? {}); + + // Allowlist — keep only the requested ids. Filtering here (before any + // `loadProvider()` call) means dropped chains never dynamic-import their + // SDK, so an `include` list gets the same lazy-loading win as `exclude`. + const included = walletProvidersConfig?.include; + if (included && included.length > 0) { + const keep = new Set(included); + providers = providers.filter((p) => keep.has(p.id)); + } + + // Blocklist — drop the requested ids. Applied after `include` so the two + // can be combined. + const excluded = walletProvidersConfig?.exclude; + if (excluded && excluded.length > 0) { + const drop = new Set(excluded); + providers = providers.filter((p) => !drop.has(p.id)); + } + + if (wagmiConfig) { + // Replace the eager EVM provider with one that adopts the host's wagmi + // config so the widget tracks the host's account/chain. Only do this + // when EVM is actually in the resolved set — respect the include/exclude + // lists literally rather than force-injecting EVM. + const evmIndex = providers.findIndex((p) => p.id === 'evm'); + if (evmIndex >= 0) { + providers[evmIndex] = createEVMProvider({ + walletConnectConfigs: walletDefaults?.walletConnect, + wagmiConfig, + }); + } + } + + return providers; + }, [wagmiConfig, walletDefaults, walletProvidersConfig]); +} diff --git a/apps/widget-cdn/worker/src/index.ts b/apps/widget-cdn/worker/src/index.ts deleted file mode 100644 index f0602bd394..0000000000 --- a/apps/widget-cdn/worker/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Layerswap widget CDN — Cloudflare Worker edge in front of an R2 bucket. - * - * Storage model (set up by `scripts/deploy-r2.mjs`): - * - * R2 bucket - * ├── 1.5.0-abc123def456/ ← immutable, write-once build (buildId = - * │ │ widget version + git sha, see build-id.mjs) - * │ ├── manifest.json ← signed; describes this exact build - * │ ├── remoteEntry.js - * ├── 1.5.0-fedcba654321/ ← next build, also immutable - * ├── assets/ ← stable, content-addressed namespace shared - * │ └── ..js across builds for browser/R2 cache reuse - * └── channels.json ← the ONLY mutable object: - * { "v1": "1.5.0-abc123def456" } - * - * This Worker does two things: - * - * 1. Rolling channel → 302 redirect. A request for `/v1/manifest.json` - * reads `channels.json`, finds the current buildId for `v1`, and - * redirects to `//manifest.json`. The loader follows the - * redirect and resolves the relative `remoteEntry` against the FINAL - * URL, so the remote anchors at the immutable `//` path. Its - * runtime loads content-hashed chunks from `/assets/`. Rollback = flip - * `channels.json`; it propagates within the redirect's 60s cache window. - * - * 2. Immutable artifact serving. Everything under a build directory is - * served from R2 with `immutable` caching and permissive CORS (the widget - * is fetched cross-origin from integrators' pages, and its chunks load - * with `crossorigin="anonymous"` for SRI). - */ - -interface Env { - BUCKET: R2Bucket; -} - -const CORS_ORIGIN = '*'; -const CHANNELS_KEY = 'channels.json'; - -// Matches a rolling major channel segment: v1, v2, … -const CHANNEL_RE = /^v\d+$/; -// Keep in sync with scripts/build-id.mjs. Build IDs are one safe route -// segment and can never shadow a rolling channel or the shared asset prefix. -const BUILD_ID_RE = /^(?!v\d+$)(?!assets$)[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/; - -const SECURITY_HEADERS: Record = { - 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload', - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', -}; - -const CORS_HEADERS: Record = { - 'Access-Control-Allow-Origin': CORS_ORIGIN, - 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Max-Age': '86400', -}; - -function baseHeaders(extra?: Record): Headers { - const h = new Headers({ ...SECURITY_HEADERS, ...CORS_HEADERS }); - if (extra) for (const [k, v] of Object.entries(extra)) h.set(k, v); - return h; -} - -const CONTENT_TYPES: Record = { - js: 'text/javascript; charset=utf-8', - mjs: 'text/javascript; charset=utf-8', - css: 'text/css; charset=utf-8', - json: 'application/json; charset=utf-8', - map: 'application/json; charset=utf-8', -}; - -function contentTypeFor(key: string): string { - const ext = key.split('.').pop()?.toLowerCase() ?? ''; - return CONTENT_TYPES[ext] ?? 'application/octet-stream'; -} - -export default { - async fetch(request: Request, env: Env): Promise { - if (request.method === 'OPTIONS') { - return new Response(null, { status: 204, headers: baseHeaders() }); - } - if (request.method !== 'GET' && request.method !== 'HEAD') { - return new Response('Method Not Allowed', { status: 405, headers: baseHeaders() }); - } - - const url = new URL(request.url); - const path = url.pathname.replace(/^\/+/, ''); - const segments = path.split('/'); - const first = segments[0] ?? ''; - - // The channel pointer is an internal control file — never serve it. - if (path === CHANNELS_KEY) { - return new Response('Not Found', { status: 404, headers: baseHeaders() }); - } - - // (1) Rolling channel → 302 to the current immutable build. - if (CHANNEL_RE.test(first)) { - const buildId = await currentBuildId(env, first); - if (!buildId) { - return new Response(`Unknown channel: ${first}`, { status: 404, headers: baseHeaders() }); - } - const rest = segments.slice(1).join('/'); // e.g. "manifest.json" - const location = `/${buildId}/${rest}`; - return new Response(null, { - status: 302, - headers: baseHeaders({ - Location: location, - // Short cache so a channel flip (roll-forward or rollback) - // is picked up within a minute. The target it points at is - // immutable, so this is the only thing that ever goes stale. - 'Cache-Control': 'public, max-age=60, must-revalidate', - }), - }); - } - - // (2) Immutable artifact under a build directory. - return serveObject(env, path, request.method === 'HEAD'); - }, -}; - -/** Read `channels.json` and return the current buildId for a major channel. */ -async function currentBuildId(env: Env, channel: string): Promise { - const obj = await env.BUCKET.get(CHANNELS_KEY); - if (!obj) return null; - try { - const channels = (await obj.json()) as Record; - const v = channels[channel]; - return typeof v === 'string' && BUILD_ID_RE.test(v) ? v : null; - } catch { - return null; - } -} - -async function serveObject(env: Env, key: string, headOnly: boolean): Promise { - const object = await env.BUCKET.get(key); - if (!object) { - return new Response('Not Found', { status: 404, headers: baseHeaders() }); - } - - // Build directories are write-once and /assets/ keys are content-hashed, - // so every externally served object is immutable. - const headers = baseHeaders({ - 'Content-Type': object.httpMetadata?.contentType || contentTypeFor(key), - 'Cache-Control': 'public, max-age=31536000, immutable', - ETag: object.httpEtag, - }); - if (object.size != null) headers.set('Content-Length', String(object.size)); - - return new Response(headOnly ? null : object.body, { status: 200, headers }); -} diff --git a/apps/widget-cdn/worker/tsconfig.json b/apps/widget-cdn/worker/tsconfig.json deleted file mode 100644 index a86714df25..0000000000 --- a/apps/widget-cdn/worker/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es2022", - "module": "es2022", - "moduleResolution": "bundler", - "lib": ["es2022"], - "types": ["@cloudflare/workers-types"], - "strict": true, - "noEmit": true, - "skipLibCheck": true, - "esModuleInterop": true - }, - "include": ["src/**/*.ts"] -} diff --git a/apps/widget-cdn/worker/wrangler.toml b/apps/widget-cdn/worker/wrangler.toml deleted file mode 100644 index d24c8d69d6..0000000000 --- a/apps/widget-cdn/worker/wrangler.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "layerswap-widget-cdn" -main = "src/index.ts" -compatibility_date = "2024-09-23" - -# The R2 bucket holding the immutable per-version builds plus the single -# mutable `channels.json` pointer. Create it once with: -# wrangler r2 bucket create layerswap-widget-cdn -[[r2_buckets]] -binding = "BUCKET" -bucket_name = "layerswap-widget-cdn" - -# Serve the CDN from your own domain. Configure the custom domain in the -# Cloudflare dashboard (Workers & Pages → this Worker → Triggers → Custom -# Domains) or uncomment below once the zone is on Cloudflare: -# -# routes = [ -# { pattern = "cdn.layerswap.io", custom_domain = true }, -# ] -# -# Until then the Worker is reachable at its *.workers.dev subdomain. - -[observability] -enabled = true diff --git a/examples/widget-react-host/README.md b/examples/widget-react-host/README.md index 0427d032ac..ee71114b2a 100644 --- a/examples/widget-react-host/README.md +++ b/examples/widget-react-host/README.md @@ -7,23 +7,20 @@ wallet adapters, no `framer-motion`, etc.). ## Run locally -Two dev servers — the CDN-remote and this host app. - ```bash -# Terminal 1 — serve the federated remote on :3100 -pnpm --filter @layerswap/widget-cdn dev - -# Terminal 2 — serve this example on :3001 pnpm --filter widget-react-host-example dev -- --host 127.0.0.1 --port 3001 --no-open ``` -Then open `http://127.0.0.1:3001/`. The host page fetches the manifest at -`http://127.0.0.1:3100/manifest.json` from the local CDN dev-server, then -loads the `remoteEntry.js` it points at and mounts the widget. +Then open `http://127.0.0.1:3001/`. The loader fetches and verifies the manifest +from its built-in production channel, then loads the `remoteEntry.js` it points +at and mounts the widget. The host cannot replace the manifest URL or disable +verification. -Point at a production CDN by setting `VITE_LAYERSWAP_MANIFEST` (e.g. -`https://cdn.layerswap.io/v1/manifest.json`), and `VITE_LAYERSWAP_VERIFY=true` -to require a valid manifest signature. +The page has two tabs: **Swap** mounts `LayerswapWidget` (the full swap form) +and **Deposit** mounts `LayerswapDepositWidget` (the fixed-destination funding +flow, pointed at a demo Base USDC/ETH recipient). They are tabs rather than +side-by-side because only one Layerswap widget may be live per page — the +widget keeps process-global state. ## What's in the host bundle @@ -31,9 +28,3 @@ Only `@layerswap/widget-react` (a few KB plus `@module-federation/runtime`). React, react-dom, wagmi, viem, react-query, and zustand stay as the host's own copies (declared peer-deps on `@layerswap/widget-react`) and are fed into the MF shared scope so the remote uses those exact instances. - -## Known follow-ups (not blockers) - -- The local dev-server manifest is unsigned, so this example runs with - `verify` off by default. Point it at a signed prod build and set - `VITE_LAYERSWAP_VERIFY=true` to exercise signature verification. diff --git a/examples/widget-react-host/package.json b/examples/widget-react-host/package.json index fb580e0789..3444efe322 100644 --- a/examples/widget-react-host/package.json +++ b/examples/widget-react-host/package.json @@ -17,6 +17,7 @@ "wagmi": "catalog:", "zustand": "catalog:" }, + "packageManager": "pnpm@10.20.0", "devDependencies": { "@types/react": "catalog:", "@types/react-dom": "catalog:", @@ -25,4 +26,4 @@ "vite": "^7.3.0", "vite-plugin-node-polyfills": "^0.24.0" } -} +} \ No newline at end of file diff --git a/examples/widget-react-host/src/App.tsx b/examples/widget-react-host/src/App.tsx index c3d43b4d96..48df9e339e 100644 --- a/examples/widget-react-host/src/App.tsx +++ b/examples/widget-react-host/src/App.tsx @@ -1,37 +1,76 @@ +import { useState } from 'react'; import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { LayerswapWidget } from '@layerswap/widget-react'; +import { LayerswapWidget, LayerswapDepositWidget } from '@layerswap/widget-react'; +import type { LayerswapWidgetProps, LayerswapDepositWidgetProps } from '@layerswap/widget-react'; import { wagmiConfig } from './wagmi'; import { HostWallet } from './HostWallet'; -// Integrators do NOT configure the widget's source — it is always the -// canonical signed Layerswap CDN baked into `@layerswap/widget-js`. This dev -// harness is the exception: it points the loader at the local widget-cdn dev -// server (`pnpm dev` in apps/widget-cdn) via the internal `__LAYERSWAP_WIDGET_*` -// override globals. These are an undocumented build/test seam, not a public API. -// -// Defaults to the local dev server; set VITE_LAYERSWAP_MANIFEST to a production -// URL (e.g. https://cdn.layerswap.io/v1/manifest.json). The dev server emits an -// unsigned manifest, so verification is off unless VITE_LAYERSWAP_VERIFY=true. -const MANIFEST_URL = - import.meta.env.VITE_LAYERSWAP_MANIFEST ?? 'http://127.0.0.1:3100/manifest.json'; -const VERIFY = import.meta.env.VITE_LAYERSWAP_VERIFY === 'true'; +const queryClient = new QueryClient(); -declare global { - interface Window { - __LAYERSWAP_WIDGET_MANIFEST__?: string; - __LAYERSWAP_WIDGET_VERIFY__?: boolean; - } -} +// Demo recipient for the deposit tab. A real integrator supplies their own +// deposit address here — the deposit widget never asks the end user for it. +const DEPOSIT_DEMO_ADDRESS = '0x2fc617e933a52713247ce25730f6695920b3befe'; -// Set before mounts (module scope runs first), so the loader -// reads them when it resolves the source. -window.__LAYERSWAP_WIDGET_MANIFEST__ = MANIFEST_URL; -window.__LAYERSWAP_WIDGET_VERIFY__ = VERIFY; +// Keep object/function props referentially stable — the widget memoizes its +// wallet-provider setup on these by identity, so fresh inline literals on +// every host render would needlessly rebuild wallet providers. Hoist them +// (or useMemo/useCallback them if they depend on host state). +const WIDGET_CONFIG: LayerswapWidgetProps['config'] = { version: 'mainnet' }; +const WALLET_PROVIDERS_CONFIG: LayerswapWidgetProps['walletProvidersConfig'] = { + exclude: ['tron', 'fuel'], +}; +const DEPOSIT_DESTINATION: LayerswapDepositWidgetProps['destination'] = { + network: 'BASE_MAINNET', + tokens: ['USDC', 'ETH'], +}; +const SWAP_CALLBACKS: LayerswapWidgetProps['callbacks'] = { + onSwapCreate: (swap) => console.log('[host] swap created', swap), + onSwapComplete: (swap) => console.log('[host] swap complete', swap), + onError: (err) => console.warn('[host] widget error', err), + onSwapModalStateChange: (open) => console.log('[host] swap modal', open), +}; +const DEPOSIT_CALLBACKS: LayerswapDepositWidgetProps['callbacks'] = { + onSwapCreate: (swap) => console.log('[host] deposit swap created', swap), + onSwapComplete: (swap) => console.log('[host] deposit complete', swap), + onError: (err) => console.warn('[host] deposit widget error', err), +}; +const onWidgetReady = () => console.log('[embed] widget ready'); +const onWidgetLoadError = (err: unknown) => console.error('[embed] failed to load', err); +const onDepositWidgetReady = () => console.log('[embed] deposit widget ready'); +const onDepositWidgetLoadError = (err: unknown) => console.error('[embed] failed to load', err); -const queryClient = new QueryClient(); +type WidgetTab = 'swap' | 'deposit'; + +const loadingFallback = ( +

+ Loading widget… +
+); export function App() { + // Only one Layerswap widget (of either kind) may be live per page — the + // widget keeps process-global state. Tabs swap between them rather than + // rendering both. + const [tab, setTab] = useState('swap'); + + const tabButton = (value: WidgetTab, label: string) => ( + + ); + return ( @@ -50,30 +89,39 @@ export function App() {

The host page below mounts its own WagmiProvider and a - connect button. The Layerswap widget is fetched at runtime via the - manifest at {MANIFEST_URL}. Connect the host wallet and + connect button. The Layerswap widget is fetched from the signed CDN + configured inside the loader package. Connect the host wallet and compare its account against what the widget sees.

+
+ {tabButton('swap', 'Swap')} + {tabButton('deposit', 'Deposit')} +
- console.log('[host] swap created', swap), - onSwapComplete: (swap) => console.log('[host] swap complete', swap), - onError: (err) => console.warn('[host] widget error', err), - onSwapModalStateChange: (open) => console.log('[host] swap modal', open), - }} - fallback={ -
- Loading widget… -
- } - onReady={() => console.log('[embed] widget ready')} - onError={(err) => console.error('[embed] failed to load', err)} - /> + {tab === 'swap' ? ( + + ) : ( + + )}
diff --git a/examples/widget-react-host/src/wagmi.ts b/examples/widget-react-host/src/wagmi.ts index 5a50210580..5a010f29d5 100644 --- a/examples/widget-react-host/src/wagmi.ts +++ b/examples/widget-react-host/src/wagmi.ts @@ -1,6 +1,10 @@ import { http, createConfig } from 'wagmi'; import { mainnet } from 'wagmi/chains'; +// Only the chains the HOST app itself uses need to be declared here. The +// widget appends every Layerswap-supported EVM chain (and transports) to +// this config when it adopts it, so in-widget network switching and +// transfers work on chains not listed below. export const wagmiConfig = createConfig({ chains: [mainnet], // Rely on wagmi's default EIP-6963 multi-injected provider discovery. The diff --git a/packages/wallets/adapters/evm/src/EVMProvider/init.ts b/packages/wallets/adapters/evm/src/EVMProvider/init.ts index 276d7253a4..7333232d87 100644 --- a/packages/wallets/adapters/evm/src/EVMProvider/init.ts +++ b/packages/wallets/adapters/evm/src/EVMProvider/init.ts @@ -1,4 +1,5 @@ import type { Config } from '@wagmi/core' +import type { Chain, Transport } from 'viem' import type { AppNetworkAdapter } from "@layerswap/wallet-core" import { getEvmChainsConfig } from '../evmUtils/chainConfigs' import { buildEVMConnectors } from './Connectors' @@ -23,6 +24,37 @@ type InitOptions = { let _initialized = false +/** + * Extend the live wagmi config with any Layerswap EVM chains it is missing. + * + * Host apps that hand the widget an external config typically register only + * the chains their own app uses (often just mainnet), but wagmi connectors + * refuse to switch/send on a chain absent from `config.chains` + * (ChainNotConfiguredError) — so without this, "Switch network" fails for + * every Layerswap network the host didn't list. Layerswap chains are appended + * after the host's own via wagmi's `_internal.chains` store, so the host's + * chain order (and default chain) is preserved; transports are added only for + * chains the host didn't configure. `getClient` resolves chains and + * transports at call time, so appended entries are picked up. Idempotent. + */ +function syncLayerswapChains(config: Config, networks: Network[], networkAdapter: AppNetworkAdapter): void { + const { chains, transports } = getEvmChainsConfig(networks, networkAdapter) + const existingIds = new Set(config.chains.map(c => c.id)) + const additions = chains.filter(c => !existingIds.has(c.id)) + if (additions.length === 0) return + config._internal.chains.setState(current => [...current, ...additions] as [Chain, ...Chain[]]) + // Undefined when the host built the config with `client` instead of + // `transports` — client-based configs don't need per-chain transports. + const configTransports = config._internal.transports as Record | undefined + if (configTransports) { + for (const chain of additions) { + if (!(chain.id in configTransports) && transports[chain.id]) { + configTransports[chain.id] = transports[chain.id] + } + } + } +} + /** * One-shot initialization of the EVM wagmi config and store sync. Safe to * call multiple times — subsequent calls are no-ops. @@ -34,7 +66,10 @@ export function initEvmProvider(opts: InitOptions): void { if (_initialized) { // Never drop a host config silently: provideExternalEvmConfig warns // when a different config is already live. - if (externalWagmiConfig) provideExternalEvmConfig(externalWagmiConfig) + if (externalWagmiConfig) { + provideExternalEvmConfig(externalWagmiConfig) + syncLayerswapChains(getEvmConfig(), networks, networkAdapter) + } return } @@ -45,6 +80,7 @@ export function initEvmProvider(opts: InitOptions): void { // to whichever config is actually live so there is exactly one // synced wagmi state. provideExternalEvmConfig(externalWagmiConfig) + syncLayerswapChains(getEvmConfig(), networks, networkAdapter) attachWagmiSync(getEvmConfig()) _initialized = true return diff --git a/packages/wallets/adapters/evm/src/rpcHealthCheckProvider.ts b/packages/wallets/adapters/evm/src/rpcHealthCheckProvider.ts index ea4b515c8e..6c501a9f16 100644 --- a/packages/wallets/adapters/evm/src/rpcHealthCheckProvider.ts +++ b/packages/wallets/adapters/evm/src/rpcHealthCheckProvider.ts @@ -56,6 +56,7 @@ function createStore(): RpcHealthCheckStore { const listeners = new Set<() => void>() let lastConnectorId: string | undefined let lastIsConnected = false + let lastChainId: number | undefined let unsubEvm: (() => void) | null = null const setSnapshot = (next: Partial) => { @@ -70,10 +71,23 @@ function createStore(): RpcHealthCheckStore { return { connector, isConnected, chainId: state.wagmiAccount.chainId } } + // Monotonic probe generation. Comparing connector/chain ids can't tell two + // in-flight probes for the *same* chain apart (a fast A→B→A flip-flop), so + // an older probe resolving late could overwrite a fresher verdict. Every + // context change and every newly started probe bumps the generation; a + // probe's verdict counts only while its generation is still the latest. + let probeGeneration = 0 + const check = async () => { const { connector, isConnected } = getActiveConnector() if (!connector || !isConnected) return + // Claim a generation synchronously, before any await — any context change + // (chain switch, disconnect) or a fresher probe starting invalidates this + // one, even if this one happens to resolve later. + const myGeneration = ++probeGeneration + const isCurrent = () => probeGeneration === myGeneration + try { const provider = (await connector.getProvider()) as Eip1193Provider | null if (!provider || typeof provider.request !== 'function') return @@ -88,6 +102,7 @@ function createStore(): RpcHealthCheckStore { 'Wallet RPC timed out', ) const latencyMs = performance.now() - start + if (!isCurrent()) return const tsHex = latestBlock?.timestamp const blockAgeSec = tsHex != null @@ -106,6 +121,7 @@ function createStore(): RpcHealthCheckStore { } setSnapshot({ health: { status: 'healthy', latencyMs, blockAgeSec } satisfies RpcHealth }) } catch (e: any) { + if (!isCurrent()) return // A wallet declining to serve the read method isn't an RPC health signal — // leave status "unknown" so we don't prompt the user to add an RPC. if (isMethodUnsupportedError(e)) { @@ -155,7 +171,7 @@ function createStore(): RpcHealthCheckStore { }) } - // Auto-check when the active connector or connectedness changes. The + // Auto-check when the active connector, connectedness, or chain changes. The // upstream subscription only lives while someone is listening (first // subscriber starts it, last unsubscriber stops it), so consumer // mount/unmount cycles — including StrictMode's — can't leave the store @@ -163,16 +179,27 @@ function createStore(): RpcHealthCheckStore { // also catch up: if a wallet is already connected, check right away. const startAutoCheck = () => { unsubEvm = useEvmStore.subscribe(() => { - const { connector, isConnected } = getActiveConnector() + const { connector, isConnected, chainId } = getActiveConnector() const connectorId = connector?.id - if (connectorId === lastConnectorId && isConnected === lastIsConnected) return + if (connectorId === lastConnectorId && isConnected === lastIsConnected && chainId === lastChainId) return lastConnectorId = connectorId lastIsConnected = isConnected + lastChainId = chainId + // The previous verdict belongs to the old connector/chain — invalidate + // any probe still in flight and drop back to "unknown" (banner hidden) + // until the fresh probe for this chain resolves. + probeGeneration++ + setSnapshot({ health: { status: undefined } satisfies RpcHealth }) if (connector && isConnected) void check() }) - const { connector, isConnected } = getActiveConnector() + const { connector, isConnected, chainId } = getActiveConnector() + if (connector?.id !== lastConnectorId || chainId !== lastChainId) { + probeGeneration++ + setSnapshot({ health: { status: undefined } satisfies RpcHealth }) + } lastConnectorId = connector?.id lastIsConnected = isConnected + lastChainId = chainId if (connector && isConnected) void check() } diff --git a/packages/wallets/adapters/evm/src/service/EvmConnectionService.ts b/packages/wallets/adapters/evm/src/service/EvmConnectionService.ts index 05afe468dc..a460ef2a25 100644 --- a/packages/wallets/adapters/evm/src/service/EvmConnectionService.ts +++ b/packages/wallets/adapters/evm/src/service/EvmConnectionService.ts @@ -2,12 +2,13 @@ import { type InternalConnector, type Wallet } from '@layerswap/widget-types'; import type { RequestAdditionalConnectorsParams, RequestAdditionalConnectorsResult, WalletConnectionService } from "@layerswap/wallet-core/types"; import type { WalletModalConnector } from "@layerswap/wallet-core/types" import type { Connector } from 'wagmi' +import { numberToHex, type Chain } from 'viem' import { connect, disconnect, getConnections, switchAccount as wagmiSwitchAccount, type Connection, } from '@wagmi/core' import { buildDeepLink, clearPendingDynamicWcMetadata, isWalletConnectRegistryConnector, mapConnectError, setDynamicWcMetadata, setPendingMetadataForRegistry, subscribeDisplayUri, type AppNetworkAdapter, type WalletConnectWalletBase } from "@layerswap/wallet-core" import { evmConnectorNameResolver, resolveEVMWalletConnectorIcon } from '../evmUtils' import { name as PROVIDER_NAME, HIDDEN_WALLETCONNECT_ID } from '../constants' import type { LSConnector } from '../connectors/types' -import { getEvmConfig } from './getEvmConfig' +import { getEvmConfig, isExternalEvmConfig } from './getEvmConfig' import { computeEvmNetworkBuckets, type EvmAdditionalSupportedNetworks, type EvmNetworkBuckets } from './networkBuckets' import { resolveSupportedNetworks } from './resolveSupportedNetworks' import { resolveWallet } from './resolveWallet' @@ -213,8 +214,24 @@ export class EvmConnectionService implements WalletConnectionService { const connector = this.resolveWalletConnector(wallet) if (!connector) throw new Error('Connector not found') + const id = Number(chainId) + + // wagmi connectors validate the target against a snapshot of + // `config.chains` captured at connector setup. With an adopted host + // config, the host created its connectors before syncLayerswapChains + // appended the Layerswap chains, so that snapshot is stale and + // connector.switchChain throws ChainNotConfiguredError even though + // the wallet can switch. Drive the EIP-1193 provider directly instead + // — wagmi still picks up the resulting `chainChanged` event. + if (isExternalEvmConfig()) { + const chain = getEvmConfig().chains.find(c => c.id === id) + if (!chain) throw new Error(`Chain ${id} is not configured`) + await switchChainViaEip1193(connector, chain) + return + } + if (connector.switchChain) { - await connector.switchChain({ chainId: Number(chainId) }) + await connector.switchChain({ chainId: id }) } else { throw new Error('Switch chain method is not available on the connector') } @@ -381,3 +398,44 @@ export class EvmConnectionService implements WalletConnectionService +} + +/** + * Chain switching over the raw EIP-1193 provider, mirroring what wagmi's + * injected connector does after its (snapshot-based) chain lookup: + * `wallet_switchEthereumChain`, falling back to `wallet_addEthereumChain` + * when the wallet doesn't know the chain (error 4902), then switching again + * for wallets that don't auto-switch after adding. + */ +async function switchChainViaEip1193(connector: Connector, chain: Chain): Promise { + const provider = await connector.getProvider() as Eip1193Provider | undefined + if (!provider?.request) throw new Error('Connector provider is not available') + const hexChainId = numberToHex(chain.id) + const requestSwitch = () => provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: hexChainId }], + }) + try { + await requestSwitch() + } catch (error) { + const code = (error as { code?: number })?.code + ?? (error as { data?: { originalError?: { code?: number } } })?.data?.originalError?.code + if (code !== 4902) throw error + await provider.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: hexChainId, + chainName: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: chain.rpcUrls?.default?.http ?? [], + blockExplorerUrls: chain.blockExplorers?.default ? [chain.blockExplorers.default.url] : undefined, + }], + }) + await requestSwitch() + } +} + +export const evmConnectionService = new EvmConnectionService() diff --git a/packages/widget/core/src/components/Pages/Deposit/Wallet/ProcessingStep.tsx b/packages/widget/core/src/components/Pages/Deposit/Wallet/ProcessingStep.tsx index ae4caadeff..5695ba91d8 100644 --- a/packages/widget/core/src/components/Pages/Deposit/Wallet/ProcessingStep.tsx +++ b/packages/widget/core/src/components/Pages/Deposit/Wallet/ProcessingStep.tsx @@ -1,9 +1,14 @@ -import { FC } from "react"; -import SwapDetails from "@/components/Pages/Swap/Withdraw/SwapDetails"; +import { FC, Suspense, lazy } from "react"; +import { SwapDetailsSceleton } from "@/components/Common/Sceletons"; import { Partner } from "@/Models/Partner"; import { useDepositStep, useReportCloseLock } from "../depositStepContext"; import { useResolvedSwapStatus } from "@/hooks/useResolvedSwapStatus"; +// SwapDetails transitively imports the whole Swap-processing subtree; loading +// it lazily (as FormWrapper already does) keeps it out of the Deposit exposes' +// sync bundle, which is nearly at the CDN bundle-budget gate. +const SwapDetails = lazy(() => import("@/components/Pages/Swap/Withdraw/SwapDetails")); + type Props = { partner?: Partner; }; @@ -26,11 +31,13 @@ const ProcessingStep: FC = ({ partner }) => { return (
- + }> + +
); }; diff --git a/packages/widget/core/src/components/Pages/Swap/Withdraw/Wallet/Common/buttons.tsx b/packages/widget/core/src/components/Pages/Swap/Withdraw/Wallet/Common/buttons.tsx index 0e2c423ee3..dce188d702 100644 --- a/packages/widget/core/src/components/Pages/Swap/Withdraw/Wallet/Common/buttons.tsx +++ b/packages/widget/core/src/components/Pages/Swap/Withdraw/Wallet/Common/buttons.tsx @@ -89,10 +89,14 @@ export const ChangeNetworkMessage: FC<{ data: ActionData, network: string }> = ( /> } else if (data.isError) { + const error = data.error as (Error & { shortMessage?: string, cause?: { shortMessage?: string } }) | null + const reason = error?.cause?.shortMessage ?? error?.shortMessage return } } diff --git a/packages/widget/js/package.json b/packages/widget/js/package.json index c4b820779d..4c6e0d22e2 100644 --- a/packages/widget/js/package.json +++ b/packages/widget/js/package.json @@ -1,7 +1,7 @@ { "name": "@layerswap/widget-js", + "version": "1.0.0", "private": true, - "version": "0.1.0", "description": "Framework-agnostic loader that fetches the Layerswap widget from the CDN at runtime via Module Federation and mounts it into any DOM element. No framework required.", "type": "module", "main": "dist/esm/index.js", @@ -25,6 +25,7 @@ "build": "pnpm clean && pnpm build:esm+types", "build:esm+types": "tsc --project tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types && tsc-alias -p tsconfig.json --outDir ./dist/esm && tsc-alias -p tsconfig.json --outDir ./dist/types", "check:types": "tsc --noEmit", + "prepublishOnly": "node scripts/assert-production-origin.mjs", "test": "pnpm build && node --test tests/*.test.mjs", "clean": "rimraf dist tsconfig.tsbuildinfo", "dev": "tsc -p tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types --watch" @@ -46,7 +47,7 @@ ], "license": "MIT", "dependencies": { - "@layerswap/widget-types": "workspace:^", + "@layerswap/widget-types": "workspace:*", "@module-federation/runtime": "^0.9.0" }, "engines": { diff --git a/packages/widget/js/scripts/assert-production-origin.mjs b/packages/widget/js/scripts/assert-production-origin.mjs new file mode 100644 index 0000000000..8154bc3410 --- /dev/null +++ b/packages/widget/js/scripts/assert-production-origin.mjs @@ -0,0 +1,27 @@ +// prepublishOnly gate shared by @layerswap/widget-js and @layerswap/widget-react +// (react ships the loader via its widget-js dependency, so both publishes are +// poisoned by a sandbox origin). +// +// WIDGET_MANIFEST_URL is deliberately hardcoded — integrators must not be able +// to repoint the widget's trust anchor. The flip side is that publishing to npm +// with the sandbox storage account still baked in would pin every real +// integrator to a personal Azure account. This check makes that publish +// impossible instead of merely documented. +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const SANDBOX_MARKER = 'layerswapcdntest'; +const manifestPath = fileURLToPath(new URL('../src/manifest.ts', import.meta.url)); +const source = readFileSync(manifestPath, 'utf8'); + +if (source.includes(SANDBOX_MARKER)) { + console.error( + `[assert-production-origin] refusing to publish: packages/widget/js/src/manifest.ts still ` + + `references the sandbox storage account ("${SANDBOX_MARKER}").\n` + + `Point WIDGET_MANIFEST_URL at the production CDN origin (and bake in the matching ` + + `production verify key) before publishing @layerswap/widget-js or @layerswap/widget-react.`, + ); + process.exit(1); +} + +console.log('[assert-production-origin] OK — no sandbox origin in the loader source'); diff --git a/packages/widget/js/src/index.ts b/packages/widget/js/src/index.ts index 1c44d0afb1..6941c68049 100644 --- a/packages/widget/js/src/index.ts +++ b/packages/widget/js/src/index.ts @@ -1,6 +1,6 @@ // Vanilla, framework-agnostic mount API — the primary entry point. -export { mountWidget } from './mount.js'; -export type { WidgetHandle, MountOptions } from './mount.js'; +export { mountWidget, mountDepositWidget } from "./mount.js"; +export type { WidgetHandle, DepositWidgetHandle, MountOptions } from "./mount.js"; // Shared widget prop/config/theme contract, re-exported from the // `@layerswap/widget-types` package (the single source of truth). @@ -10,30 +10,35 @@ export type { WidgetCallbacks, WalletDefaults, WalletProviderId, + DepositWidgetProps, + DepositConfig, + DepositMethodId, + SupportedDestination, ThemeData, ThemeColor, -} from '@layerswap/widget-types'; +} from "@layerswap/widget-types"; +export { WIDGET_PROTOCOL_MAJOR } from "@layerswap/widget-types"; // Lower-level building blocks, reused by `@layerswap/widget-react` and // available for advanced integrators wiring their own loader. -export { resolveSource } from './loader.js'; -export type { ResolvedSource } from './loader.js'; -export { initRemote, loadRemoteModule } from './runtime.js'; -export type { SharedLib } from './runtime.js'; +export { resolveSource } from "./loader.js"; +export type { ResolvedSource } from "./loader.js"; +export { initRemote, loadRemoteModule } from "./runtime.js"; +export type { SharedLib } from "./runtime.js"; // Manifest format + verification primitives. export { ManifestError, MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64, MANIFEST_CLOCK_SKEW_MS, - DEFAULT_MANIFEST_URL, + WIDGET_MANIFEST_URL, verifyManifest, manifestFreshness, canonicalize, fetchManifest, resolveRemoteEntry, -} from './manifest.js'; -export type { Manifest, FetchedManifest } from './manifest.js'; +} from "./manifest.js"; +export type { Manifest, FetchedManifest } from "./manifest.js"; // Runtime SRI interceptor. -export { registerChunkHashes } from './sri.js'; +export { registerChunkHashes } from "./sri.js"; diff --git a/packages/widget/js/src/loader.ts b/packages/widget/js/src/loader.ts index 14c7ce839b..4a5bdfeeff 100644 --- a/packages/widget/js/src/loader.ts +++ b/packages/widget/js/src/loader.ts @@ -1,44 +1,17 @@ -import { fetchManifest, resolveRemoteEntry, verifyManifest, manifestFreshness, ManifestError, DEFAULT_MANIFEST_URL } from './manifest.js'; +import { fetchManifest, resolveRemoteEntry, verifyManifest, manifestFreshness, ManifestError, WIDGET_MANIFEST_URL } from './manifest.js'; import { registerChunkHashes } from './sri.js'; +import { WIDGET_PROTOCOL_MAJOR, widgetProtocolMajorOf } from '@layerswap/widget-types'; export type ResolvedSource = { remoteEntry: string }; -/** - * Internal-only override, read from `globalThis`. NOT part of the public API: - * integrators always get the canonical signed CDN baked into this package - * ({@link DEFAULT_MANIFEST_URL}) and cannot repoint the loader. Layerswap's own - * dev harnesses (the example host, the playground) set these globals before the - * widget mounts to target the local unsigned dev server. Undocumented on - * purpose — treat it as a build/test seam, not a supported knob. - * - * globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'http://127.0.0.1:3100/manifest.json'; - * globalThis.__LAYERSWAP_WIDGET_VERIFY__ = false; - */ -type InternalOverrideGlobals = { - __LAYERSWAP_WIDGET_MANIFEST__?: unknown; - __LAYERSWAP_WIDGET_VERIFY__?: unknown; -}; - -function resolveConfig(): { manifestUrl: string; verify: boolean } { - const g = globalThis as InternalOverrideGlobals; - const manifestUrl = - typeof g.__LAYERSWAP_WIDGET_MANIFEST__ === 'string' && g.__LAYERSWAP_WIDGET_MANIFEST__ - ? g.__LAYERSWAP_WIDGET_MANIFEST__ - : DEFAULT_MANIFEST_URL; - // Fail closed: verification is on unless a harness explicitly disables it. - const verify = typeof g.__LAYERSWAP_WIDGET_VERIFY__ === 'boolean' ? g.__LAYERSWAP_WIDGET_VERIFY__ : true; - return { manifestUrl, verify }; -} - /** * Fetch + validate the manifest and install per-chunk SRI, returning the * resolved remoteEntry URL. Framework-agnostic — shared by the vanilla * `mountWidget` and the React `LayerswapWidget` so the security-critical path * (signature check + SRI registration) lives in exactly one place. * - * Takes no arguments: the manifest URL is the canonical Layerswap CDN baked - * into this package. (Layerswap's own dev harnesses can repoint it via the - * internal `__LAYERSWAP_WIDGET_*` globals — see {@link resolveConfig}.) + * Takes no arguments: the manifest URL and verification policy are fixed in + * this package. Hosts cannot repoint the loader or disable verification. * * Single-flight: concurrent mounts share one fetch + signature verification + * SRI registration. A successful resolution is reused for a short window @@ -47,59 +20,61 @@ function resolveConfig(): { manifestUrl: string; verify: boolean } { * Failures are never cached. */ export function resolveSource(): Promise { - const { manifestUrl, verify } = resolveConfig(); - const key = `${verify ? 'v' : 'u'}:${manifestUrl}`; const now = Date.now(); - const cached = pendingResolves.get(key); + const cached = pendingResolve; if (cached && (cached.settledAt === undefined || now - cached.settledAt < RESOLVE_REUSE_MS)) { return cached.promise; } const entry: PendingResolve = { - promise: resolveSourceOnce(manifestUrl, verify).then( + promise: resolveSourceOnce().then( (result) => { entry.settledAt = Date.now(); return result; }, (error) => { - pendingResolves.delete(key); + if (pendingResolve === entry) pendingResolve = undefined; throw error; }, ), }; - pendingResolves.set(key, entry); + pendingResolve = entry; return entry.promise; } const RESOLVE_REUSE_MS = 60_000; type PendingResolve = { promise: Promise; settledAt?: number }; -const pendingResolves = new Map(); +let pendingResolve: PendingResolve | undefined; -async function resolveSourceOnce(manifestUrl: string, verify: boolean): Promise { - // When verifying, force a revalidation so we check the freshest bytes. - // Otherwise let the browser HTTP cache satisfy repeated mounts. - const { manifest, url: resolvedManifestUrl } = await fetchManifest(manifestUrl, !verify); +async function resolveSourceOnce(): Promise { + // Force a revalidation so we always verify the freshest manifest bytes. + const { manifest, url: resolvedManifestUrl } = await fetchManifest(WIDGET_MANIFEST_URL); if (manifest.killSwitch) { throw new ManifestError('kill-switch', 'manifest kill switch is set — refusing to load remote'); } - if (verify) { - const ok = await verifyManifest(manifest); - if (!ok) { - throw new ManifestError('signature', 'manifest signature is missing or invalid'); - } - // Freshness is only meaningful once the signed body is trusted (an - // attacker controls unverified fields anyway) — and it is REQUIRED then: - // a valid-but-stale manifest is exactly the replay this check exists to - // stop. See `Manifest.expiresAt` for the availability policy. - const freshness = manifestFreshness(manifest, Date.now()); - if (freshness !== 'fresh') { - throw new ManifestError( - 'stale', - freshness === 'expired' - ? `manifest expired at ${manifest.expiresAt} — refusing a possibly replayed build` - : 'manifest carries no valid expiresAt — refusing to trust it indefinitely', - ); - } + const ok = await verifyManifest(manifest); + if (!ok) { + throw new ManifestError('signature', 'manifest signature is missing or invalid'); + } + // Freshness is only meaningful once the signed body is trusted (an + // attacker controls unverified fields anyway) — and it is REQUIRED then: + // a valid-but-stale manifest is exactly the replay this check exists to + // stop. See `Manifest.expiresAt` for the availability policy. + const freshness = manifestFreshness(manifest, Date.now()); + if (freshness !== 'fresh') { + throw new ManifestError( + 'stale', + freshness === 'expired' + ? `manifest expired at ${manifest.expiresAt} — refusing a possibly replayed build` + : 'manifest carries no valid expiresAt — refusing to trust it indefinitely', + ); + } + const remoteProtocolMajor = widgetProtocolMajorOf(manifest); + if (remoteProtocolMajor !== WIDGET_PROTOCOL_MAJOR) { + throw new ManifestError( + 'incompatible', + `widget protocol v${String(remoteProtocolMajor)} is incompatible with loader v${WIDGET_PROTOCOL_MAJOR}`, + ); } // Identify the build in the console — version, commit, and build time from // the (now-validated) manifest. Answers "which build is this page actually diff --git a/packages/widget/js/src/manifest.ts b/packages/widget/js/src/manifest.ts index c4edde8a92..380bda4844 100644 --- a/packages/widget/js/src/manifest.ts +++ b/packages/widget/js/src/manifest.ts @@ -1,3 +1,5 @@ +import { WIDGET_PROTOCOL_MAJOR } from '@layerswap/widget-types'; + /** * Manifest format published at `//manifest.json` by the CDN. * @@ -11,12 +13,12 @@ * of the manifest with `signature` itself set to `null`. See `verifyManifest`. */ export type Manifest = { - /** Semver of the build this manifest describes (the `@layerswap/widget` version). */ + /** Loader/remote compatibility boundary. Must match WIDGET_PROTOCOL_MAJOR. */ + protocolMajor: number; + /** Implementation semver of the `@layerswap/widget` package in this build. */ version: string; /** - * Major channel this build belongs to, e.g. `"v1"`. Informational — the - * loader fetches whatever manifest URL it's given; this field lets tooling - * and humans see which compatibility channel a pinned build came from. + * Major protocol channel this build belongs to, e.g. `"v1"`. */ channel?: string; /** @@ -35,7 +37,7 @@ export type Manifest = { * ISO-8601 end of the manifest's validity window. Replay protection: a * signature and SRI prove authenticity and byte integrity but not * freshness — without an enforced expiry, an attacker who can replay - * CDN/R2 responses could serve an older valid build indefinitely, + * CDN/storage responses could serve an older valid build indefinitely, * reviving a fixed vulnerability or bypassing a newer kill switch. * * Availability policy (explicit, so an outage is never a reason to accept @@ -52,11 +54,11 @@ export type Manifest = { /** * Absolute or manifest-relative URL to the remoteEntry.js. * - * Kept origin-relative (`"./remoteEntry.js"`) so the same signed bytes work - * whether the manifest is fetched directly at its immutable version path - * (`/1.5.0/manifest.json`) or reached via a rolling-channel redirect - * (`/v1/manifest.json` → 302 → `/1.5.0/manifest.json`). The loader resolves - * it against the manifest's FINAL (post-redirect) URL — see `resolveSource`. + * Production builds use a build-addressed relative URL such as + * `"../1.5.0-abc123def456/remoteEntry.js"`. The same signed bytes therefore + * work at the immutable path (`//manifest.json`), behind the + * existing rolling-channel redirect, or copied directly to + * `/v1/manifest.json` by Azure promotion. */ remoteEntry: string; /** @@ -90,12 +92,12 @@ export type Manifest = { * Rotating this key requires a version bump of `@layerswap/widget-js`; * integrators pin it transitively via npm SRI. * - * Current key: generated 2026-06 (pre-KMS). Before a 1.0 release, regenerate - * in a KMS/HSM and update this constant plus the GitHub secret - * `LAYERSWAP_PRIVATE_KEY_PEM`. + * Current sandbox key: rotated 2026-08 (pre-KMS). A future production + * KMS/HSM migration must + * update this constant plus the GitHub secret `LAYERSWAP_PRIVATE_KEY_PEM`. */ export const MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64 = - 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESuHFHbltz/hfcY+DzIrLq7Ixc4efHE8SLZdNg0pZZDHTfdwbqLpGk4461EgNranHLWnVsoAbyQ4IyHIVnRAVKw=='; + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY9/zg6ZUU8ZVOAbuS4qqreIR6/U8BDqY+8giwi8xFYl5dllwzE0x//l2iVRgo3fr6nRpplm9RUYRKcXaiJC/Lg=='; /** * Canonical Layerswap CDN manifest URL — the fixed source the loaders always @@ -103,20 +105,18 @@ export const MANIFEST_VERIFY_PUBLIC_KEY_SPKI_B64 = * reads it directly. Points at the rolling `v1` channel, so hosts auto-receive * forward-compatible widget updates without a redeploy. * - * Currently the Cloudflare Worker's `*.workers.dev` subdomain. When the - * `cdn.layerswap.io` custom domain is wired in `apps/widget-cdn/worker/ - * wrangler.toml`, update this constant to `https://cdn.layerswap.io/v1/ - * manifest.json` and publish a new `@layerswap/widget-js` — integrators pick - * up the new origin transitively via npm. + * Currently the Azure Blob Storage endpoint. When a custom domain such as + * `cdn.layerswap.io` fronts the storage account (e.g. via Azure Front Door), + * update this constant to `https://cdn.layerswap.io/v1/manifest.json` and + * publish a new `@layerswap/widget-js` — integrators pick up the new origin + * transitively via npm. * - * The major (`/v1/`) is pinned to this package's major version: when Layerswap - * cuts a breaking `/v2/`, it ships a new loader major whose default points - * there. There is no per-call override — pinning an exact build means - * installing an older package version. (Layerswap's own dev harnesses can - * repoint the loader at a local server via the internal `__LAYERSWAP_WIDGET_*` - * globals — see `resolveSource` in `loader.ts`.) + * The channel major is the shared widget protocol major. A breaking `/v2/` + * ships with loader package major 2. Exact build pinning is intentionally not + * a public loader feature. */ -export const DEFAULT_MANIFEST_URL = 'https://layerswap-widget-cdn.layerswapcdn.workers.dev/v1/manifest.json'; +export const WIDGET_MANIFEST_URL = + `https://layerswapcdntest.blob.core.windows.net/widget-cdn/v${WIDGET_PROTOCOL_MAJOR}/manifest.json`; const fromB64 = (b64: string): ArrayBuffer => { const bin = atob(b64); @@ -214,7 +214,10 @@ export function resolveRemoteEntry(manifestUrl: string, remoteEntry: string): st } export class ManifestError extends Error { - constructor(public readonly reason: 'fetch' | 'parse' | 'signature' | 'kill-switch' | 'stale', message: string) { + constructor( + public readonly reason: 'fetch' | 'parse' | 'signature' | 'kill-switch' | 'stale' | 'incompatible', + message: string, + ) { super(message); this.name = 'ManifestError'; } @@ -243,10 +246,9 @@ export type FetchedManifest = { manifest: Manifest; /** * The FINAL URL the manifest was served from, after any HTTP redirects. - * When a rolling channel (`/v1/manifest.json`) 302-redirects to an - * immutable build (`/1.5.0/manifest.json`), this is the latter — so - * resolving the relative `remoteEntry` against it anchors the remote (and - * every chunk it loads) at the immutable version path, not the channel root. + * The remoteEntry is build-addressed in production, so it anchors at the + * immutable build whether the manifest was redirected there or served + * directly from the rolling Azure channel path. */ url: string; }; diff --git a/packages/widget/js/src/mount.ts b/packages/widget/js/src/mount.ts index 9be89a4f67..207dd3b0c0 100644 --- a/packages/widget/js/src/mount.ts +++ b/packages/widget/js/src/mount.ts @@ -1,17 +1,24 @@ import { resolveSource } from './loader.js'; import { initRemote, loadRemoteModule, type SharedLib } from './runtime.js'; -import type { WidgetProps } from './types.js'; +import type { WidgetProps, DepositWidgetProps } from './types.js'; -/** Handle returned by `mountWidget` for updating props or tearing down. */ -export type WidgetHandle = { +/** + * Handle returned by `mountWidget` / `mountDepositWidget` for updating props + * or tearing down. Parameterized by the props shape of the mounted widget; + * defaults to the swap widget's for backward compatibility. + */ +export type WidgetHandle

= { /** Re-render the mounted widget with new props. */ - update(props: WidgetProps): void; + update(props: P): void; /** Unmount the widget and release its React root. */ destroy(): void; }; -/** Signature of the remote's `./mount` expose. */ -type RemoteMount = (target: HTMLElement, props: WidgetProps) => WidgetHandle; +/** Handle returned by `mountDepositWidget`. */ +export type DepositWidgetHandle = WidgetHandle; + +/** Signature of the remote's `./mount` / `./mountDeposit` exposes. */ +type RemoteMount

= (target: HTMLElement, props: P) => WidgetHandle

; export type MountOptions = { /** @@ -23,6 +30,31 @@ export type MountOptions = { shared?: Record; }; +/** + * Shared plumbing for the mount entries: resolve + verify the manifest, init + * the MF runtime, and call the requested remote mount expose. The + * security-critical path (signature check + SRI registration) lives in + * `resolveSource`, shared with the React loader. + */ +async function mountRemote

( + exposeName: string, + fnName: string, + target: HTMLElement, + props: P, + options: MountOptions, +): Promise> { + if (typeof window === 'undefined') { + throw new Error(`[layerswap/widget-js] ${fnName}() requires a browser environment`); + } + if (!target) { + throw new Error(`[layerswap/widget-js] ${fnName}(target, …) requires a DOM element`); + } + const { remoteEntry } = await resolveSource(); + initRemote(remoteEntry, options.shared); + const mount = await loadRemoteModule>(exposeName); + return mount(target, props); +} + /** * Fetch the Layerswap widget from the CDN and mount it into `target`. * @@ -49,14 +81,31 @@ export async function mountWidget( props: WidgetProps, options: MountOptions = {}, ): Promise { - if (typeof window === 'undefined') { - throw new Error('[layerswap/widget-js] mountWidget() requires a browser environment'); - } - if (!target) { - throw new Error('[layerswap/widget-js] mountWidget(target, …) requires a DOM element'); - } - const { remoteEntry } = await resolveSource(); - initRemote(remoteEntry, options.shared); - const mount = await loadRemoteModule('mount'); - return mount(target, props); + return mountRemote('mount', 'mountWidget', target, props, options); +} + +/** + * Fetch the Layerswap DEPOSIT widget from the CDN and mount it into `target`. + * + * The deposit widget funds a single, integrator-fixed destination + * (network + allowed tokens + recipient address) — the end user only picks a + * source. Same delivery pipeline and guarantees as `mountWidget`; only one + * widget (of either kind) may be live per page. + * + * ```js + * import { mountDepositWidget } from '@layerswap/widget-js'; + * + * const handle = await mountDepositWidget(document.getElementById('layerswap'), { + * config: { apiKey: 'mainnet' }, + * destination: { network: 'BASE_MAINNET', tokens: ['USDC'] }, + * destinationAddress: '0x…', + * }); + * ``` + */ +export async function mountDepositWidget( + target: HTMLElement, + props: DepositWidgetProps, + options: MountOptions = {}, +): Promise { + return mountRemote('mountDeposit', 'mountDepositWidget', target, props, options); } diff --git a/packages/widget/js/src/types.ts b/packages/widget/js/src/types.ts index 4602b7a97b..2fce236b06 100644 --- a/packages/widget/js/src/types.ts +++ b/packages/widget/js/src/types.ts @@ -7,4 +7,8 @@ export type { WidgetCallbacks, WalletDefaults, WalletProviderId, -} from '@layerswap/widget-types'; + DepositWidgetProps, + DepositConfig, + DepositMethodId, + SupportedDestination, +} from "@layerswap/widget-types"; diff --git a/packages/widget/js/tests/loader-assets.test.mjs b/packages/widget/js/tests/loader-assets.test.mjs index 68383283a5..8317ff2b2c 100644 --- a/packages/widget/js/tests/loader-assets.test.mjs +++ b/packages/widget/js/tests/loader-assets.test.mjs @@ -24,8 +24,15 @@ class FakeScriptElement { globalThis.window = { location: { href: 'https://host.example/page' } }; globalThis.HTMLScriptElement = FakeScriptElement; -globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'https://cdn.example/v1/manifest.json'; -globalThis.__LAYERSWAP_WIDGET_VERIFY__ = false; +Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + subtle: { + importKey: async () => ({}), + verify: async () => true, + }, + }, +}); const remoteHash = `sha384-${'J'.repeat(64)}`; const assetHash = `sha384-${'K'.repeat(64)}`; @@ -33,8 +40,11 @@ globalThis.fetch = async () => ({ ok: true, url: 'https://cdn.example/1.7.0-0123456789ab/manifest.json', json: async () => ({ + protocolMajor: 1, version: '1.7.0', remoteEntry: './remoteEntry.js', + expiresAt: '2999-01-01T00:00:00.000Z', + signature: 'AA==', assetBase: '../assets/', chunks: { 'remoteEntry.js': remoteHash, diff --git a/packages/widget/js/tests/loader-singleflight.test.mjs b/packages/widget/js/tests/loader-singleflight.test.mjs index a56f5d8a7d..55ddace794 100644 --- a/packages/widget/js/tests/loader-singleflight.test.mjs +++ b/packages/widget/js/tests/loader-singleflight.test.mjs @@ -2,13 +2,22 @@ import assert from 'node:assert/strict'; import test from 'node:test'; globalThis.window = { location: { href: 'https://host.example/page' } }; -globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'https://cdn.example/v1/manifest.json'; -globalThis.__LAYERSWAP_WIDGET_VERIFY__ = false; +Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + subtle: { + importKey: async () => ({}), + verify: async () => true, + }, + }, +}); let fetchCount = 0; let failNext = false; -globalThis.fetch = async () => { +let requestedManifestUrl; +globalThis.fetch = async (url) => { fetchCount++; + requestedManifestUrl = url; if (failNext) { failNext = false; return { ok: false, status: 503, url: 'https://cdn.example/v1/manifest.json', json: async () => ({}) }; @@ -17,26 +26,18 @@ globalThis.fetch = async () => { ok: true, url: 'https://cdn.example/1.7.0-0123456789ab/manifest.json', json: async () => ({ + protocolMajor: 1, version: '1.7.0', remoteEntry: './remoteEntry.js', + expiresAt: '2999-01-01T00:00:00.000Z', + signature: 'AA==', }), }; }; const { resolveSource } = await import('../dist/esm/loader.js'); -test('concurrent mounts share a single manifest fetch and verification', async () => { - const before = fetchCount; - const [a, b, c] = await Promise.all([resolveSource(), resolveSource(), resolveSource()]); - assert.equal(fetchCount, before + 1); - assert.equal(a.remoteEntry, b.remoteEntry); - assert.equal(b.remoteEntry, c.remoteEntry); -}); - test('a failed resolution is not cached', async () => { - // Point at a distinct manifest URL so this test does not hit the fresh - // success cached by the previous test. - globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'https://cdn.example/v2/manifest.json'; failNext = true; await assert.rejects(() => resolveSource()); const failedCount = fetchCount; @@ -44,3 +45,18 @@ test('a failed resolution is not cached', async () => { assert.equal(fetchCount, failedCount + 1); assert.equal(retry.remoteEntry, 'https://cdn.example/1.7.0-0123456789ab/remoteEntry.js'); }); + +test('the manifest source is fixed by the loader and single-flighted across concurrent mounts', async () => { + const before = fetchCount; + const realDateNow = Date.now; + Date.now = () => realDateNow() + 60_001; + try { + const [a, b, c] = await Promise.all([resolveSource(), resolveSource(), resolveSource()]); + assert.equal(fetchCount, before + 1); + assert.equal(requestedManifestUrl, 'https://layerswapcdntest.blob.core.windows.net/widget-cdn/v1/manifest.json'); + assert.equal(a.remoteEntry, b.remoteEntry); + assert.equal(b.remoteEntry, c.remoteEntry); + } finally { + Date.now = realDateNow; + } +}); diff --git a/packages/widget/js/tests/loader-verification.test.mjs b/packages/widget/js/tests/loader-verification.test.mjs new file mode 100644 index 0000000000..e96ee45685 --- /dev/null +++ b/packages/widget/js/tests/loader-verification.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +globalThis.window = { location: { href: 'https://host.example/page' } }; +Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + subtle: { + importKey: async () => ({}), + verify: async () => false, + }, + }, +}); + +globalThis.fetch = async () => ({ + ok: true, + url: 'https://cdn.example/1.7.0-0123456789ab/manifest.json', + json: async () => ({ + protocolMajor: 1, + version: '1.7.0', + remoteEntry: './remoteEntry.js', + expiresAt: '2999-01-01T00:00:00.000Z', + signature: 'AA==', + }), +}); + +const { resolveSource } = await import('../dist/esm/loader.js'); + +test('the loader always rejects a manifest that fails signature verification', async () => { + await assert.rejects( + () => resolveSource(), + (error) => error?.name === 'ManifestError' && error?.reason === 'signature', + ); +}); diff --git a/packages/widget/js/tests/protocol.test.mjs b/packages/widget/js/tests/protocol.test.mjs new file mode 100644 index 0000000000..394291ec08 --- /dev/null +++ b/packages/widget/js/tests/protocol.test.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +globalThis.window = { location: { href: 'https://host.example/page' } }; +Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + subtle: { + importKey: async () => ({}), + verify: async () => true, + }, + }, +}); + +const { WIDGET_PROTOCOL_MAJOR } = await import('../dist/esm/index.js'); + +test('the loader package major matches the supported widget protocol', async () => { + const { readFile } = await import('node:fs/promises'); + const loaderPackage = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8'), + ); + assert.equal(Number(loaderPackage.version.split('.')[0]), WIDGET_PROTOCOL_MAJOR); +}); + +test('rejects a remote from another protocol major', async () => { + globalThis.fetch = async () => ({ + ok: true, + url: 'https://cdn.example/2.0.0-0123456789ab/manifest.json', + json: async () => ({ + protocolMajor: WIDGET_PROTOCOL_MAJOR + 1, + version: '2.0.0', + remoteEntry: './remoteEntry.js', + expiresAt: '2999-01-01T00:00:00.000Z', + signature: 'AA==', + }), + }); + + const { resolveSource } = await import('../dist/esm/loader.js'); + await assert.rejects( + () => resolveSource(), + (error) => error?.name === 'ManifestError' && error?.reason === 'incompatible', + ); +}); + +test('accepts a signed legacy manifest whose channel identifies protocol v1', async () => { + globalThis.fetch = async () => ({ + ok: true, + url: 'https://cdn.example/1.7.0-legacy/manifest.json', + json: async () => ({ + channel: 'v1', + version: '1.7.0', + remoteEntry: './remoteEntry.js', + expiresAt: '2999-01-01T00:00:00.000Z', + signature: 'AA==', + }), + }); + + const { resolveSource } = await import('../dist/esm/loader.js'); + const source = await resolveSource(); + assert.equal(source.remoteEntry, 'https://cdn.example/1.7.0-legacy/remoteEntry.js'); +}); diff --git a/packages/widget/react/README.md b/packages/widget/react/README.md index d9e99c96a8..424618e40e 100644 --- a/packages/widget/react/README.md +++ b/packages/widget/react/README.md @@ -46,9 +46,43 @@ Works in the Next.js App Router out of the box: the component declares only starts the browser-only loader after hydration — no `next/dynamic` wrapper needed. -To ride a different major channel (e.g. a future `/v2/`), upgrade the -`@layerswap/widget-react` package — the source URL is pinned to the package -version, not passed at runtime. +The npm package major selects the CDN protocol major: + +```text +@layerswap/widget-react@1.x → /v1/manifest.json +@layerswap/widget-react@2.x → /v2/manifest.json +``` + +Minor and patch widget builds roll forward within that major without an +integrator redeploy. Exact CDN builds are not a public pinning API. + +## Deposit widget + +`LayerswapDepositWidget` renders the deposit flow instead of the full swap +form: you fix the destination (one network, its allowed tokens, and the +recipient address) and the end user only picks a funding source. Delivered +through the same verified manifest + Module Federation pipeline as +`LayerswapWidget`, and it accepts all of the same props plus the +deposit-specific ones (`DepositConfig` in `@layerswap/widget-types`). + +```tsx +import { LayerswapDepositWidget } from '@layerswap/widget-react'; + +export function DepositPage() { + return ( + Loading widget…} + /> + ); +} +``` + +Only one Layerswap widget (of either kind) may be live per page — the widget +keeps process-global state. Vanilla hosts use `mountDepositWidget` from +`@layerswap/widget-js` the same way they use `mountWidget`. ## Reusing the host's wagmi config @@ -79,6 +113,12 @@ The widget's EVM wallet provider adopts the host's `Config` via nested ``, no second connect flow — the widget reads the host's connected account/chain through the same `Config` instance. +You only need to declare the chains **your own app** uses — the widget +appends every Layerswap-supported EVM chain (with its transports) to the +adopted config at init, so network switching and transfers work on chains +you didn't list. Your chain order is preserved, and your transports win +for chains you did configure. + **Gotcha:** wagmi v2 defaults `multiInjectedProviderDiscovery: true`, which auto-registers an EIP-6963 connector for every announced injected provider alongside any bare connector you declared. With both an @@ -132,8 +172,8 @@ accordingly. ## Recommended Content Security Policy The widget is served from the fixed origin baked into this package release — -currently `https://layerswap-widget-cdn.layerswapcdn.workers.dev` (the -`DEFAULT_MANIFEST_URL` in `@layerswap/widget-js`; if a future release moves +currently `https://layerswapcdntest.blob.core.windows.net` (the +`WIDGET_MANIFEST_URL` in `@layerswap/widget-js`; if a future release moves to a custom domain such as `cdn.layerswap.io`, this section moves with it). A tight CSP that allowlists exactly that origin plus the LayerSwap endpoints gives integrators the smallest blast radius if the supply chain is ever @@ -142,8 +182,8 @@ compromised: ``` Content-Security-Policy: default-src 'self'; - script-src 'self' https://layerswap-widget-cdn.layerswapcdn.workers.dev; - connect-src 'self' https://layerswap-widget-cdn.layerswapcdn.workers.dev + script-src 'self' https://layerswapcdntest.blob.core.windows.net; + connect-src 'self' https://layerswapcdntest.blob.core.windows.net https://api.layerswap.io https://layerswap.io https://*.walletconnect.com https://*.walletconnect.org; style-src 'self' 'unsafe-inline'; @@ -176,27 +216,16 @@ Notes: | Widget never mounts | `ManifestError('kill-switch')` | Operational kill-switch set on the manifest. | | Widget never mounts | `ManifestError('signature')` | Manifest has no/invalid signature (verification is always on). | | Widget never mounts | `ManifestError('stale')` | Manifest expired (or carries no validity window) — replay protection refuses possibly-rolled-back builds. Layerswap re-publishing the channel resolves it. | +| Widget never mounts | `ManifestError('incompatible')` | Manifest protocol major does not match this loader package major. | | Widget loads but errors at render | Component-level | Catch via `callbacks.onError`. | ## Local development -The widget's source is fixed to the production CDN and is not overridable -through props. For working **on the widget itself** inside this monorepo, -the loader reads an internal, undocumented override from `globalThis` so -Layerswap's own harnesses can target the local widget-cdn dev server: - -```ts -// Set BEFORE mounts (e.g. at module scope). Not part of -// the public API — a build/test seam for the monorepo only. -globalThis.__LAYERSWAP_WIDGET_MANIFEST__ = 'http://127.0.0.1:3100/manifest.json'; -globalThis.__LAYERSWAP_WIDGET_VERIFY__ = false; // dev manifest is unsigned -``` - -Run the widget-cdn dev server (`pnpm dev` in `apps/widget-cdn`) — it serves -both `remoteEntry.js` and an unsigned `manifest.json` at -`http://127.0.0.1:3100`. See `examples/widget-react-host/` for a runnable -Vite host that sets these globals from `VITE_LAYERSWAP_MANIFEST` / -`VITE_LAYERSWAP_VERIFY` and wires `wagmiConfig` adoption and callbacks. +The widget source and signature verification policy are owned entirely by +`@layerswap/widget-js`. They cannot be changed through props, environment +variables, or globals. The runnable Vite host in +`examples/widget-react-host/` therefore exercises the same signed production +channel as an integrator. ## Security model diff --git a/packages/widget/react/package.json b/packages/widget/react/package.json index fe279b8b4f..4b16b72c80 100644 --- a/packages/widget/react/package.json +++ b/packages/widget/react/package.json @@ -1,7 +1,7 @@ { "name": "@layerswap/widget-react", + "version": "1.0.0", "private": true, - "version": "0.1.0", "description": "React component that fetches the Layerswap widget from the CDN at runtime via Module Federation. Integrators install this and nothing else.", "type": "module", "main": "dist/esm/index.js", @@ -26,6 +26,7 @@ "build:esm+types": "tsc --project tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types && tsc-alias -p tsconfig.json --outDir ./dist/esm && tsc-alias -p tsconfig.json --outDir ./dist/types", "check:types": "tsc --noEmit && tsc --project tsconfig.type-tests.json --noEmit", "clean": "rimraf dist tsconfig.tsbuildinfo", + "prepublishOnly": "node ../js/scripts/assert-production-origin.mjs", "dev": "tsc -p tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types --watch", "test": "pnpm build && node --test tests/*.test.mjs" }, @@ -50,7 +51,7 @@ "wagmi": "catalog:" }, "dependencies": { - "@layerswap/widget-js": "workspace:^" + "@layerswap/widget-js": "workspace:*" }, "devDependencies": { "@types/react": "catalog:", @@ -66,4 +67,4 @@ "optional": true } } -} +} \ No newline at end of file diff --git a/packages/widget/react/src/LayerswapDepositWidget.tsx b/packages/widget/react/src/LayerswapDepositWidget.tsx new file mode 100644 index 0000000000..e97d22721e --- /dev/null +++ b/packages/widget/react/src/LayerswapDepositWidget.tsx @@ -0,0 +1,45 @@ +'use client'; + +import type { ReactNode } from 'react'; +import type { Config as WagmiConfig } from 'wagmi'; +import type { DepositWidgetProps } from '@layerswap/widget-js'; +import { RemoteWidgetHost, type RemoteHostCallbacks } from './remoteWidgetHost.js'; + +/** + * Shape of the props the CDN remote's DEPOSIT widget export accepts — the + * shared framework-agnostic contract with its host-specific slots bound to + * this package's React/wagmi peers, mirroring `RemoteWidgetProps`. + */ +export type RemoteDepositWidgetProps = DepositWidgetProps; + +export type LayerswapDepositWidgetProps = RemoteDepositWidgetProps & RemoteHostCallbacks; + +/** + * Host-side React loader for the CDN-delivered Layerswap DEPOSIT widget — the + * fixed-destination funding flow (the integrator locks the destination + * network, tokens, and recipient address; the end user only picks a source). + * + * Delivered through the same verified manifest + Module Federation pipeline + * as `LayerswapWidget`; see `RemoteWidgetHost` for the SSR/hydration and + * loading behavior. + * + * ```tsx + * + * ``` + */ +export function LayerswapDepositWidget(props: LayerswapDepositWidgetProps) { + const { fallback, onReady, onError, ...rest } = props; + return ( + + ); +} diff --git a/packages/widget/react/src/LayerswapWidget.tsx b/packages/widget/react/src/LayerswapWidget.tsx index 38cd38f938..f071033b84 100644 --- a/packages/widget/react/src/LayerswapWidget.tsx +++ b/packages/widget/react/src/LayerswapWidget.tsx @@ -1,28 +1,9 @@ 'use client'; -import { - ComponentType, - ReactNode, - Suspense, - lazy, - useMemo, - useState, - useEffect, - useRef, - useCallback, - Component, - ErrorInfo, -} from 'react'; -import React from 'react'; -import ReactDOM from 'react-dom'; +import type { ReactNode } from 'react'; import type { Config as WagmiConfig } from 'wagmi'; -import { - resolveSource, - initRemote, - loadRemoteModule, - type WidgetProps, - type SharedLib, -} from '@layerswap/widget-js'; +import type { WidgetProps } from '@layerswap/widget-js'; +import { RemoteWidgetHost, type RemoteHostCallbacks } from './remoteWidgetHost.js'; /** Wallet provider ids matching what the remote's `getDefaultProviders()` emits. */ export type { WalletProviderId } from '@layerswap/widget-js'; @@ -36,124 +17,27 @@ export type { WalletProviderId } from '@layerswap/widget-js'; */ export type RemoteWidgetProps = WidgetProps; -export type LayerswapWidgetProps = RemoteWidgetProps & { - // The widget's source is not configurable: it is always fetched from the - // canonical Layerswap CDN baked into `@layerswap/widget-js`, with its - // manifest signature verified. This keeps the remote bundle's origin under - // Layerswap's control rather than the integrator's. - /** Shown while the remote bundle is being fetched / initialized. */ - fallback?: ReactNode; - /** Fired once the remote module has loaded and the widget mounts. */ - onReady?: () => void; - /** Fired when the remote fails to load or throws during render. */ - onError?: (error: unknown) => void; -}; - -type WidgetComponent = ComponentType; - -class WidgetErrorBoundary extends Component< - { fallback: ReactNode; onError?: (error: unknown) => void; children: ReactNode }, - { error: unknown } -> { - state = { error: null as unknown }; - - static getDerivedStateFromError(error: unknown) { - return { error }; - } - - componentDidCatch(error: unknown, _info: ErrorInfo) { - this.props.onError?.(error); - } - - render() { - if (this.state.error) return this.props.fallback ?? null; - return this.props.children; - } -} - -// Share the host's React/ReactDOM with the remote as MF singletons so the -// widget dedups onto the host's instance instead of bundling its own. The -// range mirrors this package's peerDependencies: accept React 18 and 19 (the -// widget relies on 18+ hook semantics) while rejecting 17, where those hooks -// don't exist. `requiredVersion: false` would silently dedup an incompatible -// host version. Vanilla (non-React) hosts use `@layerswap/widget-js` directly, -// which shares nothing and lets the remote bundle its own React. -function hostReactShare(): Record { - return { - react: { - version: (React as { version?: string }).version ?? '0.0.0', - lib: () => React, - requiredVersion: '^18.0.0 || ^19.0.0', - }, - 'react-dom': { - version: (ReactDOM as { version?: string }).version ?? '0.0.0', - lib: () => ReactDOM, - requiredVersion: '^18.0.0 || ^19.0.0', - }, - }; -} - -function buildLoader(): () => Promise<{ default: WidgetComponent }> { - return async () => { - const { remoteEntry } = await resolveSource(); - initRemote(remoteEntry, hostReactShare()); - const Widget = await loadRemoteModule('Widget'); - return { default: Widget }; - }; -} +export type LayerswapWidgetProps = RemoteWidgetProps & RemoteHostCallbacks; +// The widget's source is not configurable: it is always fetched from the +// canonical Layerswap CDN baked into `@layerswap/widget-js`, with its +// manifest signature verified. This keeps the remote bundle's origin under +// Layerswap's control rather than the integrator's. /** - * Host-side React loader for the CDN-delivered Layerswap widget. + * Host-side React loader for the CDN-delivered Layerswap swap widget. * - * Safe to import from anywhere in Next.js — the file declares `"use client"`, - * so App Router Server Components can render it directly. Because Client - * Components are still pre-rendered to HTML on the server, the component - * renders `fallback` until hydration completes and only then starts the - * browser-only work (manifest fetch, Module Federation init). + * Safe to import from anywhere in Next.js — see `RemoteWidgetHost` for the + * SSR/hydration and loading behavior. */ export function LayerswapWidget(props: LayerswapWidgetProps) { const { fallback, onReady, onError, ...rest } = props; - - // Hydration gate: `mounted` is false during SSR/prerender and the first - // client render, so server HTML and hydration output agree, and the - // browser-only loader never runs outside the browser. - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - - // De-dup guard lives on the parent (which persists across Suspense - // re-resolution) rather than inside `ReadySignal`, so `onReady` fires - // exactly once per physical `LayerswapWidget` mount. - const onReadyFiredRef = useRef(false); - const stableOnReady = useCallback(() => { - if (onReadyFiredRef.current) return; - onReadyFiredRef.current = true; - onReady?.(); - }, [onReady]); - - // Per-mount, not module scope: `lazy()` caches a rejected loader promise - // forever, so a module-level instance would turn one transient CDN failure - // into a page-lifetime one. A fresh `lazy()` per mount lets the integrator - // recover by remounting; dedup of the underlying fetch is handled by - // `resolveSource()`'s single-flight, which never caches failures. - const LazyWidget = useMemo(() => lazy(buildLoader()), []); - - if (!mounted) return <>{fallback ?? null}; - return ( - - - - - - + ); } - -function ReadySignal({ onReady }: { onReady?: () => void }) { - useEffect(() => { - // Runs once per Suspense resolve; the parent's ref guard prevents the - // callback from double-firing across LazyWidget recreation. - onReady?.(); - }, [onReady]); - return null; -} diff --git a/packages/widget/react/src/index.ts b/packages/widget/react/src/index.ts index e0945530f2..788abb3d16 100644 --- a/packages/widget/react/src/index.ts +++ b/packages/widget/react/src/index.ts @@ -1,5 +1,7 @@ export { LayerswapWidget } from './LayerswapWidget.js'; export type { LayerswapWidgetProps, RemoteWidgetProps } from './LayerswapWidget.js'; +export { LayerswapDepositWidget } from './LayerswapDepositWidget.js'; +export type { LayerswapDepositWidgetProps, RemoteDepositWidgetProps } from './LayerswapDepositWidget.js'; // Manifest format + error type live in the framework-agnostic core. export type { Manifest } from '@layerswap/widget-js'; export { ManifestError } from '@layerswap/widget-js'; @@ -14,14 +16,19 @@ import type { Config as WagmiConfig } from 'wagmi'; import type { WidgetConfig as SharedWidgetConfig, WidgetProps as SharedWidgetProps, + DepositWidgetProps as SharedDepositWidgetProps, } from '@layerswap/widget-js'; export type WidgetConfig = SharedWidgetConfig; export type WidgetProps = SharedWidgetProps; +export type DepositWidgetProps = SharedDepositWidgetProps; export type { WidgetCallbacks, WalletDefaults, WalletProviderId, + DepositConfig, + DepositMethodId, + SupportedDestination, ThemeData, ThemeColor, } from '@layerswap/widget-js'; diff --git a/packages/widget/react/src/remoteWidgetHost.tsx b/packages/widget/react/src/remoteWidgetHost.tsx new file mode 100644 index 0000000000..22014b9d2d --- /dev/null +++ b/packages/widget/react/src/remoteWidgetHost.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { + ComponentType, + ReactNode, + Suspense, + lazy, + useMemo, + useState, + useEffect, + useRef, + useCallback, + Component, + ErrorInfo, +} from 'react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { + resolveSource, + initRemote, + loadRemoteModule, + type SharedLib, +} from '@layerswap/widget-js'; + +/** + * Internal machinery shared by `LayerswapWidget` and `LayerswapDepositWidget`: + * hydration gating, remote loading via Module Federation, error containment, + * and the once-per-mount `onReady` signal. The two public components differ + * only in which remote expose they load and the props they forward. + */ + +export type RemoteHostCallbacks = { + /** Shown while the remote bundle is being fetched / initialized. */ + fallback?: ReactNode; + /** Fired once the remote module has loaded and the widget mounts. */ + onReady?: () => void; + /** Fired when the remote fails to load or throws during render. */ + onError?: (error: unknown) => void; +}; + +class WidgetErrorBoundary extends Component< + { fallback: ReactNode; onError?: (error: unknown) => void; children: ReactNode }, + { error: unknown } +> { + state = { error: null as unknown }; + + static getDerivedStateFromError(error: unknown) { + return { error }; + } + + componentDidCatch(error: unknown, _info: ErrorInfo) { + this.props.onError?.(error); + } + + render() { + if (this.state.error) return this.props.fallback ?? null; + return this.props.children; + } +} + +// Share the host's React/ReactDOM with the remote as MF singletons so the +// widget dedups onto the host's instance instead of bundling its own. The +// range mirrors this package's peerDependencies: accept React 18 and 19 (the +// widget relies on 18+ hook semantics) while rejecting 17, where those hooks +// don't exist. `requiredVersion: false` would silently dedup an incompatible +// host version. Vanilla (non-React) hosts use `@layerswap/widget-js` directly, +// which shares nothing and lets the remote bundle its own React. +function hostReactShare(): Record { + return { + react: { + version: (React as { version?: string }).version ?? '0.0.0', + lib: () => React, + requiredVersion: '^18.0.0 || ^19.0.0', + }, + 'react-dom': { + version: (ReactDOM as { version?: string }).version ?? '0.0.0', + lib: () => ReactDOM, + requiredVersion: '^18.0.0 || ^19.0.0', + }, + }; +} + +function buildLoader

(expose: string): () => Promise<{ default: ComponentType

}> { + return async () => { + const { remoteEntry } = await resolveSource(); + initRemote(remoteEntry, hostReactShare()); + const Widget = await loadRemoteModule>(expose); + return { default: Widget }; + }; +} + +function ReadySignal({ onReady }: { onReady?: () => void }) { + useEffect(() => { + // Runs once per Suspense resolve; the parent's ref guard prevents the + // callback from double-firing across LazyWidget recreation. + onReady?.(); + }, [onReady]); + return null; +} + +/** + * Host-side loader for one of the CDN remote's component exposes. + * + * Safe to render from anywhere in Next.js — this file declares `"use client"`, + * so App Router Server Components can render its consumers directly. Because + * Client Components are still pre-rendered to HTML on the server, it renders + * `fallback` until hydration completes and only then starts the browser-only + * work (manifest fetch, Module Federation init). + */ +export function RemoteWidgetHost

({ + expose, + widgetProps, + fallback, + onReady, + onError, +}: RemoteHostCallbacks & { expose: string; widgetProps: P }) { + // Hydration gate: `mounted` is false during SSR/prerender and the first + // client render, so server HTML and hydration output agree, and the + // browser-only loader never runs outside the browser. + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + // De-dup guard lives on the parent (which persists across Suspense + // re-resolution) rather than inside `ReadySignal`, so `onReady` fires + // exactly once per physical widget mount. + const onReadyFiredRef = useRef(false); + const stableOnReady = useCallback(() => { + if (onReadyFiredRef.current) return; + onReadyFiredRef.current = true; + onReady?.(); + }, [onReady]); + + // Per-mount, not module scope: `lazy()` caches a rejected loader promise + // forever, so a module-level instance would turn one transient CDN failure + // into a page-lifetime one. A fresh `lazy()` per mount lets the integrator + // recover by remounting; dedup of the underlying fetch is handled by + // `resolveSource()`'s single-flight, which never caches failures. + const LazyWidget = useMemo(() => lazy(buildLoader

(expose)), []); + + if (!mounted) return <>{fallback ?? null}; + + return ( + + + + + + + ); +} diff --git a/packages/widget/react/tests/widget-props.test.ts b/packages/widget/react/tests/widget-props.test.ts index a8dfc8f5dc..6481553b2e 100644 --- a/packages/widget/react/tests/widget-props.test.ts +++ b/packages/widget/react/tests/widget-props.test.ts @@ -1,6 +1,10 @@ import type { Config as WagmiConfig } from 'wagmi'; -import type { WidgetProps as VanillaWidgetProps } from '@layerswap/widget-js'; +import type { + WidgetProps as VanillaWidgetProps, + DepositWidgetProps as VanillaDepositWidgetProps, +} from '@layerswap/widget-js'; import type { RemoteWidgetProps } from '../src/LayerswapWidget'; +import type { RemoteDepositWidgetProps } from '../src/LayerswapDepositWidget'; const vanillaProps: VanillaWidgetProps = { config: { apiKey: 'mainnet' } }; @@ -22,7 +26,38 @@ const reactProps: RemoteWidgetProps = { config: { loadingComponent: 'Loading widget…' }, }; +const vanillaDepositProps: VanillaDepositWidgetProps = { + config: { apiKey: 'mainnet' }, + destination: { network: 'BASE_MAINNET', tokens: ['USDC'] }, + destinationAddress: '0x0000000000000000000000000000000000000000', + methods: ['wallet', 'deposit_address'], +}; + +// @ts-expect-error `destination` and `destinationAddress` are required. +const invalidDepositMissingDestination: VanillaDepositWidgetProps = { + config: { apiKey: 'mainnet' }, +}; + +const invalidDepositMethod: VanillaDepositWidgetProps = { + destination: { network: 'BASE_MAINNET', tokens: ['USDC'] }, + destinationAddress: '0x0000000000000000000000000000000000000000', + // @ts-expect-error Unknown deposit method id. + methods: ['bank_transfer'], +}; + +const reactDepositProps: RemoteDepositWidgetProps = { + wagmiConfig, + config: { loadingComponent: 'Loading widget…' }, + destination: { network: 'BASE_MAINNET', tokens: ['USDC'] }, + destinationAddress: '0x0000000000000000000000000000000000000000', + mode: 'button', +}; + void vanillaProps; void invalidVanillaWagmi; void invalidVanillaLoading; void reactProps; +void vanillaDepositProps; +void invalidDepositMissingDestination; +void invalidDepositMethod; +void reactDepositProps; diff --git a/packages/widget/types/src/config.ts b/packages/widget/types/src/config.ts index 95496af872..26e8e5d62e 100644 --- a/packages/widget/types/src/config.ts +++ b/packages/widget/types/src/config.ts @@ -1,4 +1,4 @@ -import type { ThemeData } from './theme'; +import type { ThemeData } from "./theme"; /** * Wallet provider ids matching what the remote's `getDefaultProviders()` @@ -8,15 +8,15 @@ import type { ThemeData } from './theme'; * Note the id for Solana is `'solana'` (the chain), not `'svm'`. */ export type WalletProviderId = - | 'evm' - | 'starknet' - | 'fuel' - | 'paradex' - | 'bitcoin' - | 'ton' - | 'solana' - | 'tron' - | 'imtblPassport'; + | "evm" + | "starknet" + | "fuel" + | "paradex" + | "bitcoin" + | "ton" + | "solana" + | "tron" + | "imtblPassport"; /** * Public widget configuration contract. @@ -25,7 +25,7 @@ export type WalletProviderId = * (`@layerswap/widget`) refines it internally — its `LayerswapWidgetConfig` * is `WidgetConfig` intersected with precise types for the deep fields * (`settings`, `initialValues`) — so the two can never structurally diverge, - * while integrators and the loaders depend only on this package. + * while integrators and the loaders depend only on this lightweight package. * * Framework-agnostic by construction: `TLoading` is the host's renderable type * (`ReactNode` in React hosts — `@layerswap/widget-react` binds it), kept open @@ -36,7 +36,7 @@ export type WidgetConfig = { apiKey?: string; apiUri?: string; /** Network set to target. */ - version?: 'mainnet' | 'testnet'; + version?: "mainnet" | "testnet"; /** Visual theme overrides. */ theme?: ThemeData | null; /** @@ -93,6 +93,66 @@ export type WidgetCallbacks = { onMenuNavigationChange?: (path: string) => void; }; +/** + * The single destination the deposit widget funds. Structurally typed — the + * precise source of truth is `SupportedDestination` in `@layerswap/widget` + * (`components/Pages/Deposit/DestinationTokenPicker.tsx`); the CDN remote + * spreads these props into that component, so a divergence fails its + * typecheck rather than drifting silently. + */ +export type SupportedDestination = { + /** Network `name` (canonical identifier like `BASE_MAINNET`). */ + network: string; + /** Token symbols (case-insensitive, e.g. `["USDC", "USDT"]`). The user picks + * one of these via the token dropdown; the network is fixed. */ + tokens: string[]; +}; + +/** + * Deposit funding methods. Mirrors `DEPOSIT_METHODS` in `@layerswap/widget` + * (`components/Pages/Deposit/depositMethods.ts`) — kept in lockstep by the CDN + * remote's typecheck, same as {@link SupportedDestination}. + */ +export type DepositMethodId = + | "wallet" + | "deposit_address" + | "hyperliquid" + | "polymarket"; + +/** + * Deposit-widget-specific props — the integrator-facing surface of + * `DepositProps` in `@layerswap/widget`. `partner` is deliberately not part of + * the public contract (it is a Layerswap-internal model). + */ +export type DepositConfig = { + /** The single destination network and its allowed tokens. The network is + * fixed; the user picks one of the tokens via the token dropdown. */ + destination: SupportedDestination; + /** Recipient address on the destination network. Required — the deposit + * widget never asks the end user for this. */ + destinationAddress: string; + /** "inline" (default) renders the widget directly. "button" renders a Deposit + * button that opens the widget inside a dialog. */ + mode?: "inline" | "button"; + /** Title shown in the widget header. Defaults to "Deposit". */ + title?: string; + /** Label for the trigger button when mode="button". Defaults to "Deposit". */ + buttonLabel?: string; + /** Extra className applied to the trigger button when mode="button". */ + buttonClassName?: string; + /** When true, show the "Send to" destination address row in the quote + * summary. Defaults to false. */ + showDestinationAddress?: boolean; + actionButtonText?: string; + /** Default amount (in USD) seeded into the wallet flow once the user + * picks a source token. Defaults to $1. Set to 0 to disable seeding. */ + defaultAmountUsd?: number; + /** The deposit funding methods to offer, e.g. `['wallet','deposit_address']`. + * Acts as an allow-list: only listed methods can appear (a method also still + * needs its own runtime condition). Defaults to all available methods. */ + methods?: DepositMethodId[]; +}; + /** * Props the CDN remote's widget export accepts — the shared shape forwarded by * the vanilla `mountWidget` and the React `LayerswapWidget`. @@ -138,3 +198,22 @@ export type WidgetProps< */ wagmiConfig?: TWagmi; }; + +/** + * Props the CDN remote's DEPOSIT widget export accepts — the shared shape + * forwarded by the vanilla `mountDepositWidget` and the React + * `LayerswapDepositWidget`: the common widget props plus the deposit-specific + * configuration, flattened to mirror `Deposit`'s props in `@layerswap/widget`. + * + * Same generic scheme as {@link WidgetProps}: framework-agnostic by default, + * bound to precise host/remote types by `@layerswap/widget-react` and the CDN + * remote respectively. + */ +export type DepositWidgetProps< + TWagmi = never, + TLoading = never, + TConfig = WidgetConfig, + TWalletDefaults = WalletDefaults, + TCallbacks = WidgetCallbacks, +> = WidgetProps & + DepositConfig; diff --git a/packages/widget/types/src/index.ts b/packages/widget/types/src/index.ts index a2983a7bf6..1135d2c003 100644 --- a/packages/widget/types/src/index.ts +++ b/packages/widget/types/src/index.ts @@ -1,10 +1,15 @@ export type { ThemeData, ThemeColor, StatusColor } from './theme'; +export { WIDGET_PROTOCOL_MAJOR, widgetProtocolMajorOf } from './protocol.js'; export type { WidgetConfig, WidgetCallbacks, WidgetProps, WalletDefaults, WalletProviderId, + DepositWidgetProps, + DepositConfig, + DepositMethodId, + SupportedDestination, } from './config'; export { NetworkType } from './network'; export type { Refuel, AvailableSourceNetworkTypes } from './network'; diff --git a/packages/widget/types/src/protocol.ts b/packages/widget/types/src/protocol.ts new file mode 100644 index 0000000000..9a73199324 --- /dev/null +++ b/packages/widget/types/src/protocol.ts @@ -0,0 +1,29 @@ +/** + * Public compatibility boundary between an npm loader and the CDN remote. + * + * This changes only when the loader/remote contract breaks (manifest shape, + * exposed Module Federation modules, widget props, or required host runtime). + * It is deliberately independent from @layerswap/widget's implementation + * version so ordinary core releases continue to roll within the same channel. + */ +export const WIDGET_PROTOCOL_MAJOR = 1; + +/** + * Read a manifest's compatibility major. The channel fallback keeps signed + * pre-protocol-field v1 manifests usable during the migration; every newly + * published manifest is required to carry protocolMajor explicitly. + */ +export function widgetProtocolMajorOf(manifest: { + protocolMajor?: unknown; + channel?: unknown; +}): number | undefined { + if ( + Number.isInteger(manifest.protocolMajor) && + Number(manifest.protocolMajor) > 0 + ) { + return Number(manifest.protocolMajor); + } + if (typeof manifest.channel !== "string") return undefined; + const match = /^v([1-9]\d*)$/.exec(manifest.channel); + return match ? Number(match[1]) : undefined; +} diff --git a/packages/widget/types/tsconfig.json b/packages/widget/types/tsconfig.json index 96c4f7f763..d6fd15eb4f 100644 --- a/packages/widget/types/tsconfig.json +++ b/packages/widget/types/tsconfig.json @@ -13,6 +13,9 @@ "skipLibCheck": true, "esModuleInterop": true }, + "tsc-alias": { + "resolveFullPaths": true + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ae44d3120..9dd87d2e0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,7 +163,7 @@ importers: version: 0.9.0(@types/react@19.2.3)(immer@11.1.15)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3)) '@flags-sdk/vercel': specifier: ^1.4.3 - version: 1.4.3(@aws-sdk/credential-provider-web-identity@3.972.66)(flags@4.2.0(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 1.4.3(flags@4.2.0(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@layerswap/wallet-evm': specifier: workspace:^ version: link:../../packages/wallets/adapters/evm @@ -431,12 +431,6 @@ importers: specifier: 'catalog:' version: 4.5.7(@types/react@19.2.3)(immer@11.1.15)(react@19.2.3) devDependencies: - '@aws-sdk/client-s3': - specifier: ^3.700.0 - version: 3.1092.0 - '@cloudflare/workers-types': - specifier: ^4.20241127.0 - version: 4.20260702.1 '@module-federation/enhanced': specifier: ^0.9.0 version: 0.9.1(@rspack/core@1.7.12(@swc/helpers@0.5.18))(bufferutil@4.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -464,9 +458,6 @@ importers: typescript: specifier: ^5.9.2 version: 5.9.3 - wrangler: - specifier: ^4.0.0 - version: 4.114.0(@cloudflare/workers-types@4.20260702.1)(bufferutil@4.1.0)(utf-8-validate@5.0.10) apps/widget-playground: dependencies: @@ -750,7 +741,7 @@ importers: version: 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/ethereum-provider': specifier: 2.21.8 - version: 2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + version: 2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) ethers: specifier: 5.7.2 version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -877,7 +868,7 @@ importers: version: 8.9.2 starknetkit: specifier: ^3.4.3 - version: 3.4.3(@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3))(@scure/bip39@1.6.0)(@types/react@19.2.3)(bufferutil@4.1.0)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@12.26.2(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(starknet@8.9.2)(swr@2.3.8(react@19.2.3))(tailwind-merge@3.4.0)(tailwindcss-animate@1.0.7(tailwindcss@4.0.15))(tailwindcss@4.0.15)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + version: 3.4.3(@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3))(@scure/bip39@1.6.0)(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@12.26.2(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(starknet@8.9.2)(swr@2.3.8(react@19.2.3))(tailwind-merge@3.4.0)(tailwindcss-animate@1.0.7(tailwindcss@4.0.15))(tailwindcss@4.0.15)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) devDependencies: '@layerswap/utils': specifier: workspace:^ @@ -902,7 +893,7 @@ importers: version: 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/wallet-adapter-wallets': specifier: ^0.19.38 - version: 0.19.38(@babel/runtime@7.28.4)(@solana/sysvars@2.3.0(typescript@5.9.3))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bs58@6.0.0)(bufferutil@4.1.0)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) + version: 0.19.38(@babel/runtime@7.28.4)(@solana/sysvars@2.3.0(typescript@5.9.3))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bs58@6.0.0)(bufferutil@4.1.0)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76) '@solana/wallet-standard-wallet-adapter-base': specifier: ^1.1.4 version: 1.1.4(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bs58@6.0.0) @@ -914,16 +905,16 @@ importers: version: 1.1.1 '@walletconnect/ethereum-provider': specifier: 2.21.8 - version: 2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': specifier: 'catalog:' - version: 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + version: 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/universal-provider': specifier: 2.21.8 - version: 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/utils': specifier: 'catalog:' - version: 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: specifier: ^6.0.0 version: 6.0.0 @@ -1216,7 +1207,7 @@ importers: packages/widget/js: dependencies: '@layerswap/widget-types': - specifier: workspace:^ + specifier: workspace:* version: link:../types '@module-federation/runtime': specifier: ^0.9.0 @@ -1225,7 +1216,7 @@ importers: packages/widget/react: dependencies: '@layerswap/widget-js': - specifier: workspace:^ + specifier: workspace:* version: link:../js devDependencies: '@types/react': @@ -1303,78 +1294,6 @@ packages: url-join: 5.0.0 zod: 4.0.5 - '@aws-sdk/checksums@3.1000.19': - resolution: {integrity: sha512-Hc4N100RdkuWshKBnhPzmpdftfi9mCLz+OHFELHM1QIgMH4QRUUWyWgfiebta/YX2Bd62wTcm3EqAP8TeXv0gA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/client-s3@3.1092.0': - resolution: {integrity: sha512-NfcptdANQM1IgUT8QITKBN+PZPjshm5FyLKKjotEwscsDQGik4iDdLgwFYJSTlGoREv26Tf97WHpL7IZ3HF9nA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.976.0': - resolution: {integrity: sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.60': - resolution: {integrity: sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.62': - resolution: {integrity: sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.973.5': - resolution: {integrity: sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.67': - resolution: {integrity: sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.71': - resolution: {integrity: sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.60': - resolution: {integrity: sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.973.4': - resolution: {integrity: sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.66': - resolution: {integrity: sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-sdk-s3@3.972.65': - resolution: {integrity: sha512-udwNhRfDTfCB98mAHjjgsnKQlxygB4e0X+Obne/XjJpvVsF0YCQC8ZErd/8Z6IPoLQjtiKHzwqEDbZiLrJEnOg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/nested-clients@3.997.34': - resolution: {integrity: sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.41': - resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1092.0': - resolution: {integrity: sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.974.2': - resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/xml-builder@3.972.36': - resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.3.0': - resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} - engines: {node: '>=18.0.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1591,62 +1510,12 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cloudflare/kv-asset-handler@0.5.0': - resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} - engines: {node: '>=22.0.0'} - - '@cloudflare/unenv-preset@2.16.1': - resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: '>1.20260305.0 <2.0.0-0' - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/workerd-darwin-64@1.20260722.1': - resolution: {integrity: sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260722.1': - resolution: {integrity: sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20260722.1': - resolution: {integrity: sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260722.1': - resolution: {integrity: sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20260722.1': - resolution: {integrity: sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cloudflare/workers-types@4.20260702.1': - resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} - '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} '@coinbase/wallet-sdk@4.3.0': resolution: {integrity: sha512-T3+SNmiCw4HzDm4we9wCHCxlP0pqCiwKe4sOwPH3YAK2KSKjxPRydKu6UQJrdONFVLG7ujXvbd/6ZqmvJb8rkw==} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -2316,284 +2185,142 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.2': - resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.35.3': resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.2': - resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.35.3': resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.2': - resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} - engines: {node: '>=20.9.0'} - os: [freebsd] - '@img/sharp-freebsd-wasm32@0.35.3': resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.1': - resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.2': resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.1': - resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.1': - resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linux-arm64@1.3.2': resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.3.1': - resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} - cpu: [arm] - os: [linux] - '@img/sharp-libvips-linux-arm@1.3.2': resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.3.1': - resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} - cpu: [ppc64] - os: [linux] - '@img/sharp-libvips-linux-ppc64@1.3.2': resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-riscv64@1.3.1': - resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} - cpu: [riscv64] - os: [linux] - '@img/sharp-libvips-linux-riscv64@1.3.2': resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.3.1': - resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} - cpu: [s390x] - os: [linux] - '@img/sharp-libvips-linux-s390x@1.3.2': resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.3.1': - resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linux-x64@1.3.2': resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': - resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.3.1': - resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.35.2': - resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [linux] - '@img/sharp-linux-arm64@0.35.3': resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.35.2': - resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} - engines: {node: '>=20.9.0'} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.35.3': resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.35.2': - resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} - engines: {node: '>=20.9.0'} - cpu: [ppc64] - os: [linux] - '@img/sharp-linux-ppc64@0.35.3': resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] - '@img/sharp-linux-riscv64@0.35.2': - resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} - engines: {node: '>=20.9.0'} - cpu: [riscv64] - os: [linux] - '@img/sharp-linux-riscv64@0.35.3': resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] - '@img/sharp-linux-s390x@0.35.2': - resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} - engines: {node: '>=20.9.0'} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.35.3': resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.35.2': - resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.35.3': resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.35.2': - resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.35.3': resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.35.2': - resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [linux] - '@img/sharp-linuxmusl-x64@0.35.3': resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.35.2': - resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} - engines: {node: '>=20.9.0'} - '@img/sharp-wasm32@0.35.3': resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.2': - resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} - engines: {node: '>=20.9.0'} - cpu: [wasm32] - '@img/sharp-webcontainers-wasm32@0.35.3': resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.2': - resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [win32] - '@img/sharp-win32-arm64@0.35.3': resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.2': - resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} - engines: {node: ^20.9.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.35.3': resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.2': - resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.35.3': resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} engines: {node: '>=20.9.0'} @@ -2643,9 +2370,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -3235,15 +2959,6 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@posthog/cli@0.7.25': resolution: {integrity: sha512-o+NiX3D7nmXPiY2aOQ7WqzhNyLXsGfpCrha/i8dnBGouP43Pqeyxw+N/H5CM0McH7uMZyEv76JIziPzL91YMJw==} engines: {node: '>=14.14', npm: '>=6'} @@ -4790,34 +4505,6 @@ packages: '@sinclair/typebox@0.33.22': resolution: {integrity: sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==} - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - - '@smithy/core@3.29.7': - resolution: {integrity: sha512-BiEE2bnnGoPKdlGe3L+gOYORDHFGPuYVRLP7iUow/Sflm0B4hC4XY3FC1MRuc7ltzpW2xNnXopKi34TTkULlKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.4.12': - resolution: {integrity: sha512-ZZPDbl/aRp77aycuoMlo3BTayT4CE2a3uoqETYZU5ySnVbhpl5IJiY7dCZedn+ZusyDLqVv44IvKBiXd2/nK0Q==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.6.9': - resolution: {integrity: sha512-EJktha5m5MXCwzdXrlWyqb9UCNHNFKlg+PmTpRsdX3dncJPTiqYleM9OKj2mLgdVJHR01d2tU4alG+z2NdH5rQ==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.9.9': - resolution: {integrity: sha512-xVBZ3hptB99iNO9XyWqEhC7KD9bP9UPXhuy3h5Y2ItCfBv160D9IIC/Fmmp3EbnWwit4C+KVqlSE+E29Nk/pPg==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.6.8': - resolution: {integrity: sha512-iGBm6hIwD2MGvVRSgrjVWa4FXtXDq3akxu0DCpnkmBo0xtEHZ/siMRt7ycfZAefYr2UdywUgmGtoRLaq5u56pg==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.16.1': - resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} - engines: {node: '>=18.0.0'} - '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -5384,9 +5071,6 @@ packages: peerDependencies: '@solana/web3.js': '*' - '@speed-highlight/core@1.2.17': - resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -6769,9 +6453,6 @@ packages: resolution: {integrity: sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w==} engines: {node: '>= 10'} - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - blakejs@1.2.1: resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} @@ -7104,10 +6785,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - cookies@0.9.1: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} @@ -7504,9 +7181,6 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -8712,10 +8386,6 @@ packages: keyvaluestorage-interface@1.0.0: resolution: {integrity: sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} @@ -8984,11 +8654,6 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true - miniflare@4.20260722.0: - resolution: {integrity: sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==} - engines: {node: '>=22.0.0'} - hasBin: true - minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -9490,16 +9155,10 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} @@ -10250,10 +9909,6 @@ packages: sha256-uint8array@0.10.7: resolution: {integrity: sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==} - sharp@0.35.2: - resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} - engines: {node: '>=20.9.0'} - sharp@0.35.3: resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} engines: {node: '>=20.9.0'} @@ -10526,10 +10181,6 @@ packages: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} engines: {node: '>=14.0.0'} - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -10820,13 +10471,6 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - unidragger@3.0.1: resolution: {integrity: sha512-RngbGSwBFmqGBWjkaH+yB677uzR95blSQyxq6hYbrQCejH3Mx1nm8DVOuh3M9k2fQyTstWUG5qlgCnNqV/9jVw==} @@ -11231,21 +10875,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - workerd@1.20260722.1: - resolution: {integrity: sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==} - engines: {node: '>=16'} - hasBin: true - - wrangler@4.114.0: - resolution: {integrity: sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==} - engines: {node: '>=22.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^5.20260722.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -11348,12 +10977,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - yup@1.6.1: resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} @@ -11445,171 +11068,6 @@ snapshots: tailwindcss-animate: 1.0.7(tailwindcss@4.0.15) zod: 4.1.11 - '@aws-sdk/checksums@3.1000.19': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/client-s3@3.1092.0': - dependencies: - '@aws-sdk/checksums': 3.1000.19 - '@aws-sdk/core': 3.976.0 - '@aws-sdk/credential-provider-node': 3.972.71 - '@aws-sdk/middleware-sdk-s3': 3.972.65 - '@aws-sdk/signature-v4-multi-region': 3.996.41 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/fetch-http-handler': 5.6.9 - '@smithy/node-http-handler': 4.9.9 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/core@3.976.0': - dependencies: - '@aws-sdk/types': 3.974.2 - '@aws-sdk/xml-builder': 3.972.36 - '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.29.7 - '@smithy/signature-v4': 5.6.8 - '@smithy/types': 4.16.1 - bowser: 2.13.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.972.60': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.62': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/fetch-http-handler': 5.6.9 - '@smithy/node-http-handler': 4.9.9 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.973.5': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/credential-provider-env': 3.972.60 - '@aws-sdk/credential-provider-http': 3.972.62 - '@aws-sdk/credential-provider-login': 3.972.67 - '@aws-sdk/credential-provider-process': 3.972.60 - '@aws-sdk/credential-provider-sso': 3.973.4 - '@aws-sdk/credential-provider-web-identity': 3.972.66 - '@aws-sdk/nested-clients': 3.997.34 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/credential-provider-imds': 4.4.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-login@3.972.67': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/nested-clients': 3.997.34 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-node@3.972.71': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.60 - '@aws-sdk/credential-provider-http': 3.972.62 - '@aws-sdk/credential-provider-ini': 3.973.5 - '@aws-sdk/credential-provider-process': 3.972.60 - '@aws-sdk/credential-provider-sso': 3.973.4 - '@aws-sdk/credential-provider-web-identity': 3.972.66 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/credential-provider-imds': 4.4.12 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-process@3.972.60': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.973.4': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/nested-clients': 3.997.34 - '@aws-sdk/token-providers': 3.1092.0 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-web-identity@3.972.66': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/nested-clients': 3.997.34 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.972.65': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/signature-v4-multi-region': 3.996.41 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.997.34': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/signature-v4-multi-region': 3.996.41 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/fetch-http-handler': 5.6.9 - '@smithy/node-http-handler': 4.9.9 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.996.41': - dependencies: - '@aws-sdk/types': 3.974.2 - '@smithy/signature-v4': 5.6.8 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1092.0': - dependencies: - '@aws-sdk/core': 3.976.0 - '@aws-sdk/nested-clients': 3.997.34 - '@aws-sdk/types': 3.974.2 - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/types@3.974.2': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.36': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.3.0': {} - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -11779,14 +11237,14 @@ snapshots: '@cartridge/controller-wasm@0.3.19': {} - '@cartridge/controller@0.10.7(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@cartridge/controller@0.10.7(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@cartridge/controller-wasm': 0.3.19 '@cartridge/penpal': 6.2.4 '@starknet-io/types-js': 0.9.1 '@telegram-apps/sdk': 2.11.3 '@turnkey/sdk-browser': 4.3.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/ethereum-provider': 2.21.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/ethereum-provider': 2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) bs58: 6.0.0 cbor-x: 1.6.0 ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -11986,31 +11444,6 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@cloudflare/kv-asset-handler@0.5.0': {} - - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260722.1 - - '@cloudflare/workerd-darwin-64@1.20260722.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260722.1': - optional: true - - '@cloudflare/workerd-linux-64@1.20260722.1': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260722.1': - optional: true - - '@cloudflare/workerd-windows-64@1.20260722.1': - optional: true - - '@cloudflare/workers-types@4.20260702.1': {} - '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.3 @@ -12029,13 +11462,9 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 clsx: 1.2.1 - eventemitter3: 5.0.1 + eventemitter3: 5.0.4 preact: 10.28.2 - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - '@discoveryjs/json-ext@0.5.7': {} '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': @@ -12565,9 +11994,9 @@ snapshots: '@emurgo/cardano-serialization-lib-browser': 13.2.1 '@emurgo/cardano-serialization-lib-nodejs': 13.2.0 - '@flags-sdk/vercel@1.4.3(@aws-sdk/credential-provider-web-identity@3.972.66)(flags@4.2.0(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@flags-sdk/vercel@1.4.3(flags@4.2.0(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@vercel/flags-core': 1.5.2(@aws-sdk/credential-provider-web-identity@3.972.66)(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@vercel/flags-core': 1.5.2(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) flags: 4.2.0(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - '@aws-sdk/credential-provider-web-identity' @@ -12848,11 +12277,7 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/colour@1.1.0': optional: true '@img/sharp-darwin-arm64@0.35.3': @@ -12860,201 +12285,102 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.1 - optional: true - '@img/sharp-darwin-x64@0.35.3': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.3.2 optional: true - '@img/sharp-freebsd-wasm32@0.35.2': - dependencies: - '@img/sharp-wasm32': 0.35.2 - optional: true - '@img/sharp-freebsd-wasm32@0.35.3': dependencies: '@img/sharp-wasm32': 0.35.3 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.1': - optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true - '@img/sharp-libvips-darwin-x64@1.3.1': - optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm64@1.3.1': - optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm@1.3.1': - optional: true - '@img/sharp-libvips-linux-arm@1.3.2': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.1': - optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.1': - optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true - '@img/sharp-libvips-linux-s390x@1.3.1': - optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': optional: true - '@img/sharp-libvips-linux-x64@1.3.1': - optional: true - '@img/sharp-libvips-linux-x64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.1': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.1 - optional: true - '@img/sharp-linux-arm64@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true - '@img/sharp-linux-arm@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.1 - optional: true - '@img/sharp-linux-arm@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.3.2 optional: true - '@img/sharp-linux-ppc64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.1 - optional: true - '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true - '@img/sharp-linux-riscv64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.1 - optional: true - '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true - '@img/sharp-linux-s390x@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.1 - optional: true - '@img/sharp-linux-s390x@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true - '@img/sharp-linux-x64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.1 - optional: true - '@img/sharp-linux-x64@0.35.3': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 - optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 - optional: true - '@img/sharp-linuxmusl-x64@0.35.3': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true - '@img/sharp-wasm32@0.35.2': - dependencies: - '@emnapi/runtime': 1.11.3 - optional: true - '@img/sharp-wasm32@0.35.3': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.2': - dependencies: - '@img/sharp-wasm32': 0.35.2 - optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': dependencies: '@img/sharp-wasm32': 0.35.3 optional: true - '@img/sharp-win32-arm64@0.35.2': - optional: true - '@img/sharp-win32-arm64@0.35.3': optional: true - '@img/sharp-win32-ia32@0.35.2': - optional: true - '@img/sharp-win32-ia32@0.35.3': optional: true - '@img/sharp-win32-x64@0.35.2': - optional: true - '@img/sharp-win32-x64@0.35.3': optional: true @@ -13129,11 +12455,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -13970,18 +13291,6 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - '@posthog/cli@0.7.25': dependencies: detect-libc: 2.1.2 @@ -15383,13 +14692,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15418,13 +14727,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15453,12 +14762,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' @@ -15523,14 +14832,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) lit: 3.3.0 - valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) + valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15559,12 +14868,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) lit: 3.3.0 valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) transitivePeerDependencies: @@ -15595,14 +14904,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) lit: 3.3.0 - valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15671,12 +14980,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -15707,12 +15016,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -15743,12 +15052,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -15815,10 +15124,10 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -15850,10 +15159,10 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -15885,10 +15194,10 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -15955,16 +15264,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15993,16 +15302,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16031,15 +15340,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' @@ -16118,21 +15427,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) bs58: 6.0.0 - valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16161,21 +15470,21 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16204,20 +15513,20 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@reown/appkit@1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) bs58: 6.0.0 - valtio: 1.13.2(@types/react@19.2.3)(react@18.3.1) + valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) transitivePeerDependencies: - '@azure/app-configuration' @@ -16257,7 +15566,7 @@ snapshots: '@reown/appkit-ui': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-utils': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.3)(react@19.2.3) @@ -16541,41 +15850,6 @@ snapshots: '@sinclair/typebox@0.33.22': {} - '@sindresorhus/is@7.2.0': {} - - '@smithy/core@3.29.7': - dependencies: - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.4.12': - dependencies: - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.6.9': - dependencies: - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.9.9': - dependencies: - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.6.8': - dependencies: - '@smithy/core': 3.29.7 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - - '@smithy/types@4.16.1': - dependencies: - tslib: 2.8.1 - '@socket.io/component-emitter@3.1.2': {} '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': @@ -17284,11 +16558,11 @@ snapshots: '@solana/wallet-standard-util': 1.1.2 '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/wallet-adapter-walletconnect@0.1.21(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@solana/wallet-adapter-walletconnect@0.1.21(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/solana-adapter': 0.0.8(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/solana-adapter': 0.0.8(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17317,7 +16591,7 @@ snapshots: - utf-8-validate - zod - '@solana/wallet-adapter-wallets@0.19.38(@babel/runtime@7.28.4)(@solana/sysvars@2.3.0(typescript@5.9.3))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bs58@6.0.0)(bufferutil@4.1.0)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@solana/wallet-adapter-wallets@0.19.38(@babel/runtime@7.28.4)(@solana/sysvars@2.3.0(typescript@5.9.3))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bs58@6.0.0)(bufferutil@4.1.0)(react@19.2.3)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: '@solana/wallet-adapter-alpha': 0.1.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/wallet-adapter-avana': 0.1.17(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -17353,7 +16627,7 @@ snapshots: '@solana/wallet-adapter-trezor': 0.1.6(@solana/sysvars@2.3.0(typescript@5.9.3))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.1.0)(tslib@2.8.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@solana/wallet-adapter-trust': 0.1.17(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/wallet-adapter-unsafe-burner': 0.1.11(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) - '@solana/wallet-adapter-walletconnect': 0.1.21(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@solana/wallet-adapter-walletconnect': 0.1.21(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@solana/wallet-adapter-xdefi': 0.1.11(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -17460,8 +16734,6 @@ snapshots: eventemitter3: 5.0.4 uuid: 11.1.1 - '@speed-highlight/core@1.2.17': {} - '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -18599,9 +17871,9 @@ snapshots: dependencies: execa: 5.1.1 - '@vercel/flags-core@1.5.2(@aws-sdk/credential-provider-web-identity@3.972.66)(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@vercel/flags-core@1.5.2(next@15.5.22(@types/node@24.10.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@vercel/functions': 3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@vercel/functions': 3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@vercel/oidc': 3.5.0 jose: 5.2.1 js-xxhash: 4.0.0 @@ -18611,11 +17883,10 @@ snapshots: - '@aws-sdk/credential-provider-web-identity' - ws - '@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@vercel/oidc': 3.8.0 optionalDependencies: - '@aws-sdk/credential-provider-web-identity': 3.972.66 ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@vercel/oidc@3.5.0': {} @@ -18767,21 +18038,21 @@ snapshots: dependencies: '@wallet-standard/base': 1.1.1 - '@walletconnect/core@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -18811,21 +18082,21 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/core@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -18862,13 +18133,13 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/utils': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 @@ -18899,21 +18170,21 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.23.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/core@2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 3.0.1 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.1 - '@walletconnect/utils': 2.23.1(typescript@5.9.3)(zod@4.1.11) + '@walletconnect/types': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.1.11) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -18947,18 +18218,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -18988,18 +18259,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19029,18 +18300,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/ethereum-provider@2.21.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/sign-client': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/utils': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19077,9 +18348,9 @@ snapshots: '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/sign-client': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/universal-provider': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/utils': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) events: 3.3.0 @@ -19158,11 +18429,11 @@ snapshots: - bufferutil - utf-8-validate - '@walletconnect/keyvaluestorage@1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@walletconnect/keyvaluestorage@1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.2 - unstorage: 1.17.3(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(idb-keyval@6.2.2) + unstorage: 1.17.3(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(idb-keyval@6.2.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -19209,16 +18480,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19245,16 +18516,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/sign-client@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@walletconnect/core': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/core': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19289,7 +18560,7 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/utils': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) events: 3.3.0 transitivePeerDependencies: @@ -19317,16 +18588,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.23.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/sign-client@2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: - '@walletconnect/core': 2.23.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/core': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 3.0.1 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.1 - '@walletconnect/utils': 2.23.1(typescript@5.9.3)(zod@4.1.11) + '@walletconnect/types': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.1.11) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19353,13 +18624,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/solana-adapter@0.0.8(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/solana-adapter@0.0.8(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.19.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.19.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -19393,12 +18664,12 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/types@2.19.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.19.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -19422,12 +18693,12 @@ snapshots: - ioredis - uploadthing - '@walletconnect/types@2.21.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.21.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -19451,12 +18722,12 @@ snapshots: - ioredis - uploadthing - '@walletconnect/types@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': + '@walletconnect/types@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 events: 3.3.0 transitivePeerDependencies: @@ -19480,12 +18751,12 @@ snapshots: - ioredis - uploadthing - '@walletconnect/types@2.23.1': + '@walletconnect/types@2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 3.0.1 events: 3.3.0 transitivePeerDependencies: @@ -19509,18 +18780,18 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.39.3 events: 3.3.0 transitivePeerDependencies: @@ -19549,18 +18820,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/universal-provider@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/sign-client': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/utils': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) es-toolkit: 1.39.3 events: 3.3.0 transitivePeerDependencies: @@ -19596,10 +18867,10 @@ snapshots: '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 '@walletconnect/sign-client': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/utils': 2.21.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) es-toolkit: 1.39.3 events: 3.3.0 @@ -19629,18 +18900,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.19.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.19.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 '@noble/hashes': 1.8.0 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.19.0(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.19.0(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 detect-browser: 5.3.0 @@ -19673,7 +18944,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@msgpack/msgpack': 3.1.2 '@noble/ciphers': 1.3.0 @@ -19681,12 +18952,12 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 blakejs: 1.2.1 @@ -19720,7 +18991,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': + '@walletconnect/utils@2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11)': dependencies: '@msgpack/msgpack': 3.1.2 '@noble/ciphers': 1.3.0 @@ -19728,12 +18999,12 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 blakejs: 1.2.1 @@ -19775,12 +19046,12 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/types': 2.21.8(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 blakejs: 1.2.1 @@ -19814,7 +19085,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.23.1(typescript@5.9.3)(zod@4.1.11)': + '@walletconnect/utils@2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(typescript@5.9.3)(zod@4.1.11)': dependencies: '@msgpack/msgpack': 3.1.2 '@noble/ciphers': 1.3.0 @@ -19822,13 +19093,13 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) + '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/logger': 3.0.1 '@walletconnect/relay-api': 1.0.11 '@walletconnect/relay-auth': 1.1.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.1 + '@walletconnect/types': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))) '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 blakejs: 1.2.1 @@ -20244,8 +19515,6 @@ snapshots: node-gyp-build: 4.8.4 readable-stream: 3.6.2 - blake3-wasm@2.1.5: {} - blakejs@1.2.1: {} bn.js@4.12.3: {} @@ -20626,8 +19895,6 @@ snapshots: cookie@0.7.2: {} - cookie@1.1.1: {} - cookies@0.9.1: dependencies: depd: 2.0.0 @@ -21029,8 +20296,6 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - error-stack-parser-es@1.0.5: {} - es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -22599,8 +21864,6 @@ snapshots: keyvaluestorage-interface@1.0.0: {} - kleur@4.1.5: {} - koa-compose@4.1.0: {} koa-convert@2.0.0: @@ -22872,18 +22135,6 @@ snapshots: mini-svg-data-uri@1.4.4: {} - miniflare@4.20260722.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.35.2 - undici: 7.28.0 - workerd: 1.20260722.1 - ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - minimalistic-assert@1.0.1: {} minimalistic-crypto-utils@1.0.1: {} @@ -23374,12 +22625,8 @@ snapshots: path-to-regexp@0.1.13: {} - path-to-regexp@6.3.0: {} - path-type@4.0.0: {} - pathe@2.0.3: {} - pause-stream@0.0.11: dependencies: through: 2.3.8 @@ -24292,38 +23539,6 @@ snapshots: sha256-uint8array@0.10.7: {} - sharp@0.35.2: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.2 - '@img/sharp-darwin-x64': 0.35.2 - '@img/sharp-freebsd-wasm32': 0.35.2 - '@img/sharp-libvips-darwin-arm64': 1.3.1 - '@img/sharp-libvips-darwin-x64': 1.3.1 - '@img/sharp-libvips-linux-arm': 1.3.1 - '@img/sharp-libvips-linux-arm64': 1.3.1 - '@img/sharp-libvips-linux-ppc64': 1.3.1 - '@img/sharp-libvips-linux-riscv64': 1.3.1 - '@img/sharp-libvips-linux-s390x': 1.3.1 - '@img/sharp-libvips-linux-x64': 1.3.1 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 - '@img/sharp-linux-arm': 0.35.2 - '@img/sharp-linux-arm64': 0.35.2 - '@img/sharp-linux-ppc64': 0.35.2 - '@img/sharp-linux-riscv64': 0.35.2 - '@img/sharp-linux-s390x': 0.35.2 - '@img/sharp-linux-x64': 0.35.2 - '@img/sharp-linuxmusl-arm64': 0.35.2 - '@img/sharp-linuxmusl-x64': 0.35.2 - '@img/sharp-webcontainers-wasm32': 0.35.2 - '@img/sharp-win32-arm64': 0.35.2 - '@img/sharp-win32-ia32': 0.35.2 - '@img/sharp-win32-x64': 0.35.2 - sharp@0.35.3(@types/node@20.19.28): dependencies: '@img/colour': 1.1.0 @@ -24565,16 +23780,16 @@ snapshots: pako: 2.1.0 ts-mixer: 6.0.4 - starknetkit@3.4.3(@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3))(@scure/bip39@1.6.0)(@types/react@19.2.3)(bufferutil@4.1.0)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@12.26.2(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(starknet@8.9.2)(swr@2.3.8(react@19.2.3))(tailwind-merge@3.4.0)(tailwindcss-animate@1.0.7(tailwindcss@4.0.15))(tailwindcss@4.0.15)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11): + starknetkit@3.4.3(@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3))(@scure/bip39@1.6.0)(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@12.26.2(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(starknet@8.9.2)(swr@2.3.8(react@19.2.3))(tailwind-merge@3.4.0)(tailwindcss-animate@1.0.7(tailwindcss@4.0.15))(tailwindcss@4.0.15)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11): dependencies: '@argent/x-ui': 1.109.1(@radix-ui/react-slot@1.2.4(@types/react@19.2.3)(react@19.2.3))(@scure/bip39@1.6.0)(@starknet-io/types-js@0.8.4)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@12.26.2(@emotion/is-prop-valid@0.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(lodash-es@4.18.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(starknet@8.9.2)(swr@2.3.8(react@19.2.3))(tailwind-merge@3.4.0)(tailwindcss-animate@1.0.7(tailwindcss@4.0.15))(tailwindcss@4.0.15)(zod@4.1.11) - '@cartridge/controller': 0.10.7(@types/react@19.2.3)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@cartridge/controller': 0.10.7(@types/react@19.2.3)(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) '@starknet-io/get-starknet': 4.0.8 '@starknet-io/get-starknet-core': 4.0.8 '@starknet-io/types-js': 0.8.4 '@trpc/client': 10.45.4(@trpc/server@10.45.4) '@trpc/server': 10.45.4 - '@walletconnect/sign-client': 2.23.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) + '@walletconnect/sign-client': 2.23.1(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.11) bowser: 2.13.1 detect-browser: 5.3.0 eventemitter3: 5.0.4 @@ -24783,8 +23998,6 @@ snapshots: superstruct@2.0.2: {} - supports-color@10.2.2: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -25079,12 +24292,6 @@ snapshots: undici-types@7.18.2: {} - undici@7.28.0: {} - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - unidragger@3.0.1: dependencies: ev-emitter: 2.1.2 @@ -25121,7 +24328,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.3(@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(idb-keyval@6.2.2): + unstorage@1.17.3(@vercel/functions@3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)))(idb-keyval@6.2.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -25132,7 +24339,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.4 optionalDependencies: - '@vercel/functions': 3.7.5(@aws-sdk/credential-provider-web-identity@3.972.66)(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@vercel/functions': 3.7.5(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) idb-keyval: 6.2.2 upath@2.0.1: {} @@ -25670,31 +24877,6 @@ snapshots: wordwrap@1.0.0: {} - workerd@1.20260722.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260722.1 - '@cloudflare/workerd-darwin-arm64': 1.20260722.1 - '@cloudflare/workerd-linux-64': 1.20260722.1 - '@cloudflare/workerd-linux-arm64': 1.20260722.1 - '@cloudflare/workerd-windows-64': 1.20260722.1 - - wrangler@4.114.0(@cloudflare/workers-types@4.20260702.1)(bufferutil@4.1.0)(utf-8-validate@5.0.10): - dependencies: - '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) - blake3-wasm: 2.1.5 - esbuild: 0.28.1 - miniflare: 4.20260722.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260722.1 - optionalDependencies: - '@cloudflare/workers-types': 4.20260702.1 - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -25801,19 +24983,6 @@ snapshots: yocto-queue@0.1.0: {} - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.17 - cookie: 1.1.1 - youch-core: 0.3.3 - yup@1.6.1: dependencies: property-expr: 2.0.6