diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d1dbf21..130f283a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: done test: - name: Test + name: Unit Tests runs-on: ubuntu-latest steps: - name: Checkout @@ -113,93 +113,30 @@ jobs: - name: Run tests run: pnpm test - integration-test: - name: Integration Tests - runs-on: ubuntu-latest + call-tests: + name: Tests if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - needs: [build] - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run integration tests - run: pnpm test:integration - env: - ZD_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }} - - e2e-test: - name: Browser E2E Tests + uses: ./.github/workflows/test.yml + with: + use_real_email: false + secrets: inherit + + all-checks-pass: + name: All Checks Pass + if: always() + needs: + - lint-and-format + - typecheck + - build + - test + - call-tests runs-on: ubuntu-latest - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - needs: [build] steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 20 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build SDK - run: pnpm build - - - name: Install Playwright browsers - run: pnpm exec playwright install --with-deps chromium - - - name: Build and start demo app - working-directory: apps/zerodev-signer-demo + - name: Verify required checks run: | - pnpm build - pnpm start & - env: - NEXT_PUBLIC_ZERODEV_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }} - NEXT_PUBLIC_KMS_PROXY_BASE_URL: https://kms.staging.zerodev.app/api/v1 - # Pin the bundler/paymaster to staging so the AA stack matches the - # staging KMS above (the SDK default host is prod). - NEXT_PUBLIC_ZERODEV_AA_HOST: https://staging-meta-aa-provider.onrender.com - - - name: Wait for demo app - run: | - for i in $(seq 1 15); do - if curl -sf http://localhost:3000 > /dev/null 2>&1; then - echo "Demo app is ready" - exit 0 - fi - echo "Waiting for demo app... ($i/15)" - sleep 2 - done - echo "Demo app failed to start" - exit 1 - - - name: Run E2E tests - run: pnpm test:e2e - env: - DEMO_APP_URL: http://localhost:3000 - - - name: Upload Playwright report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 14 \ No newline at end of file + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more required checks failed or were cancelled." + echo "Results: ${{ toJSON(needs.*.result) }}" + exit 1 + fi + echo "All required checks passed." diff --git a/.github/workflows/scheduled-tests.yml b/.github/workflows/scheduled-tests.yml new file mode 100644 index 00000000..5c1d3982 --- /dev/null +++ b/.github/workflows/scheduled-tests.yml @@ -0,0 +1,20 @@ +name: Scheduled Tests + +on: + schedule: + - cron: '0 8 * * *' + workflow_dispatch: + inputs: + use_real_email: + description: 'Use real email service (mail.tm) for OTP tests' + required: false + default: true + type: boolean + +jobs: + scheduled-tests: + name: Scheduled Tests (Real Email) + uses: ./.github/workflows/test.yml + with: + use_real_email: ${{ github.event_name == 'workflow_dispatch' && inputs.use_real_email || true }} + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..1aec6e10 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,130 @@ +name: Tests + +on: + workflow_call: + inputs: + use_real_email: + description: 'Use real email service (mail.tm) for OTP tests' + required: false + default: true + type: boolean + secrets: + ZD_PROJECT_ID: + required: true + workflow_dispatch: + inputs: + use_real_email: + description: 'Use real email service (mail.tm) for OTP tests' + required: false + default: true + type: boolean + +jobs: + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + steps: + - name: Annotate email mode + run: | + if [ "${{ inputs.use_real_email }}" = "true" ]; then + echo "::notice::Email mode: real (Guerrilla Mail + Turnkey)" + else + echo "::notice::Email mode: mock (no external dependencies)" + fi + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run integration tests + run: pnpm test:integration + env: + ZD_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }} + USE_REAL_EMAIL: ${{ inputs.use_real_email }} + + e2e-test: + name: Browser E2E Tests + runs-on: ubuntu-latest + steps: + - name: Annotate email mode + run: | + if [ "${{ inputs.use_real_email }}" = "true" ]; then + echo "::notice::Email mode: real (Guerrilla Mail + Turnkey)" + else + echo "::notice::Email mode: mock (no external dependencies)" + fi + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build SDK + run: pnpm build + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + + - name: Build and start demo app + working-directory: apps/zerodev-signer-demo + run: | + if [ "$USE_REAL_EMAIL" = "false" ]; then + export NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY=$(cat ../../e2e/fixtures/test-signer-public-key.txt) + fi + pnpm build + pnpm start & + env: + NEXT_PUBLIC_ZERODEV_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }} + NEXT_PUBLIC_KMS_PROXY_BASE_URL: https://kms.staging.zerodev.app/api/v1 + # Pin the bundler/paymaster to staging so the AA stack matches the + # staging KMS above (the SDK default host is prod). + NEXT_PUBLIC_ZERODEV_AA_HOST: https://staging-meta-aa-provider.onrender.com + USE_REAL_EMAIL: ${{ inputs.use_real_email }} + + - name: Wait for demo app + run: | + for i in $(seq 1 15); do + if curl -sf http://localhost:3000 > /dev/null 2>&1; then + echo "Demo app is ready" + exit 0 + fi + echo "Waiting for demo app... ($i/15)" + sleep 2 + done + echo "Demo app failed to start" + exit 1 + + - name: Run E2E tests + run: pnpm test:e2e + env: + DEMO_APP_URL: http://localhost:3000 + USE_REAL_EMAIL: ${{ inputs.use_real_email }} + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 14 diff --git a/apps/zerodev-signer-demo/.env.example b/apps/zerodev-signer-demo/.env.example index 865351f5..97a45ff7 100644 --- a/apps/zerodev-signer-demo/.env.example +++ b/apps/zerodev-signer-demo/.env.example @@ -3,3 +3,6 @@ NEXT_PUBLIC_SEPOLIA_RPC_URL= NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL= NEXT_PUBLIC_ARB_SEPOLIA_RPC_URL= NEXT_PUBLIC_ZERODEV_PROJECT_ID= + +# For E2E test mocking only — never set in production +# NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY= diff --git a/apps/zerodev-signer-demo/src/app/wagmi-config.tsx b/apps/zerodev-signer-demo/src/app/wagmi-config.tsx index 011782f1..5824a572 100644 --- a/apps/zerodev-signer-demo/src/app/wagmi-config.tsx +++ b/apps/zerodev-signer-demo/src/app/wagmi-config.tsx @@ -44,6 +44,9 @@ export const config = createConfig({ organizationId: process.env.NEXT_PUBLIC_ORG_ID, }), ...(mode && { mode }), + ...(process.env.NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY && { + dangerouslyOverrideOtpSignerPublicKey: process.env.NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY, + }), config: { logo: , auth: { diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..6bf5447a --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,77 @@ +# E2E Tests + +## Two-mode testing strategy + +Browser tests run in two modes controlled by the `USE_REAL_EMAIL` environment variable. + +| | Mock mode (`USE_REAL_EMAIL=false`) | Real-email mode (`USE_REAL_EMAIL=true`) | +|---|---|---| +| **When** | PR CI, local development | Nightly scheduled run | +| **Email** | Intercepted via `page.route()` | mail.tm temporary inbox | +| **Auth session** | Fake JWT (never hits backend) | Real session from ZeroDev KMS | +| **Signing** | Fake signature stub | Real ZeroDev KMS signing | +| **Wallet address** | Indeterminate (no real account) | Real counterfactual address | +| **Project config** | Not enforced | Enforced (chains, sponsorship) | +| **Speed** | Fast, no network waits | Slower, dependent on external services | + +### What mock mode does NOT cover + +Mock mode verifies UI flow and component wiring. It deliberately bypasses: + +- **Session validation** — the fake JWT is never checked against the database; user/org do not exist. +- **Project configuration** — allowed chains, permitted operations, and gas sponsorship rules are never evaluated. +- **Cryptographic signing** — the returned signature is a deterministic stub; it is not valid for any message and is not tied to any key or wallet address. +- **Real wallet address** — no account is created; wallet address is meaningless in mock mode. + +These gaps exist because mocking auth cascades: once auth is mocked the backend rejects all subsequent authenticated requests, so signing endpoints must also be mocked. This is an accepted tradeoff — the nightly real-email run covers correctness end-to-end. + +### What mock mode does cover + +- OTP entry form renders and accepts input +- Magic-link entry form renders and accepts input +- Error states (wrong code, expired session) — by adjusting fixture responses +- Post-auth UI (wallet address display, sign message, logout flow) +- Component composition in `@zerodev/wallet-react-kit` + +## Directory structure + +``` +e2e/ +├── browser/ # Playwright tests (UI + integration) +│ ├── otp.spec.ts +│ ├── magic-link.spec.ts +│ └── post-auth.spec.ts +├── integration/ # Vitest tests against real backend (no UI) +├── fixtures/ +│ ├── auth.ts # Playwright fixture extensions (OTP, magic-link, authenticated sessions) +│ ├── test-otp-bundle.json # Pre-signed EncryptionTargetEnvelope for mock mode +│ ├── test-signer-public-key.txt # enclaveQuorumPublic hex for the test key pair +│ └── README.md # Fixture regeneration instructions +├── helpers/ +│ ├── mock-backend.ts # page.route() intercepts for auth + signing +│ ├── mock-session.ts # Fake session JWT builder +│ └── env-utils.ts # isRealEmail() helper +└── scripts/ + └── generate-test-otp-bundle.ts # One-time fixture generator +``` + +## Running tests locally + +```bash +# Mock mode (default — no email service needed) +pnpm test:e2e:headed + +# Real-email mode +USE_REAL_EMAIL=true pnpm test:e2e:headed +``` + +`USE_REAL_EMAIL` unset is treated as `false` — mock mode is the default so tests work out of the box without any configuration. + +## CI workflows + +- **`ci.yml`** calls `test.yml` with `use_real_email: false` on every PR and push to `main`/`develop`. +- **`scheduled-tests.yml`** calls `test.yml` with `use_real_email: true` daily at 08:00 UTC. + +## Fixture maintenance + +The pre-signed OTP bundle in `e2e/fixtures/` must be regenerated when `encryptOtpAttempt` validation logic or the `EncryptionTargetEnvelope` shape changes. See [`e2e/fixtures/README.md`](fixtures/README.md) for instructions. diff --git a/e2e/browser/magic-link.spec.ts b/e2e/browser/magic-link.spec.ts index d7bdab40..481af16d 100644 --- a/e2e/browser/magic-link.spec.ts +++ b/e2e/browser/magic-link.spec.ts @@ -2,40 +2,31 @@ * Browser E2E test for the Magic Link authentication flow. * * Tests the magic link flow through the demo app UI: - * 1. Create temp email + * 1. Create temp email (or use mock) * 2. Navigate to login page * 3. Enter email and click "Continue with email magic link" * 4. Wait for "Magic link sent" confirmation - * 5. Poll for email, extract OTP code from the magic link URL + * 5. Resolve OTP code (mock: fixture constant; real: poll inbox and extract from URL) * 6. Navigate to the magic link URL (simulating email click) * 7. Verify auto-verification succeeds and redirects to /dashboard + * + * The `magicLinkSession` fixture handles USE_REAL_EMAIL branching; no if/else here. */ -import { expect, test } from '@playwright/test' +import { expect, test } from '../fixtures/auth.js' import { EMAIL_POLL_INTERVAL_MS, EMAIL_POLL_TIMEOUT_MS, } from '../helpers/constants.js' import { extractOtpCodeFromMagicLinkUrl } from '../helpers/otp-utils.js' -import { - createNewAccount, - ping, - searchForNewEmail, -} from '../helpers/temp-email.js' +import { searchForNewEmail } from '../helpers/temp-email.js' test.describe('Magic Link Flow', () => { - test.beforeEach(async () => { - try { - await ping() - } catch { - test.skip(true, 'Email service unavailable') - } - }) - - test('should complete magic link login through the UI', async ({ page }) => { - // Step 1: Create temp email - const emailAccount = await createNewAccount() - const email = emailAccount.address + test('should complete magic link login through the UI', async ({ + page, + magicLinkSession, + }) => { + const email = magicLinkSession.email // Step 2: Seed the demo's email-method choice so wagmi-config picks // magic-link on first paint. Then navigate. @@ -55,13 +46,16 @@ test.describe('Magic Link Flow', () => { timeout: 30_000, }) - // Step 6: Poll for email and extract OTP code from the magic link URL - const emailContent = await searchForNewEmail( - emailAccount.authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - const otpCode = extractOtpCodeFromMagicLinkUrl(emailContent) + // Step 6: Resolve OTP code — use fixture value in mock mode, poll inbox in real mode + const otpCode = + magicLinkSession.otpCode ?? + extractOtpCodeFromMagicLinkUrl( + await searchForNewEmail( + magicLinkSession.authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, + ), + ) expect(otpCode).toBeTruthy() console.log(`Extracted magic link code: ${otpCode}`) diff --git a/e2e/browser/otp.spec.ts b/e2e/browser/otp.spec.ts index ac6e4e8d..753dbc36 100644 --- a/e2e/browser/otp.spec.ts +++ b/e2e/browser/otp.spec.ts @@ -2,45 +2,35 @@ * Browser E2E test for the OTP authentication flow. * * Tests the full OTP flow through the demo app UI: - * 1. Create temp email + * 1. Create temp email (or use mock) * 2. Navigate to login page * 3. Enter email and click "Continue with email OTP code" * 4. Wait for OTP verification step - * 5. Poll for email, extract OTP code + * 5. Resolve OTP code (mock: fixture constant; real: poll inbox) * 6. Enter OTP code and click "Verify and continue" * 7. Verify redirect to /dashboard * 8. Verify wallet address and balance are displayed + * + * The `otpSession` fixture handles USE_REAL_EMAIL branching; no if/else here. */ -import { expect, test } from '@playwright/test' +import { expect, test } from '../fixtures/auth.js' import { EMAIL_POLL_INTERVAL_MS, EMAIL_POLL_TIMEOUT_MS, } from '../helpers/constants.js' +import { extractOtpCode } from '../helpers/otp-utils.js' +import { searchForNewEmail } from '../helpers/temp-email.js' // Demo app uses 6-digit OTP codes (configured in zerodev-signer-demo) const DEMO_APP_OTP_LENGTH = 6 -import { extractOtpCode } from '../helpers/otp-utils.js' -import { - createNewAccount, - ping, - searchForNewEmail, -} from '../helpers/temp-email.js' - test.describe('OTP Flow', () => { - test.beforeEach(async () => { - try { - await ping() - } catch { - test.skip(true, 'Email service unavailable') - } - }) - - test('should complete OTP login through the UI', async ({ page }) => { - // Step 1: Create temp email - const emailAccount = await createNewAccount() - const email = emailAccount.address + test('should complete OTP login through the UI', async ({ + page, + otpSession, + }) => { + const email = otpSession.email // Step 2: Seed the demo's email-method choice so wagmi-config picks OTP // on first paint. Then navigate. @@ -61,17 +51,24 @@ test.describe('OTP Flow', () => { }), ).toBeVisible({ timeout: 30_000 }) - // Step 6: Poll for email and extract OTP code - const emailContent = await searchForNewEmail( - emailAccount.authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - const otpCode = extractOtpCode(emailContent, DEMO_APP_OTP_LENGTH, true) + // Step 6: Resolve OTP code — use fixture value in mock mode, poll inbox in real mode + const otpCode = + otpSession.otpCode ?? + extractOtpCode( + await searchForNewEmail( + otpSession.authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, + ), + DEMO_APP_OTP_LENGTH, + true, + ) expect(otpCode).toBeTruthy() // Step 7: Enter OTP code - await page.getByLabel('Verification code').fill(otpCode!) + // CodeInput uses a hidden input (opacity:0) with autoFocus. Type via keyboard + // so React's synthetic onChange fires correctly on each character. + await page.keyboard.type(otpCode!) // Step 8: Click verify await page.getByRole('button', { name: /Confirm code/i }).click() diff --git a/e2e/browser/passkey.spec.ts b/e2e/browser/passkey.spec.ts index c3836251..c7504eb0 100644 --- a/e2e/browser/passkey.spec.ts +++ b/e2e/browser/passkey.spec.ts @@ -14,12 +14,20 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' +import { isRealEmail } from '../helpers/env-utils.js' import { setupVirtualAuthenticator, teardownVirtualAuthenticator, type VirtualAuthenticator, } from '../helpers/virtual-authenticator.js' +// Passkey tests register real passkeys against Turnkey and create on-chain +// accounts — they require the full backend stack, not just email mocking. +test.skip( + !isRealEmail(), + 'Passkey tests require real Turnkey + backend (USE_REAL_EMAIL=true)', +) + /** * Sample EIP-712 typed data using Arbitrum Sepolia chainId (421614). * Must match a chain allowed by the project, otherwise the backend rejects it. diff --git a/e2e/browser/post-auth.spec.ts b/e2e/browser/post-auth.spec.ts index f1044f80..b115f3d8 100644 --- a/e2e/browser/post-auth.spec.ts +++ b/e2e/browser/post-auth.spec.ts @@ -6,19 +6,24 @@ * 2. Send a transaction via the "Send Transaction" tab * 3. Copy wallet address * 4. Logout and verify redirect to login page + * + * The `otpSession` fixture handles USE_REAL_EMAIL branching; no if/else here. + * Signing and minting tests are skipped in mock mode (require real Turnkey). */ -import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect, type OtpSession, test } from '../fixtures/auth.js' import { EMAIL_POLL_INTERVAL_MS, EMAIL_POLL_TIMEOUT_MS, } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' +import { extractOtpCode } from '../helpers/otp-utils.js' +import { searchForNewEmail } from '../helpers/temp-email.js' // Demo app uses 6-digit OTP codes (configured in zerodev-signer-demo) const DEMO_APP_OTP_LENGTH = 6 -import { extractOtpCode } from '../helpers/otp-utils.js' - /** * Sample EIP-712 typed data using Arbitrum Sepolia chainId (421614). * Must match a chain allowed by the project, otherwise the backend rejects it. @@ -59,36 +64,34 @@ const TYPED_DATA_SAMPLE = JSON.stringify( 2, ) -import { - createNewAccount, - ping, - searchForNewEmail, -} from '../helpers/temp-email.js' - -/** Helper to complete OTP login through the UI */ -async function loginWithOtp( - page: import('@playwright/test').Page, - email: string, - authToken: string, -) { +/** Helper to complete OTP login through the UI using a session fixture. */ +async function loginWithOtp(page: Page, session: OtpSession) { await page.addInitScript(() => { localStorage.setItem('zd:emailAuthMethod', 'otp') }) await page.goto('/') - await page.getByPlaceholder('Enter your email').fill(email) + await page.getByPlaceholder('Enter your email').fill(session.email) await page.getByPlaceholder('Enter your email').press('Enter') await expect( - page.getByText(`Enter the code from the email we sent to ${email}`, { - exact: false, - }), + page.getByText( + `Enter the code from the email we sent to ${session.email}`, + { + exact: false, + }, + ), ).toBeVisible({ timeout: 30_000 }) - const emailContent = await searchForNewEmail( - authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - const otpCode = extractOtpCode(emailContent, DEMO_APP_OTP_LENGTH, true) + const otpCode = + session.otpCode ?? + extractOtpCode( + await searchForNewEmail( + session.authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, + ), + DEMO_APP_OTP_LENGTH, + true, + ) expect(otpCode).toBeTruthy() await page.getByLabel('Verification code').fill(otpCode!) @@ -101,17 +104,11 @@ async function loginWithOtp( } test.describe('Post-Auth Operations', () => { - test.beforeEach(async () => { - try { - await ping() - } catch { - test.skip(true, 'Email service unavailable') - } - }) - - test('should sign a message after login', async ({ page }) => { - const emailAccount = await createNewAccount() - await loginWithOtp(page, emailAccount.address, emailAccount.authToken) + test('should sign a message after login', async ({ + page, + authenticatedSession, + }) => { + await loginWithOtp(page, authenticatedSession) // Click the "Sign Message" tab (in the navigation area) await page @@ -135,9 +132,11 @@ test.describe('Post-Auth Operations', () => { console.log('Message signed successfully') }) - test('should sign typed data (EIP-712) after login', async ({ page }) => { - const emailAccount = await createNewAccount() - await loginWithOtp(page, emailAccount.address, emailAccount.authToken) + test('should sign typed data (EIP-712) after login', async ({ + page, + authenticatedSession, + }) => { + await loginWithOtp(page, authenticatedSession) // Click the "Sign Message" tab await page @@ -166,9 +165,15 @@ test.describe('Post-Auth Operations', () => { console.log('Typed data (EIP-712) signed successfully') }) - test('should mint NFT (send transaction) after login', async ({ page }) => { - const emailAccount = await createNewAccount() - await loginWithOtp(page, emailAccount.address, emailAccount.authToken) + test('should mint NFT (send transaction) after login', async ({ + page, + otpSession, + }, testInfo) => { + testInfo.skip( + !isRealEmail(), + 'Mint requires real Turnkey + chain (Anvil planned)', + ) + await loginWithOtp(page, otpSession) // Navigate to the "Gas-free Mint" tab await page @@ -187,9 +192,11 @@ test.describe('Post-Auth Operations', () => { console.log('Mint NFT (send transaction) successful') }) - test('should logout and redirect to login page', async ({ page }) => { - const emailAccount = await createNewAccount() - await loginWithOtp(page, emailAccount.address, emailAccount.authToken) + test('should logout and redirect to login page', async ({ + page, + otpSession, + }) => { + await loginWithOtp(page, otpSession) // Click logout await page.getByRole('button', { name: /Logout/i }).click() diff --git a/e2e/fixtures/README.md b/e2e/fixtures/README.md new file mode 100644 index 00000000..f39f0045 --- /dev/null +++ b/e2e/fixtures/README.md @@ -0,0 +1,34 @@ +# E2E Test Fixtures + +## `test-otp-bundle.json` + `test-signer-public-key.txt` + +These two files are used by Playwright tests when `USE_REAL_EMAIL=false` (the default for PR CI and local runs). They allow the tests to bypass Turnkey's pinned production signing key without hitting a real email inbox. + +### How they are used + +- `test-otp-bundle.json` is returned by the mocked `registerWithOTP` response via `page.route()`. It is a valid `EncryptionTargetEnvelope` signed with a test key pair rather than Turnkey's production key. +- `test-signer-public-key.txt` contains the `enclaveQuorumPublic` hex for that test key pair. It is injected into the demo app at build time as `NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY` so `encryptOtpAttempt` accepts the test bundle. + +Both files are committed. The private key that signed them is not stored anywhere — it is discarded after generation and is not needed again unless you regenerate. + +### When to regenerate + +Regenerate (and commit both files) when any of the following change: + +- The `encryptOtpAttempt` validation logic (new bundle version, different algorithm, extra required fields) +- The `EncryptionTargetEnvelope` type shape in `packages/core/src/utils/encryptOtpAttempt.ts` +- `@noble/curves` is upgraded to a version with a breaking API change that causes existing fixtures to fail verification + +### How to regenerate + +From the repo root: + +```bash +pnpm tsx e2e/scripts/generate-test-otp-bundle.ts +``` + +Commit both output files. CI reads the public key at build time: + +```bash +NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY=$(cat e2e/fixtures/test-signer-public-key.txt) +``` diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts new file mode 100644 index 00000000..8f365962 --- /dev/null +++ b/e2e/fixtures/auth.ts @@ -0,0 +1,102 @@ +/** + * Playwright auth fixtures that centralise USE_REAL_EMAIL branching. + * + * Consumers import `test` and `expect` from this module instead of + * `@playwright/test`. The `otpSession` and `magicLinkSession` fixtures + * automatically: + * - In mock mode : register route mocks and return a known OTP code. + * - In real-email mode: ping the email service (skip on failure), create a + * temp inbox, and return the authToken for later polling. + */ + +import { test as base, expect } from '@playwright/test' +import { isRealEmail } from '../helpers/env-utils.js' +import { setupOtpMocks, setupSigningMocks } from '../helpers/mock-backend.js' +import { + createRealEmailOtpSession, + type OtpSession, +} from '../helpers/otp-session.js' +import { setupPageTracing } from '../helpers/tracing.js' + +export type { OtpSession } + +type AuthFixtures = { + _tracing: void + otpSession: OtpSession + magicLinkSession: OtpSession + authenticatedSession: OtpSession // OTP + signing mocks combined +} + +export const test = base.extend({ + // Auto fixture: runs for every test that imports from this module. + _tracing: [ + async ({ page }, use) => { + await setupPageTracing(page) + await use() + }, + { auto: true }, + ], + + otpSession: async ({ page }, use, testInfo) => { + if (isRealEmail()) { + let session: OtpSession + try { + session = await createRealEmailOtpSession() + } catch { + testInfo.skip(true, 'Email service unavailable') + return + } + await use(session) + } else { + const { otpCode } = await setupOtpMocks(page) + await use({ + email: `mock-${Date.now()}@test.example.com`, + otpCode, + authToken: null, + }) + } + }, + + magicLinkSession: async ({ page }, use, testInfo) => { + if (isRealEmail()) { + let session: OtpSession + try { + session = await createRealEmailOtpSession() + } catch { + testInfo.skip(true, 'Email service unavailable') + return + } + await use(session) + } else { + const { otpCode } = await setupOtpMocks(page) + await use({ + email: `mock-${Date.now()}@test.example.com`, + otpCode, + authToken: null, + }) + } + }, + + authenticatedSession: async ({ page }, use, testInfo) => { + if (isRealEmail()) { + let session: OtpSession + try { + session = await createRealEmailOtpSession() + } catch { + testInfo.skip(true, 'Email service unavailable') + return + } + await use(session) + } else { + const { otpCode } = await setupOtpMocks(page) + await setupSigningMocks(page) + await use({ + email: `mock-${Date.now()}@test.example.com`, + otpCode, + authToken: null, + }) + } + }, +}) + +export { expect } diff --git a/e2e/fixtures/test-otp-bundle.json b/e2e/fixtures/test-otp-bundle.json new file mode 100644 index 00000000..58dcdac0 --- /dev/null +++ b/e2e/fixtures/test-otp-bundle.json @@ -0,0 +1,6 @@ +{ + "version": "v1.0.0", + "data": "7b227461726765745075626c6963223a2230343466336661373736396431383465363135333537343434306366343732656161353865656135616431333034303033626632356139646533633432666161633034613062636138666564323830643364393436303236636133623364623761306130333666346263663061333361656136636437633638383637613963613630222c226f7267616e697a6174696f6e4964223a22746573742d6f72672d6964222c22757365724964223a22746573742d757365722d6964227d", + "dataSignature": "3045022100efb7a70bc28fa7bd0b0e45d2317671333792ff308f9e4f787c5bf5f6305b09b202205cd318d9fa92565024f8553b388eecd0b6642d5726c01d32d7ed2b0e19a04b47", + "enclaveQuorumPublic": "042bab66c5e0dd9aa2e230f253ab812411cbf6c1d3d6a631691d2f0c69bb4a1d2dea81933a05c7ad8fa4161788c4eb77b074bc8e1e87b4dd6db3def16f3e0a8e73" +} diff --git a/e2e/fixtures/test-signer-public-key.txt b/e2e/fixtures/test-signer-public-key.txt new file mode 100644 index 00000000..da1d9114 --- /dev/null +++ b/e2e/fixtures/test-signer-public-key.txt @@ -0,0 +1 @@ +042bab66c5e0dd9aa2e230f253ab812411cbf6c1d3d6a631691d2f0c69bb4a1d2dea81933a05c7ad8fa4161788c4eb77b074bc8e1e87b4dd6db3def16f3e0a8e73 \ No newline at end of file diff --git a/e2e/helpers/env-utils.ts b/e2e/helpers/env-utils.ts new file mode 100644 index 00000000..b30b4b69 --- /dev/null +++ b/e2e/helpers/env-utils.ts @@ -0,0 +1,7 @@ +/** + * Returns true when tests should use a real email service (mail.tm) for OTP + * delivery. When false, tests are expected to mock the backend routes instead. + */ +export function isRealEmail(): boolean { + return process.env.USE_REAL_EMAIL === 'true' +} diff --git a/e2e/helpers/mock-backend-node.ts b/e2e/helpers/mock-backend-node.ts new file mode 100644 index 00000000..5e3e3803 --- /dev/null +++ b/e2e/helpers/mock-backend-node.ts @@ -0,0 +1,78 @@ +/** + * Node.js MSW server for the ZeroDev KMS backend and Turnkey auth proxy. + * + * The Node-compatible counterpart of mock-backend.ts (Playwright page.route()). + * Both share response factories from mock-responses.ts so payloads never drift. + * + * Call `setupNodeMocks()` in `beforeAll` and invoke the returned teardown in + * `afterAll` to start/stop the server. For per-test handler overrides use the + * exported `server` directly: + * + * server.use(http.post('https://authproxy.turnkey.com/v1/otp_verify_v2', () => + * HttpResponse.json({ error: 'OTP failed' }, { status: 400 }), + * )) + * try { ... } finally { server.resetHandlers() } + */ + +import { HttpResponse, http } from 'msw' +import { setupServer } from 'msw/node' +import { + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_OTP_CODE, + MOCK_OTP_SIGNER_PUBLIC_KEY, + MOCK_PARENT_ORG_ID, + MOCK_PROJECT_ID, + mockResponses, +} from './mock-responses.js' + +export { + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_OTP_CODE, + MOCK_OTP_SIGNER_PUBLIC_KEY, + MOCK_PARENT_ORG_ID, + MOCK_PROJECT_ID, +} + +export const server = setupServer( + http.get(/\/server-info\/auth-proxy-id/, () => + HttpResponse.json(mockResponses.authProxyId()), + ), + http.get(/\/server-info\/parent-org-id/, () => + HttpResponse.json(mockResponses.parentOrgId()), + ), + http.post(/\/auth\/init\/otp/, () => + HttpResponse.json(mockResponses.otpInit()), + ), + http.post('https://authproxy.turnkey.com/v1/otp_verify_v2', () => + HttpResponse.json(mockResponses.otpVerify()), + ), + http.post(/\/auth\/login\/otp/, () => + HttpResponse.json(mockResponses.otpLogin()), + ), + http.post(/\/auth\/login\/stamp/, () => + HttpResponse.json(mockResponses.stampLogin()), + ), + http.post(/\/whoami/, () => HttpResponse.json(mockResponses.whoami())), + http.get(/\/user-wallet/, () => + HttpResponse.json(mockResponses.userWallet()), + ), + http.post(/\/sign\/message/, () => + HttpResponse.json(mockResponses.signMessage()), + ), + http.post(/\/sign\/typed-data-v4/, () => + HttpResponse.json(mockResponses.signTypedData()), + ), +) + +/** + * Starts the MSW server to intercept fetch calls in Node. + * + * Unhandled requests are passed through (bypass), so real-email-mode calls + * to Guerrilla Mail and Turnkey are unaffected when tests run in real mode. + * + * @returns teardown function — call in `afterAll` to stop the server + */ +export function setupNodeMocks(): () => void { + server.listen({ onUnhandledRequest: 'bypass' }) + return () => server.close() +} diff --git a/e2e/helpers/mock-backend.ts b/e2e/helpers/mock-backend.ts new file mode 100644 index 00000000..5ce0ee69 --- /dev/null +++ b/e2e/helpers/mock-backend.ts @@ -0,0 +1,93 @@ +/** + * Playwright route mocks for the ZeroDev KMS backend and Turnkey auth proxy. + * + * Used when `USE_REAL_EMAIL=false` so that OTP tests can run without hitting + * the real email service or backend. All routes are intercepted via + * `page.route()` glob patterns and fulfilled with deterministic fixture data. + * + * Response payloads are imported from mock-responses.ts — the same module used + * by mock-backend-node.ts — so both environments always return identical data. + * + * Route patterns use `**` which Playwright resolves across path separators, so + * "**\/auth\/init\/otp" matches + * "https://kms.staging.zerodev.app/api/v1/{projectId}/auth/init/otp". + */ + +import type { Page } from '@playwright/test' +import { MOCK_OTP_CODE, mockResponses } from './mock-responses.js' + +export { MOCK_OTP_CODE } + +/** + * Registers Playwright route mocks for the full OTP login flow. + * + * Intercepts the four network calls the SDK makes during OTP auth: + * 1. `server-info/auth-proxy-id` — returns a deterministic config ID + * 2. `auth/init/otp` — returns a mock OTP ID + encrypted bundle fixture + * 3. Turnkey `otp_verify_v2` — returns a mock verification token + * 4. `auth/login/otp` — returns a mock session JWT + * + * @returns The fixed OTP code the mock expects the test to submit + */ +export async function setupOtpMocks(page: Page): Promise<{ otpCode: string }> { + await page.route('**/server-info/auth-proxy-id', (route) => + route.fulfill({ json: mockResponses.authProxyId() }), + ) + + await page.route('**/auth/init/otp', (route) => + route.fulfill({ json: mockResponses.otpInit() }), + ) + + await page.route('https://authproxy.turnkey.com/v1/otp_verify_v2', (route) => + route.fulfill({ json: mockResponses.otpVerify() }), + ) + + await page.route('**/auth/login/otp', (route) => + route.fulfill({ json: mockResponses.otpLogin() }), + ) + + // `toViemAccount` calls this to resolve the wallet address after auth. + // Without the mock it would hit the real backend with a fake token and hang + // until the request times out (causing the post-auth redirect to stall). + await page.route('**/user-wallet', (route) => + route.fulfill({ json: mockResponses.userWallet() }), + ) + + return { otpCode: MOCK_OTP_CODE } +} + +/** + * Registers Playwright route mocks for ZeroDev KMS signing endpoints. + * + * Intercepts the two signing calls the SDK makes after authentication: + * 1. `sign/message` — returns a fake ECDSA signature + * 2. `sign/typed-data-v4` — returns a fake ECDSA signature + * + * The fake signature is a valid-looking 65-byte (130 hex char) value that + * satisfies the `0x`-prefixed format the SDK expects. + * + * ⚠ What these mocks bypass: + * - Session validation — the backend never checks if the user/org exists + * - Project configuration — allowed chains, permitted operations, and gas + * sponsorship rules are never enforced + * - Real cryptographic signing — the returned signature is not valid for + * any message and is not tied to any real key or wallet address + * + * These mocks are a direct consequence of mocking auth: `setupOtpMocks` + * returns a fake session JWT, so all subsequent authenticated calls to the + * real backend would be rejected. Signing mocks are therefore required + * whenever auth is mocked. + * + * The scheduled real-email workflow (USE_REAL_EMAIL=true) covers what mock + * mode cannot: real session validation, correct wallet address, valid + * on-chain signatures, and project configuration enforcement. + */ +export async function setupSigningMocks(page: Page): Promise { + await page.route('**/sign/message', (route) => + route.fulfill({ json: mockResponses.signMessage() }), + ) + + await page.route('**/sign/typed-data-v4', (route) => + route.fulfill({ json: mockResponses.signTypedData() }), + ) +} diff --git a/e2e/helpers/mock-responses.ts b/e2e/helpers/mock-responses.ts new file mode 100644 index 00000000..5888e6eb --- /dev/null +++ b/e2e/helpers/mock-responses.ts @@ -0,0 +1,54 @@ +/** + * Shared response factories for mock backend handlers. + * + * Single source of truth for all mocked response payloads. Imported by both + * the Node MSW server (`mock-backend-node.ts`) and the Playwright route mocks + * (`mock-backend.ts`) so the two environments always return identical data. + */ + +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { + createMockSessionJwt, + createMockVerificationToken, +} from './mock-session.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export const MOCK_PROJECT_ID = 'mock-project-id' +export const MOCK_AUTH_PROXY_CONFIG_ID = 'mock-auth-proxy-config-id' +export const MOCK_PARENT_ORG_ID = 'mock-parent-org-id' +export const MOCK_OTP_CODE = '000000' + +export const MOCK_OTP_SIGNER_PUBLIC_KEY: string = readFileSync( + path.join(__dirname, '..', 'fixtures', 'test-signer-public-key.txt'), + 'utf-8', +).trim() + +export function getTestOtpBundle(): string { + return readFileSync( + path.join(__dirname, '..', 'fixtures', 'test-otp-bundle.json'), + 'utf-8', + ) +} + +export const FAKE_SIGNATURE = '0x' + 'ab'.repeat(32) + 'cd'.repeat(32) + '01' + +export const mockResponses = { + authProxyId: () => ({ authProxyConfigId: MOCK_AUTH_PROXY_CONFIG_ID }), + parentOrgId: () => ({ parentOrgId: MOCK_PARENT_ORG_ID }), + otpInit: () => ({ + otpId: 'mock-otp-id-abc123', + otpEncryptionTargetBundle: getTestOtpBundle(), + }), + otpVerify: () => ({ verificationToken: createMockVerificationToken() }), + otpLogin: () => ({ session: createMockSessionJwt() }), + stampLogin: () => ({ session: createMockSessionJwt() }), + whoami: () => ({ userId: 'mock-user-id', organizationId: 'mock-org-id' }), + userWallet: () => ({ + walletAddresses: ['0x000000000000000000000000000000000000dEaD'], + }), + signMessage: () => ({ signature: FAKE_SIGNATURE }), + signTypedData: () => ({ signature: FAKE_SIGNATURE }), +} diff --git a/e2e/helpers/mock-session.ts b/e2e/helpers/mock-session.ts new file mode 100644 index 00000000..118c7aa7 --- /dev/null +++ b/e2e/helpers/mock-session.ts @@ -0,0 +1,51 @@ +/** + * Creates a minimal but structurally valid JWT for use in mock backend routes. + * + * The payload matches the fields expected by `parseSession()` in + * `packages/core/src/utils/utils.ts`: `exp`, `public_key`, `session_type`, + * `user_id`, `organization_id`. + * + * Uses `Buffer.from(...).toString('base64url')` rather than `btoa()` because + * this runs in the Node.js Playwright test-runner process where `Buffer` is + * natively available and handles the URL-safe encoding without manual + * character substitution. + */ + +/** + * Creates a minimal verification token JWT for use in the Turnkey otp_verify_v2 mock. + * The payload must contain an `id` field, which `buildClientSignature` uses as tokenId. + */ +export function createMockVerificationToken( + id: string = 'mock-token-id-123', +): string { + const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString( + 'base64url', + ) + const payload = Buffer.from(JSON.stringify({ id })).toString('base64url') + return `${header}.${payload}.fakesig` +} + +type MockSessionOverrides = { + userId?: string + organizationId?: string + sessionType?: string + publicKey?: string + exp?: number +} + +export function createMockSessionJwt( + overrides: MockSessionOverrides = {}, +): string { + const payload = { + user_id: overrides.userId ?? 'mock-user-id', + organization_id: overrides.organizationId ?? 'mock-org-id', + session_type: overrides.sessionType ?? 'SESSION_TYPE_READ_WRITE', + public_key: overrides.publicKey ?? '0'.repeat(66), + exp: overrides.exp ?? Math.floor(Date.now() / 1000) + 3600, + } + const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString( + 'base64url', + ) + const body = Buffer.from(JSON.stringify(payload)).toString('base64url') + return `${header}.${body}.fakesig` +} diff --git a/e2e/helpers/otp-login.ts b/e2e/helpers/otp-login.ts index c9849bd1..b146f9b7 100644 --- a/e2e/helpers/otp-login.ts +++ b/e2e/helpers/otp-login.ts @@ -1,10 +1,13 @@ /** - * Shared helper: completes an end-to-end OTP login against a real backend. + * Shared helper: completes an end-to-end OTP login. * * Used by integration tests that need an authenticated session as a setup * step (`session-management`, `wallet-operations`). Handles the full - * register → email → encrypted-verify → login flow and returns the - * authenticated client + session. + * register → (email) → encrypted-verify → login flow. + * + * In mock mode (USE_REAL_EMAIL !== 'true') the email steps are skipped and the + * known test OTP code is used instead. Network calls hit the `globalThis.fetch` + * interceptor set up by `setupNodeMocks()`. */ import { createAuthProxyClient } from '../../packages/core/src/client/authProxy.js' @@ -16,6 +19,11 @@ import { EMAIL_POLL_TIMEOUT_MS, OTP_CODE_LENGTH, } from './constants.js' +import { isRealEmail } from './env-utils.js' +import { + MOCK_OTP_CODE, + MOCK_OTP_SIGNER_PUBLIC_KEY, +} from './mock-backend-node.js' import { extractOtpCode } from './otp-utils.js' import { createNewAccount, searchForNewEmail } from './temp-email.js' import { createTestClient } from './test-client.js' @@ -25,29 +33,46 @@ export async function completeOtpLogin( projectId: string, authProxyConfigId: string, ) { - const emailAccount = await createNewAccount() const stamper = createTestStamper() const publicKey = (await stamper.getPublicKey())! - const client = createTestClient(stamper) + let email: string + let authToken: string | undefined + + if (isRealEmail()) { + const account = await createNewAccount() + email = account.address + authToken = account.authToken + } else { + email = `mock-${Date.now()}@test.example.com` + } + const registerResult = await client.registerWithOTP({ - email: emailAccount.address, - contact: { type: 'email', contact: emailAccount.address }, + email, + contact: { type: 'email', contact: email }, projectId, }) - const emailContent = await searchForNewEmail( - emailAccount.authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - const otpCode = extractOtpCode(emailContent, OTP_CODE_LENGTH)! + let otpCode: string + if (isRealEmail()) { + const emailContent = await searchForNewEmail( + authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, + ) + otpCode = extractOtpCode(emailContent, OTP_CODE_LENGTH)! + } else { + otpCode = MOCK_OTP_CODE + } const encryptedOtpBundle = await encryptOtpAttempt({ otpCode, publicKey, encryptionTargetBundle: registerResult.otpEncryptionTargetBundle, + dangerouslyOverrideSignerPublicKey: isRealEmail() + ? undefined + : MOCK_OTP_SIGNER_PUBLIC_KEY, }) const authProxyClient = createAuthProxyClient({ authProxyConfigId }) diff --git a/e2e/helpers/otp-session.ts b/e2e/helpers/otp-session.ts new file mode 100644 index 00000000..0d61d81c --- /dev/null +++ b/e2e/helpers/otp-session.ts @@ -0,0 +1,19 @@ +import { createNewAccount, ping } from './temp-email.js' + +export type OtpSession = { + email: string + /** Known OTP code in mock mode; null in real-email mode (polled after UI submit). */ + otpCode: string | null + /** Guerrilla Mail auth token; only set in real-email mode. */ + authToken: string | null +} + +/** + * Pings the email service and creates a temp inbox. + * Throws if the email service is unavailable — callers handle the skip. + */ +export async function createRealEmailOtpSession(): Promise { + await ping() + const { address, authToken } = await createNewAccount() + return { email: address, otpCode: null, authToken } +} diff --git a/e2e/helpers/tracing.ts b/e2e/helpers/tracing.ts new file mode 100644 index 00000000..af274bc9 --- /dev/null +++ b/e2e/helpers/tracing.ts @@ -0,0 +1,98 @@ +import type { Page } from '@playwright/test' + +/** + * Attaches SDK fetch tracing and browser log bridging to a Playwright page. + * + * Wraps window.fetch to emit [SDK] log lines for every non-internal request. + * Must use console.log (not console.error): Next.js 15's dev overlay intercepts + * console.error, which re-enters this wrapper on every /__nextjs fetch and causes + * an infinite React render loop ("Maximum update depth exceeded"). + * + * Safe to call from any fixture or test that needs network + console visibility. + */ +export async function setupPageTracing(page: Page): Promise { + await page.addInitScript(() => { + const log = (...args: unknown[]) => console.log('[SDK]', ...args) + const INTERNAL_URL = /__nextjs|_next\/|webpack-hmr|hot-update/ + // Captures unhandled JS errors and promise rejections for CI diagnostics. + window.addEventListener('unhandledrejection', (e) => + log('unhandledrejection:', e.reason), + ) + window.addEventListener('error', (e) => { + const short = String(e.filename || '') + .split('/') + .slice(-2) + .join('/') + log( + 'error:', + e.message, + `${short}:${e.lineno}`, + (e.error as Error | undefined)?.stack + ?.split('\n') + .slice(0, 6) + .join(' | '), + ) + }) + const origFetch = window.fetch.bind(window) + ;(window as Window & typeof globalThis).fetch = async ( + ...args: Parameters + ) => { + const a = args[0] + const url: string = + typeof a === 'string' + ? a + : a instanceof URL + ? a.href + : ((a as Request).url ?? '') + if (!url || INTERNAL_URL.test(url)) return origFetch(...args) + log('fetch→', url.split('/').slice(-3).join('/')) + try { + const res = await origFetch(...args) + log('fetch←', res.status, url.split('/').slice(-2).join('/')) + return res + } catch (err) { + log('fetchERR', String(err)) + throw err + } + } + }) + // Bridges browser-side [SDK]/[connector]/[authStore] logs to the test runner + // output, making mock route hits visible without a headed browser. + page.on('console', (msg) => { + const t = msg.type() + const text = msg.text().slice(0, 500) + if (t === 'error') console.log('[browser:error]', text) + else if ( + t === 'log' && + (text.includes('[SDK]') || + text.includes('[OtpInput]') || + text.includes('[authStore]') || + text.includes('[connector]') || + text.includes('ZeroDevWallet') || + text.includes('Creating kernel')) + ) { + console.log('[browser:log]', text) + } + }) + // Surfaces uncaught page errors (e.g. thrown from event handlers) in CI logs. + page.on('pageerror', (err) => + console.error( + '[page error]', + err.message, + '\n[stack]', + err.stack?.split('\n').slice(0, 8).join('\n'), + ), + ) + // Logs outgoing external requests (non-static, non-localhost) for mock coverage auditing. + page.on('request', (req) => { + const url = req.url() + if ( + !url.match(/\.(js|css|png|ico|woff|svg|map)($|\?)/) && + !url.includes('webpack') && + !url.includes('__nextjs') && + !url.includes('localhost') + ) { + console.log('[req]', req.method(), url.split('/').slice(-3).join('/')) + } + }) +} diff --git a/e2e/integration/fixture-shape.test.ts b/e2e/integration/fixture-shape.test.ts new file mode 100644 index 00000000..7fe48fcd --- /dev/null +++ b/e2e/integration/fixture-shape.test.ts @@ -0,0 +1,88 @@ +/** + * Guards against silent drift between `test-otp-bundle.json` and the live backend. + * + * `test-otp-bundle.json` is a static fixture used in mock mode. If the backend + * changes the bundle structure (new fields, removed fields, different encoding), + * mock-mode tests keep passing because the mock always returns the same fixture. + * This test catches that drift by comparing the fixture's key structure against + * a fresh bundle from the real backend. + * + * Skipped in mock mode — it requires a live `auth/init/otp` response by design. + * Runs automatically in the scheduled real-email workflow (USE_REAL_EMAIL=true). + * + * No email inbox needed: we call `registerWithOTP` with a fixed dummy address + * to get the bundle. We never poll for the OTP, so no Guerrilla Mail calls are + * made and there is no rate-limit risk from this test. + */ + +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { beforeAll, describe, expect, it } from 'vitest' +import { waitForBackend } from '../helpers/backend-health.js' +import { BACKEND_URL } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' +import { createTestClient } from '../helpers/test-client.js' +import { createTestStamper } from '../helpers/test-stamper.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +describe.skipIf(!isRealEmail())('OTP bundle fixture shape', () => { + let projectId: string + let skipReason = '' + + beforeAll(async () => { + try { + await waitForBackend(BACKEND_URL) + } catch { + skipReason = `Backend not reachable at ${BACKEND_URL}` + return + } + + projectId = process.env.ZD_PROJECT_ID ?? '' + if (!projectId) { + skipReason = 'ZD_PROJECT_ID not set' + } + }) + + it('fixture keys match the live backend bundle', async (context) => { + context.skip(!!skipReason, skipReason) + + const client = createTestClient(createTestStamper()) + const result = await client.registerWithOTP({ + email: 'fixture-shape-check@example.com', + contact: { + type: 'email', + contact: 'fixture-shape-check@example.com', + }, + projectId, + }) + + const liveBundle = JSON.parse(result.otpEncryptionTargetBundle) + const fixture = JSON.parse( + readFileSync( + path.join(__dirname, '..', 'fixtures', 'test-otp-bundle.json'), + 'utf-8', + ), + ) + + expect( + Object.keys(liveBundle).sort(), + 'Top-level bundle keys have drifted — regenerate test-otp-bundle.json with e2e/scripts/generate-test-otp-bundle.ts', + ).toEqual(Object.keys(fixture).sort()) + + // The `data` field is hex-encoded JSON containing the enclave's ephemeral + // public key and org ID. Decode and compare inner keys too. + const liveData = JSON.parse( + Buffer.from(liveBundle.data, 'hex').toString('utf-8'), + ) + const fixtureData = JSON.parse( + Buffer.from(fixture.data, 'hex').toString('utf-8'), + ) + + expect( + Object.keys(liveData).sort(), + 'Inner bundle.data keys have drifted — regenerate test-otp-bundle.json with e2e/scripts/generate-test-otp-bundle.ts', + ).toEqual(Object.keys(fixtureData).sort()) + }) +}) diff --git a/e2e/integration/magic-link-flow.test.ts b/e2e/integration/magic-link-flow.test.ts index 51302450..9b28f142 100644 --- a/e2e/integration/magic-link-flow.test.ts +++ b/e2e/integration/magic-link-flow.test.ts @@ -1,25 +1,16 @@ /** * E2E integration test for the Magic Link authentication flow. * - * Magic link is built on top of the OTP flow. Instead of sending a plain - * OTP code, Turnkey embeds the code into a clickable URL in the email. - * Whether a magic link (vs a plain code) is sent — and the link's URL - * template — is configured per-project on the backend - * (`wallet.otp_configs.magic_link_template`); the SDK just calls - * registerWithOTP. This test extracts the code from the magic-link URL when - * present and otherwise falls back to plain-code extraction. + * Magic link is built on top of the OTP flow. Turnkey embeds the OTP code + * into a clickable URL in the email; the URL template is configured per-project + * on the backend (`wallet.otp_configs.magic_link_template`). * - * Flow: - * 1. Create temp email account - * 2. Register with OTP - * 3. Extract OTP code from the magic link URL in the email (or plain code) - * 4. Verify OTP with Auth Proxy - * 5. Build client signature - * 6. Login with OTP via backend - * 7. Verify session works (whoami) + * In real-email mode the magic link URL is extracted from the received email. + * In mock mode all network calls are intercepted by `setupNodeMocks()` and the + * known test OTP code is used directly. */ -import { beforeAll, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createAuthProxyClient } from '../../packages/core/src/client/authProxy.js' import { buildClientSignature } from '../../packages/core/src/utils/buildClientSignature.js' import { encryptOtpAttempt } from '../../packages/core/src/utils/encryptOtpAttempt.js' @@ -32,12 +23,16 @@ import { BACKEND_URL, EMAIL_POLL_INTERVAL_MS, EMAIL_POLL_TIMEOUT_MS, - OTP_CODE_LENGTH, } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' import { - extractOtpCode, - extractOtpCodeFromMagicLinkUrl, -} from '../helpers/otp-utils.js' + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_OTP_CODE, + MOCK_OTP_SIGNER_PUBLIC_KEY, + MOCK_PROJECT_ID, + setupNodeMocks, +} from '../helpers/mock-backend-node.js' +import { extractOtpCodeFromMagicLinkUrl } from '../helpers/otp-utils.js' import { createNewAccount, ping, @@ -50,8 +45,16 @@ describe('Magic Link Authentication Flow', () => { let projectId: string let authProxyConfigId: string let skipReason = '' + let teardownMocks: (() => void) | undefined beforeAll(async () => { + if (!isRealEmail()) { + teardownMocks = setupNodeMocks() + projectId = MOCK_PROJECT_ID + authProxyConfigId = MOCK_AUTH_PROXY_CONFIG_ID + return + } + try { await waitForBackend(BACKEND_URL) } catch { @@ -75,14 +78,23 @@ describe('Magic Link Authentication Flow', () => { } }) + afterAll(() => teardownMocks?.()) + it('should complete the full magic link register + login flow', async (context) => { - console.log(`Skipping magic link flow test: ${skipReason}`) context.skip(!!skipReason, skipReason) - // Step 1: Create temp email account - const emailAccount = await createNewAccount() - const email = emailAccount.address - console.log(`Created temp email: ${email}`) + // Step 1: Create email account (real) or use a placeholder (mock) + let email: string + let authToken: string | undefined + + if (isRealEmail()) { + const emailAccount = await createNewAccount() + email = emailAccount.address + authToken = emailAccount.authToken + } else { + email = `mock-${Date.now()}@test.example.com` + } + console.log(`Using email: ${email}`) // Step 2: Create test stamper const stamper = createTestStamper() @@ -92,10 +104,8 @@ describe('Magic Link Authentication Flow', () => { // Step 3: Create SDK client const client = createTestClient(stamper) - // Step 4: Register with OTP. Whether the email carries a magic link or a - // plain code (and the link template) is configured per-project on the - // backend (`wallet.otp_configs.magic_link_template`); the client no longer - // supplies a template. + // Step 4: Register with OTP — whether the email carries a magic link or a + // plain code is configured per-project on the backend const registerResult = await client.registerWithOTP({ email, contact: { type: 'email', contact: email }, @@ -105,31 +115,33 @@ describe('Magic Link Authentication Flow', () => { expect(registerResult.otpEncryptionTargetBundle).toBeTruthy() console.log(`OTP initiated with magic link, otpId: ${registerResult.otpId}`) - // Step 5: Poll for email and extract OTP code from magic link URL - console.log('Waiting for magic link email...') - const emailContent = await searchForNewEmail( - emailAccount.authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - console.log(`Email content preview: ${emailContent.substring(0, 200)}...`) - - // Try extracting from URL first, fall back to plain OTP extraction - let otpCode = extractOtpCodeFromMagicLinkUrl(emailContent) - if (!otpCode) { - console.log( - 'No magic link URL found in email, falling back to plain OTP extraction', + // Step 5: Resolve OTP code — extract from magic link URL in real mode; + // use the known mock code in mock mode + let otpCode: string + if (isRealEmail()) { + console.log('Waiting for magic link email...') + const emailContent = await searchForNewEmail( + authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, ) - otpCode = extractOtpCode(emailContent, OTP_CODE_LENGTH) + console.log(`Email content preview: ${emailContent.substring(0, 200)}...`) + const extracted = extractOtpCodeFromMagicLinkUrl(emailContent) + expect(extracted).toBeTruthy() + otpCode = extracted! + } else { + otpCode = MOCK_OTP_CODE } - expect(otpCode).toBeTruthy() - console.log(`Extracted OTP code: ${otpCode}`) + console.log(`Extracted magic link code: ${otpCode}`) - // Step 6: HPKE-seal the OTP attempt and verify with Auth Proxy. + // Step 6: HPKE-seal the OTP attempt and verify with Auth Proxy const encryptedOtpBundle = await encryptOtpAttempt({ - otpCode: otpCode!, + otpCode, publicKey: publicKey!, encryptionTargetBundle: registerResult.otpEncryptionTargetBundle, + dangerouslyOverrideSignerPublicKey: isRealEmail() + ? undefined + : MOCK_OTP_SIGNER_PUBLIC_KEY, }) const authProxyClient = createAuthProxyClient({ authProxyConfigId }) const verifyResult = await authProxyClient.verifyOtp({ diff --git a/e2e/integration/otp-flow.test.ts b/e2e/integration/otp-flow.test.ts index 773863ed..35e09781 100644 --- a/e2e/integration/otp-flow.test.ts +++ b/e2e/integration/otp-flow.test.ts @@ -1,19 +1,15 @@ /** * E2E integration test for the OTP authentication flow. * - * Tests the complete OTP flow against a real KMS backend + Turnkey: - * 1. Create temp email account - * 2. Register with OTP (sends email) - * 3. Extract OTP code from email - * 4. Verify OTP with Auth Proxy - * 5. Build client signature - * 6. Login with OTP via backend - * 7. Verify session works (whoami) + * In real-email mode (USE_REAL_EMAIL=true) tests the complete OTP flow against + * a real KMS backend + Turnkey. In mock mode all network calls are intercepted + * by `setupNodeMocks()` and the known test OTP code is used instead. * * Mirrors the Go E2E test at doorway-kms/testing/e2e/e2e_otp_test.go */ -import { beforeAll, describe, expect, it } from 'vitest' +import { HttpResponse, http } from 'msw' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createAuthProxyClient } from '../../packages/core/src/client/authProxy.js' import { buildClientSignature } from '../../packages/core/src/utils/buildClientSignature.js' import { encryptOtpAttempt } from '../../packages/core/src/utils/encryptOtpAttempt.js' @@ -28,6 +24,15 @@ import { EMAIL_POLL_TIMEOUT_MS, OTP_CODE_LENGTH, } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' +import { + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_OTP_CODE, + MOCK_OTP_SIGNER_PUBLIC_KEY, + MOCK_PROJECT_ID, + server, + setupNodeMocks, +} from '../helpers/mock-backend-node.js' import { extractOtpCode } from '../helpers/otp-utils.js' import { createNewAccount, @@ -41,9 +46,16 @@ describe('OTP Authentication Flow', () => { let projectId: string let authProxyConfigId: string let skipReason = '' + let teardownMocks: (() => void) | undefined beforeAll(async () => { - // Check backend availability first (fast fail) + if (!isRealEmail()) { + teardownMocks = setupNodeMocks() + projectId = MOCK_PROJECT_ID + authProxyConfigId = MOCK_AUTH_PROXY_CONFIG_ID + return + } + try { await waitForBackend(BACKEND_URL) } catch { @@ -51,7 +63,6 @@ describe('OTP Authentication Flow', () => { return } - // Check email service availability try { await ping() } catch { @@ -59,10 +70,8 @@ describe('OTP Authentication Flow', () => { return } - // Get auth proxy config ID from backend authProxyConfigId = await getAuthProxyConfigId(BACKEND_URL) - // Get project ID from env projectId = process.env.ZD_PROJECT_ID || '' if (!projectId) { skipReason = 'ZD_PROJECT_ID not set' @@ -70,13 +79,23 @@ describe('OTP Authentication Flow', () => { } }) + afterAll(() => teardownMocks?.()) + it('should complete the full OTP register + login flow', async (context) => { context.skip(!!skipReason, skipReason) - // Step 1: Create temp email account - const emailAccount = await createNewAccount() - const email = emailAccount.address - console.log(`Created temp email: ${email}`) + // Step 1: Create email account (real) or use a placeholder (mock) + let email: string + let authToken: string | undefined + + if (isRealEmail()) { + const emailAccount = await createNewAccount() + email = emailAccount.address + authToken = emailAccount.authToken + } else { + email = `mock-${Date.now()}@test.example.com` + } + console.log(`Using email: ${email}`) // Step 2: Create test stamper (Node.js ECDSA P-256 implementation) const stamper = createTestStamper() @@ -87,7 +106,7 @@ describe('OTP Authentication Flow', () => { // Step 3: Create SDK client with test transport (includes Origin header) const client = createTestClient(stamper) - // Step 4: Register with OTP (triggers email send) + // Step 4: Register with OTP (triggers email send in real mode; mocked in mock mode) const registerResult = await client.registerWithOTP({ email, contact: { type: 'email', contact: email }, @@ -97,23 +116,30 @@ describe('OTP Authentication Flow', () => { expect(registerResult.otpEncryptionTargetBundle).toBeTruthy() console.log(`OTP initiated, otpId: ${registerResult.otpId}`) - // Step 5: Poll for email and extract OTP code - console.log('Waiting for OTP email...') - const emailContent = await searchForNewEmail( - emailAccount.authToken, - EMAIL_POLL_INTERVAL_MS, - EMAIL_POLL_TIMEOUT_MS, - ) - const otpCode = extractOtpCode(emailContent, OTP_CODE_LENGTH) + // Step 5: Resolve OTP code — poll email in real mode, use known code in mock mode + let otpCode: string + if (isRealEmail()) { + console.log('Waiting for OTP email...') + const emailContent = await searchForNewEmail( + authToken!, + EMAIL_POLL_INTERVAL_MS, + EMAIL_POLL_TIMEOUT_MS, + ) + otpCode = extractOtpCode(emailContent, OTP_CODE_LENGTH)! + } else { + otpCode = MOCK_OTP_CODE + } expect(otpCode).toBeTruthy() - console.log(`Extracted OTP code: ${otpCode}`) + console.log(`OTP code: ${otpCode}`) - // Step 6: HPKE-seal the OTP attempt to the enclave's per-session target - // key, then verify with Auth Proxy. + // Step 6: HPKE-seal the OTP attempt and verify with Auth Proxy const encryptedOtpBundle = await encryptOtpAttempt({ - otpCode: otpCode!, + otpCode, publicKey: publicKey!, encryptionTargetBundle: registerResult.otpEncryptionTargetBundle, + dangerouslyOverrideSignerPublicKey: isRealEmail() + ? undefined + : MOCK_OTP_SIGNER_PUBLIC_KEY, }) const authProxyClient = createAuthProxyClient({ authProxyConfigId }) const verifyResult = await authProxyClient.verifyOtp({ @@ -167,34 +193,52 @@ describe('OTP Authentication Flow', () => { it('should reject an invalid OTP code', async (context) => { context.skip(!!skipReason, skipReason) - // Create temp email - const emailAccount = await createNewAccount() - const email = emailAccount.address + const email = isRealEmail() + ? (await createNewAccount()).address + : `mock-${Date.now()}@test.example.com` - // Create stamper and client const stamper = createTestStamper() const publicKey = await stamper.getPublicKey() const client = createTestClient(stamper) - // Register with OTP const registerResult = await client.registerWithOTP({ email, contact: { type: 'email', contact: email }, projectId, }) - // Try to verify with a wrong code const wrongEncryptedBundle = await encryptOtpAttempt({ otpCode: 'WRONG12', publicKey: publicKey!, encryptionTargetBundle: registerResult.otpEncryptionTargetBundle, + dangerouslyOverrideSignerPublicKey: isRealEmail() + ? undefined + : MOCK_OTP_SIGNER_PUBLIC_KEY, }) - const authProxyClient = createAuthProxyClient({ authProxyConfigId }) - await expect( - authProxyClient.verifyOtp({ - otpId: registerResult.otpId, - encryptedOtpBundle: wrongEncryptedBundle, - }), - ).rejects.toThrow() + + // In mock mode, override the verify handler to return HTTP 400 for the + // duration of this assertion only, then restore the default handlers. + if (!isRealEmail()) { + server.use( + http.post('https://authproxy.turnkey.com/v1/otp_verify_v2', () => + HttpResponse.json( + { error: 'OTP verification failed', code: 'INVALID_OTP' }, + { status: 400 }, + ), + ), + ) + } + + try { + const authProxyClient = createAuthProxyClient({ authProxyConfigId }) + await expect( + authProxyClient.verifyOtp({ + otpId: registerResult.otpId, + encryptedOtpBundle: wrongEncryptedBundle, + }), + ).rejects.toThrow() + } finally { + if (!isRealEmail()) server.resetHandlers() + } }) }) diff --git a/e2e/integration/otp-init-shape.test.ts b/e2e/integration/otp-init-shape.test.ts index 5794c078..06bfb64f 100644 --- a/e2e/integration/otp-init-shape.test.ts +++ b/e2e/integration/otp-init-shape.test.ts @@ -11,12 +11,20 @@ import { beforeAll, describe, expect, it } from 'vitest' import { encryptOtpAttempt } from '../../packages/core/src/utils/encryptOtpAttempt.js' import { waitForBackend } from '../helpers/backend-health.js' import { BACKEND_URL } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' describe('OTP init wire shape', () => { let projectId: string let skipReason = '' beforeAll(async () => { + // This test verifies the real backend response shape and the production + // TLS Fetcher key — mocking defeats its purpose entirely. + if (!isRealEmail()) { + skipReason = 'Mock mode: this test requires a real backend response' + return + } + try { await waitForBackend(BACKEND_URL) } catch { diff --git a/e2e/integration/session-management.test.ts b/e2e/integration/session-management.test.ts index f3e96605..12384777 100644 --- a/e2e/integration/session-management.test.ts +++ b/e2e/integration/session-management.test.ts @@ -4,9 +4,12 @@ * After OTP login: * 1. Login with stamp using a new key pair (session refresh) * 2. Verify new session works for whoami + * + * In mock mode all network calls are intercepted by `setupNodeMocks()`; + * `completeOtpLogin` uses the known test OTP code instead of a real email. */ -import { beforeAll, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { parseSession } from '../../packages/core/src/utils/utils.js' import { getAuthProxyConfigId, @@ -14,6 +17,13 @@ import { waitForBackend, } from '../helpers/backend-health.js' import { BACKEND_URL } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' +import { + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_PARENT_ORG_ID, + MOCK_PROJECT_ID, + setupNodeMocks, +} from '../helpers/mock-backend-node.js' import { completeOtpLogin } from '../helpers/otp-login.js' import { ping } from '../helpers/temp-email.js' import { createTestClient } from '../helpers/test-client.js' @@ -24,8 +34,17 @@ describe('Session Management', () => { let authProxyConfigId: string let parentOrgId: string let skipReason = '' + let teardownMocks: (() => void) | undefined beforeAll(async () => { + if (!isRealEmail()) { + teardownMocks = setupNodeMocks() + projectId = MOCK_PROJECT_ID + authProxyConfigId = MOCK_AUTH_PROXY_CONFIG_ID + parentOrgId = MOCK_PARENT_ORG_ID + return + } + try { await waitForBackend(BACKEND_URL) } catch { @@ -50,6 +69,8 @@ describe('Session Management', () => { } }) + afterAll(() => teardownMocks?.()) + it('should refresh session via loginWithStamp with a new key pair', async (context) => { context.skip(!!skipReason, skipReason) diff --git a/e2e/integration/wallet-operations.test.ts b/e2e/integration/wallet-operations.test.ts index 1ccbf67c..02c635ad 100644 --- a/e2e/integration/wallet-operations.test.ts +++ b/e2e/integration/wallet-operations.test.ts @@ -4,14 +4,23 @@ * After OTP login: * 1. Get user wallet addresses * 2. Sign a message + * + * In mock mode all network calls are intercepted by `setupNodeMocks()`; + * `completeOtpLogin` uses the known test OTP code instead of a real email. */ -import { beforeAll, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { getAuthProxyConfigId, waitForBackend, } from '../helpers/backend-health.js' import { BACKEND_URL } from '../helpers/constants.js' +import { isRealEmail } from '../helpers/env-utils.js' +import { + MOCK_AUTH_PROXY_CONFIG_ID, + MOCK_PROJECT_ID, + setupNodeMocks, +} from '../helpers/mock-backend-node.js' import { completeOtpLogin } from '../helpers/otp-login.js' import { ping } from '../helpers/temp-email.js' @@ -19,8 +28,16 @@ describe('Wallet Operations', () => { let projectId: string let authProxyConfigId: string let skipReason = '' + let teardownMocks: (() => void) | undefined beforeAll(async () => { + if (!isRealEmail()) { + teardownMocks = setupNodeMocks() + projectId = MOCK_PROJECT_ID + authProxyConfigId = MOCK_AUTH_PROXY_CONFIG_ID + return + } + try { await waitForBackend(BACKEND_URL) } catch { @@ -44,6 +61,8 @@ describe('Wallet Operations', () => { } }) + afterAll(() => teardownMocks?.()) + it('should get user wallet addresses after login', async (context) => { context.skip(!!skipReason, skipReason) @@ -77,7 +96,6 @@ describe('Wallet Operations', () => { authProxyConfigId, ) - // First get wallet address const wallet = await client.getUserWallet({ organizationId: session.organizationId, projectId, @@ -86,7 +104,6 @@ describe('Wallet Operations', () => { expect(wallet.walletAddresses.length).toBeGreaterThan(0) const walletAddress = wallet.walletAddresses[0]! - // Sign a test message const signature = await client.signMessage({ organizationId: session.organizationId, projectId, diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 1047be72..3ddea612 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig, devices } from '@playwright/test' @@ -5,6 +6,17 @@ import { defineConfig, devices } from '@playwright/test' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const demoAppDir = path.resolve(__dirname, '../apps/zerodev-signer-demo') +// In mock mode, inject the test signer key so the Next.js dev server bakes it +// into the client bundle at startup (NEXT_PUBLIC_* are compile-time constants). +// This must happen before the webServer subprocess is spawned. +if (process.env.USE_REAL_EMAIL !== 'true') { + const keyPath = path.resolve(__dirname, 'fixtures/test-signer-public-key.txt') + process.env.NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY = readFileSync( + keyPath, + 'utf-8', + ).trim() +} + export default defineConfig({ testDir: './browser', fullyParallel: false, @@ -35,7 +47,11 @@ export default defineConfig({ : { command: `cd ${demoAppDir} && pnpm dev`, url: 'http://localhost:3000', - reuseExistingServer: true, + // In real-email mode, always start a fresh server so it is built + // without NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY baked in. Reusing a + // mock-mode server would activate dangerouslyOverrideOtpSignerPublicKey + // and change how Turnkey constructs the auth email. + reuseExistingServer: process.env.USE_REAL_EMAIL !== 'true', timeout: 30_000, }, }) diff --git a/e2e/scripts/generate-test-otp-bundle.ts b/e2e/scripts/generate-test-otp-bundle.ts new file mode 100644 index 00000000..b2a98049 --- /dev/null +++ b/e2e/scripts/generate-test-otp-bundle.ts @@ -0,0 +1,111 @@ +/** + * Generates test P-256 fixtures used to bypass Turnkey's pinned signing key + * during Playwright E2E tests (USE_REAL_EMAIL=false mode). + * + * Outputs (both must be committed to the repo): + * e2e/fixtures/test-otp-bundle.json — signed EncryptionTargetEnvelope returned + * by the mocked registerWithOTP response + * e2e/fixtures/test-signer-public-key.txt — enclaveQuorumPublic hex; passed to the + * demo app as NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY + * so encryptOtpAttempt accepts the test bundle + * + * ── When to regenerate ─────────────────────────────────────────────────────── + * + * Run this script again (and commit both output files) whenever: + * 1. The `encryptOtpAttempt` validation logic changes — e.g. a new bundle + * version string, a different signature algorithm, or extra required fields. + * 2. The `EncryptionTargetEnvelope` type in encryptOtpAttempt.ts changes shape. + * 3. @noble/curves is upgraded to a version with a breaking API change that + * causes the verification step to reject existing fixtures. + * + * The private key is NOT committed and does NOT need to be preserved — + * fresh key pairs are generated on every run. The only thing that must stay + * in sync is: the public key in test-signer-public-key.txt matches the key + * that signed test-otp-bundle.json (they are always produced together). + * + * ── How to regenerate ──────────────────────────────────────────────────────── + * + * pnpm tsx e2e/scripts/generate-test-otp-bundle.ts + * + * Then commit BOTH output files. The CI workflow reads the public key at + * build time: NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY=$(cat e2e/fixtures/test-signer-public-key.txt) + * + * ───────────────────────────────────────────────────────────────────────────── + * + * The @noble packages are resolved from packages/core/node_modules where + * they are already installed as production dependencies — no extra installs needed. + */ + +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Resolve @noble packages from packages/core/node_modules where they are installed. +// This script lives at e2e/scripts/, so ../../packages/core/node_modules is correct. +const coreNodeModules = resolve(__dirname, '../../packages/core/node_modules') + +const { p256 } = (await import( + pathToFileURL(resolve(coreNodeModules, '@noble/curves/nist.js')).href +)) as typeof import('@noble/curves/nist.js') + +const { bytesToHex } = (await import( + pathToFileURL(resolve(coreNodeModules, '@noble/hashes/utils.js')).href +)) as typeof import('@noble/hashes/utils.js') + +// 1. Generate the test signer key pair (replaces the pinned production key in tests) +const signerKeypair = p256.keygen() +const signerPrivKey = signerKeypair.secretKey +const signerPubKeyBytes = p256.getPublicKey(signerPrivKey, false) // uncompressed, 65 bytes +const signerPubKeyHex = bytesToHex(signerPubKeyBytes) + +// 2. Generate the HPKE target key pair (represents the enclave's ephemeral key) +const targetKeypair = p256.keygen() +const targetPrivKey = targetKeypair.secretKey +const targetPubKeyBytes = p256.getPublicKey(targetPrivKey, false) // uncompressed, 65 bytes +const targetPubKeyHex = bytesToHex(targetPubKeyBytes) + +// 3. Build the signed data JSON and hex-encode its UTF-8 bytes +const signedDataJson = JSON.stringify({ + targetPublic: targetPubKeyHex, + organizationId: 'test-org-id', + userId: 'test-user-id', +}) +const dataBytes = new TextEncoder().encode(signedDataJson) +const dataHex = bytesToHex(dataBytes) + +// 4. Sign the raw data bytes with the signer private key (sha256 prehash, DER format). +// In @noble/curves v2.x, p256.sign() returns a Uint8Array; passing format:'der' produces +// ASN.1 DER output directly, which is what encryptOtpAttempt verifies with format:'der'. +const signatureBytes = p256.sign(dataBytes, signerPrivKey, { + prehash: true, + lowS: true, + format: 'der', +}) +const dataSignatureHex = bytesToHex(signatureBytes) + +// 5. Assemble the envelope matching EncryptionTargetEnvelope in encryptOtpAttempt.ts +const envelope = { + version: 'v1.0.0', + data: dataHex, + dataSignature: dataSignatureHex, + enclaveQuorumPublic: signerPubKeyHex, +} + +// 6. Write fixtures +const fixturesDir = resolve(__dirname, '..', 'fixtures') +mkdirSync(fixturesDir, { recursive: true }) + +const bundlePath = resolve(fixturesDir, 'test-otp-bundle.json') +writeFileSync(bundlePath, JSON.stringify(envelope, null, 2), 'utf-8') + +const signerKeyPath = resolve(fixturesDir, 'test-signer-public-key.txt') +writeFileSync(signerKeyPath, signerPubKeyHex, 'utf-8') + +console.log('Generated test OTP bundle fixtures:') +console.log(` ${bundlePath}`) +console.log(` ${signerKeyPath}`) +console.log( + `Signer public key (first 32 chars): ${signerPubKeyHex.slice(0, 32)}...`, +) diff --git a/package.json b/package.json index 955c63a9..8e66ab9d 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "license": "MIT", "devDependencies": { "@biomejs/biome": "^2.2.4", - "@playwright/test": "^1.52.0", "@changesets/cli": "^2.29.7", + "@playwright/test": "^1.52.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/node": "^20.0.0", @@ -53,6 +53,7 @@ "happy-dom": "^20.6.0", "husky": "^9.1.7", "lint-staged": "^16.2.3", + "msw": "^2.15.0", "typescript": "~5.9.0", "vitest": "^4.0.18" }, diff --git a/packages/core/src/core/createZeroDevWalletCore.ts b/packages/core/src/core/createZeroDevWalletCore.ts index 318a4778..da9c3b49 100644 --- a/packages/core/src/core/createZeroDevWalletCore.ts +++ b/packages/core/src/core/createZeroDevWalletCore.ts @@ -32,6 +32,12 @@ export interface ZeroDevWalletConfigCore { apiKeyStamper: ApiKeyStamper passkeyStamper?: PasskeyStamper fetchOptions?: CreateTransportOptions['fetchOptions'] + /** + * @internal + * Test-only override for the pinned Turnkey signing key used in + * `encryptOtpAttempt`. Never set this in production. + */ + dangerouslyOverrideOtpSignerPublicKey?: string } export type { StorageAdapter, StorageManager } from '../storage/manager.js' @@ -385,6 +391,10 @@ export async function createZeroDevWalletCore( otpCode, publicKey: targetPublicKey, encryptionTargetBundle: otpEncryptionTargetBundle, + ...(config.dangerouslyOverrideOtpSignerPublicKey && { + dangerouslyOverrideSignerPublicKey: + config.dangerouslyOverrideOtpSignerPublicKey, + }), }) // Step 2b: Verify OTP via Auth Proxy diff --git a/packages/core/src/utils/encryptOtpAttempt.test.ts b/packages/core/src/utils/encryptOtpAttempt.test.ts index 450a52fe..fb075955 100644 --- a/packages/core/src/utils/encryptOtpAttempt.test.ts +++ b/packages/core/src/utils/encryptOtpAttempt.test.ts @@ -197,4 +197,25 @@ describe('encryptOtpAttempt', () => { }), ).rejects.toThrow(/failed to parse encryption target bundle/) }) + + it('enforces the production pinned key when dangerouslyOverrideSignerPublicKey is not provided', async () => { + // Build a bundle signed by a fresh, ephemeral test key — NOT the real + // TURNKEY_TLS_FETCHER_SIGN_PUBLIC_KEY. Without the override, the function + // must reject any bundle whose enclaveQuorumPublic doesn't match the + // production pinned key, protecting integrators who omit the option. + const { secretKey: signerSk } = p256.keygen() + const signerPubHex = bytesToHex(p256.getPublicKey(signerSk, false)) + const targetPublicHex = await generateTargetPubHex() + const bundle = buildBundle({ signerSk, signerPubHex, targetPublicHex }) + + await expect( + encryptOtpAttempt({ + otpCode: '123456', + publicKey: + '02aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899', + encryptionTargetBundle: bundle, + // dangerouslyOverrideSignerPublicKey intentionally omitted + }), + ).rejects.toThrow(/does not match pinned signing key/) + }) }) diff --git a/packages/react/src/connector.test.ts b/packages/react/src/connector.test.ts index 3a1a8f35..fe74c58d 100644 --- a/packages/react/src/connector.test.ts +++ b/packages/react/src/connector.test.ts @@ -59,6 +59,7 @@ vi.mock('@zerodev/wallet-core', () => ({ }), })) +import { createZeroDevWallet } from '@zerodev/wallet-core' import { zeroDevWalletCore } from './core/connector.js' type ConnectorInstance = ReturnType> @@ -168,3 +169,48 @@ describe('zeroDevWallet connector — mode branching', () => { }) }) }) + +describe('dangerouslyOverrideOtpSignerPublicKey security guard', () => { + const mockedCreateZeroDevWallet = vi.mocked(createZeroDevWallet) + + beforeEach(() => { + mockedCreateZeroDevWallet.mockClear() + }) + + it('never forwards dangerouslyOverrideOtpSignerPublicKey when not set by the integrator', async () => { + // A connector configured without the override — the common case for any + // real integrator. createZeroDevWallet must not receive the option at all, + // meaning encryptOtpAttempt will use the production pinned signing key. + const connector = createConnector() + await seedEoa(connector) // triggers doInitialize() → createZeroDevWallet() + + expect(mockedCreateZeroDevWallet).toHaveBeenCalledOnce() + const callArgs = mockedCreateZeroDevWallet.mock.calls[0]![0] + expect(callArgs).not.toHaveProperty('dangerouslyOverrideOtpSignerPublicKey') + }) + + it('forwards dangerouslyOverrideOtpSignerPublicKey only when explicitly set', async () => { + // Verify the positive case: the override IS forwarded when the connector + // is explicitly configured with it (e.g. in test environments). + const factory = zeroDevWalletCore({ + projectId: 'proj-test', + chains: [sepolia], + dangerouslyOverrideOtpSignerPublicKey: 'test-signer-key', + }) + const wagmiConfig = { + transports: {}, + emitter: { emit: vi.fn() }, + storage: null, + } as unknown as import('@wagmi/core').Config + const connector = factory(wagmiConfig as never) as ConnectorInstance + + await seedEoa(connector) + + expect(mockedCreateZeroDevWallet).toHaveBeenCalledOnce() + const callArgs = mockedCreateZeroDevWallet.mock.calls[0]![0] + expect(callArgs).toHaveProperty( + 'dangerouslyOverrideOtpSignerPublicKey', + 'test-signer-key', + ) + }) +}) diff --git a/packages/react/src/core/connector.ts b/packages/react/src/core/connector.ts index 3418b280..24265d75 100644 --- a/packages/react/src/core/connector.ts +++ b/packages/react/src/core/connector.ts @@ -63,6 +63,12 @@ export type ConnectorCoreParams = { // Controls whether setup() automatically initializes the connector on mount. // When false, the connector still initializes lazily on connect/getProvider/getStore. autoInitialize?: boolean | (() => boolean) + /** + * @internal + * Test-only override for the pinned Turnkey signing key used in + * `encryptOtpAttempt`. Never set this in production. + */ + dangerouslyOverrideOtpSignerPublicKey?: string } export function zeroDevWalletCore( @@ -226,6 +232,10 @@ export function zeroDevWalletCore( ...(apiKeyStamper && { apiKeyStamper }), ...(passkeyStamper && { passkeyStamper }), ...(params.fetchOptions && { fetchOptions: params.fetchOptions }), + ...(params.dangerouslyOverrideOtpSignerPublicKey && { + dangerouslyOverrideOtpSignerPublicKey: + params.dangerouslyOverrideOtpSignerPublicKey, + }), }) // Create store diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1e5cda3..b0ac1e94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 20.19.11 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(terser@5.46.1)(yaml@2.8.1)) happy-dom: specifier: ^20.6.0 version: 20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6) @@ -38,12 +38,15 @@ importers: lint-staged: specifier: ^16.2.3 version: 16.2.3 + msw: + specifier: ^2.15.0 + version: 2.15.0(@types/node@20.19.11)(typescript@5.9.2) typescript: specifier: ~5.9.0 version: 5.9.2 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) + version: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(terser@5.46.1)(yaml@2.8.1) apps/expo-example: dependencies: @@ -173,7 +176,7 @@ importers: version: 5.90.21(react@19.2.0) '@wagmi/core': specifier: ^3.0.0 - version: 3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + version: 3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) '@zerodev/ecdsa-validator': specifier: ^5.4.9 version: 5.4.9(@zerodev/sdk@5.5.4(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) @@ -221,7 +224,7 @@ importers: version: 2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12) wagmi: specifier: ^3.0.0 - version: 3.6.15(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + version: 3.7.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) zustand: specifier: ^5.0.3 version: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) @@ -518,7 +521,7 @@ importers: version: 4.7.0(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(terser@5.46.1)(yaml@2.8.1)) '@zerodev/smart-routing-address': specifier: ^0.2.5 version: 0.2.5(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) @@ -551,7 +554,7 @@ importers: version: 6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) + version: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(terser@5.46.1)(yaml@2.8.1) packages/wallet-react-ui: dependencies: @@ -2073,6 +2076,28 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.2': resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} engines: {node: '>=18'} @@ -2082,6 +2107,19 @@ packages: '@types/node': optional: true + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -2248,6 +2286,10 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2387,6 +2429,18 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/runtime@0.115.0': resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2484,7 +2538,7 @@ packages: '@paulmillr/qr@0.2.1': resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} - deprecated: 'The package is now available as "qr": npm install qr' + deprecated: 'Switch to "qr" (new package name) for security updates: npm install qr' '@peculiar/asn1-cms@2.6.1': resolution: {integrity: sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==} @@ -4114,9 +4168,15 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4199,6 +4259,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -4369,19 +4430,19 @@ packages: typescript: optional: true - '@wagmi/connectors@8.0.14': - resolution: {integrity: sha512-B+iMcT2wGBDujL8dX3g1vk0iZ94h0BK+S+6kQckItGWDkoNt7mterVIqFoYePtAqw3dmG4Y/W50cTxwplBXOjQ==} + '@wagmi/connectors@8.0.21': + resolution: {integrity: sha512-DA1F746DI2l+t9LoT8WiUsIM1boxucMvZ4nPQYs/c1j6j4TmxPHI9XnlgSpa6oPimmc+RAu0s2GZWrjnX6j2fA==} peerDependencies: '@base-org/account': ^2.5.1 '@coinbase/wallet-sdk': ^4.3.6 - '@metamask/connect-evm': ^1.0.0 + '@metamask/connect-evm': ^2.1.0 '@safe-global/safe-apps-provider': ~0.18.6 '@safe-global/safe-apps-sdk': ^9.1.0 - '@wagmi/core': 3.4.12 + '@wagmi/core': 3.6.0 '@walletconnect/ethereum-provider': ^2.21.1 - accounts: ~0.10 + accounts: ~0.14 porto: ~0.2.35 - typescript: '>=5.7.3' + typescript: '>=5.9.3' viem: 2.x peerDependenciesMeta: '@base-org/account': @@ -4415,12 +4476,12 @@ packages: typescript: optional: true - '@wagmi/core@3.4.12': - resolution: {integrity: sha512-q/NYoq+Up6JNfWNE6G0iXj9cHBSYgOyC8toqJLBWTeUnY1Ha9Q4OyFUHnXfvGTudbC0Gxhh7uUkqW3/LGdsQfg==} + '@wagmi/core@3.6.0': + resolution: {integrity: sha512-6AE+ywOMtjeg49j5IU2PE1hw7DXpjt05WBgRwgAelIy10SEUgP3mvCGJTN4/vH5mVcOVEL34o1LMd+1qgpesSw==} peerDependencies: '@tanstack/query-core': '>=5.0.0' - accounts: ~0.12 - typescript: '>=5.7.3' + accounts: ~0.14 + typescript: '>=5.9.3' viem: 2.x peerDependenciesMeta: '@tanstack/query-core': @@ -5124,6 +5185,10 @@ packages: resolution: {integrity: sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==} engines: {node: '>=20'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -5227,6 +5292,10 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} @@ -6100,6 +6169,15 @@ packages: fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6296,6 +6374,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -6345,6 +6427,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hermes-compiler@0.14.1: resolution: {integrity: sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA==} @@ -6593,6 +6678,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7411,12 +7499,26 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} multitars@1.0.0: resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -7644,6 +7746,9 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -7771,6 +7876,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + 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'} @@ -7793,6 +7901,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -8371,6 +8483,9 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8476,6 +8591,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@3.1.1: + resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -8682,6 +8800,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -8817,6 +8938,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -8975,6 +9100,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -9121,6 +9250,9 @@ packages: uploadthing: optional: true + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -9405,12 +9537,12 @@ packages: typescript: optional: true - wagmi@3.6.15: - resolution: {integrity: sha512-/CmbLqS7VNiK3Rv9RPTcog1MSF/7oE1/QRVXHxlcv4oSHIMMpgUMunXtZx1MepV9Bmc0vGX9JvrPn6RAYjcvrA==} + wagmi@3.7.0: + resolution: {integrity: sha512-xun/Jbuif5ujoaUPHagaAQnE3yZ/SAzxbHAnB6xwxOFsOS2YfLX59zV6lANUIP0UQ8yAgnx9rLg49BgPh6Z2pA==} peerDependencies: '@tanstack/react-query': '>=5.0.0' react: '>=18' - typescript: '>=5.7.3' + typescript: '>=5.9.3' viem: 2.x peerDependenciesMeta: typescript: @@ -11993,6 +12125,27 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@20.19.11)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@20.19.11) + '@inquirer/type': 4.0.7(@types/node@20.19.11) + optionalDependencies: + '@types/node': 20.19.11 + + '@inquirer/core@11.2.1(@types/node@20.19.11)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@20.19.11) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 20.19.11 + '@inquirer/external-editor@1.0.2(@types/node@20.19.11)': dependencies: chardet: 2.1.0 @@ -12000,6 +12153,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.11 + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@20.19.11)': + optionalDependencies: + '@types/node': 20.19.11 + '@isaacs/ttlcache@1.4.1': {} '@istanbuljs/load-nyc-config@1.1.0': @@ -12360,6 +12519,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -12462,6 +12630,17 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@oxc-project/runtime@0.115.0': {} '@oxc-project/types@0.115.0': {} @@ -15892,8 +16071,14 @@ snapshots: '@types/resolve@1.20.6': {} + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 20.19.11 + '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': {} '@types/uuid@10.0.0': {} @@ -16090,7 +16275,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(terser@5.46.1)(yaml@2.8.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -16102,7 +16287,21 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) + vitest: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(terser@5.46.1)(yaml@2.8.1) + + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(terser@5.46.1)(yaml@2.8.1))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.18 + ast-v8-to-istanbul: 0.3.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(terser@5.46.1)(yaml@2.8.1) '@vitest/expect@3.2.4': dependencies: @@ -16121,12 +16320,22 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.18(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.15.0(@types/node@20.19.11)(typescript@5.9.2) + vite: 6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) + + '@vitest/mocker@4.0.18(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.15.0(@types/node@20.19.11)(typescript@5.9.3) vite: 6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': @@ -16324,16 +16533,16 @@ snapshots: - wagmi - zod - '@wagmi/connectors@8.0.14(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': + '@wagmi/connectors@8.0.21(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': dependencies: - '@wagmi/core': 3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + '@wagmi/core': 3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) viem: 2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12) optionalDependencies: '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12) - porto: 0.2.35(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(expo-web-browser@56.0.5(expo@56.0.13)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6)))(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(wagmi@3.6.15) + porto: 0.2.35(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(expo-web-browser@56.0.5(expo@56.0.13)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6)))(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(wagmi@3.7.0) typescript: 5.9.3 '@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': @@ -16381,7 +16590,7 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': + '@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) @@ -16396,7 +16605,7 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': + '@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) @@ -18505,6 +18714,8 @@ snapshots: slice-ansi: 7.1.2 string-width: 8.1.0 + cli-width@4.1.0: {} + client-only@0.0.1: {} cliui@6.0.0: @@ -18605,6 +18816,8 @@ snapshots: cookie-es@1.2.2: {} + cookie@1.1.1: {} + core-js-compat@3.49.0: dependencies: browserslist: 4.28.1 @@ -20251,6 +20464,16 @@ snapshots: fast-stable-stringify@1.0.0: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -20275,6 +20498,10 @@ snapshots: transitivePeerDependencies: - encoding + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -20458,6 +20685,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.14.2: {} + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -20523,6 +20752,11 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.1 + hermes-compiler@0.14.1: {} hermes-compiler@250829098.0.10: {} @@ -20761,6 +20995,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -22008,10 +22244,63 @@ snapshots: ms@2.1.3: {} + msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@20.19.11) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/node' + + msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@20.19.11) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + multiformats@9.9.0: {} multitars@1.0.0: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -22283,6 +22572,8 @@ snapshots: outdent@0.5.0: {} + outvariant@1.4.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -22499,6 +22790,8 @@ snapshots: lru-cache: 11.2.7 minipass: 7.1.3 + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -22518,6 +22811,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + picomatch@4.0.4: {} pidtree@0.6.0: {} @@ -22639,9 +22934,9 @@ snapshots: - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(expo-web-browser@56.0.5(expo@56.0.13)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6)))(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(wagmi@3.6.15): + porto@0.2.35(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(expo-web-browser@56.0.5(expo@56.0.13)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6)))(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(wagmi@3.7.0): dependencies: - '@wagmi/core': 3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + '@wagmi/core': 3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) hono: 4.10.4 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) @@ -22655,7 +22950,7 @@ snapshots: react: 19.2.0 react-native: 0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.0(@babel/core@7.29.0))(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(utf-8-validate@6.0.6) typescript: 5.9.3 - wagmi: 3.6.15(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + wagmi: 3.7.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) transitivePeerDependencies: - '@types/react' - immer @@ -23749,6 +24044,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + rettime@0.11.11: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -23906,6 +24203,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@3.1.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -24196,6 +24495,8 @@ snapshots: streamsearch@1.1.0: {} + strict-event-emitter@0.5.1: {} + strict-uri-encode@2.0.0: {} string-argv@0.3.2: {} @@ -24345,6 +24646,8 @@ snapshots: symbol-tree@3.2.4: {} + tagged-tag@1.0.0: {} + tailwind-merge@3.5.0: {} tailwindcss@4.2.2: {} @@ -24399,8 +24702,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinyrainbow@2.0.0: {} @@ -24481,6 +24784,10 @@ snapshots: type-fest@0.7.1: {} + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -24600,6 +24907,8 @@ snapshots: optionalDependencies: idb-keyval: 6.2.2 + until-async@3.0.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -24955,10 +25264,49 @@ snapshots: terser: 5.46.1 yaml: 2.8.1 - vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1): + vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(terser@5.46.1)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.11 + happy-dom: 20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6) + jsdom: 29.0.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vitest@4.0.18(@types/node@20.19.11)(happy-dom@20.6.0(bufferutil@4.0.9)(utf-8-validate@6.0.6))(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(terser@5.46.1)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) + '@vitest/mocker': 4.0.18(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -25137,11 +25485,11 @@ snapshots: - utf-8-validate - zod - wagmi@3.6.15(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)): + wagmi@3.7.0(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.0))(@types/react@19.2.2)(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(react@19.2.0)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)): dependencies: '@tanstack/react-query': 5.90.21(react@19.2.0) - '@wagmi/connectors': 8.0.14(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@wagmi/core@3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) - '@wagmi/core': 3.4.12(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + '@wagmi/connectors': 8.0.21(@coinbase/wallet-sdk@4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(@wagmi/core@3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)))(@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12))(porto@0.2.35)(typescript@5.9.3)(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) + '@wagmi/core': 3.6.0(@tanstack/query-core@5.90.20)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)) react: 19.2.0 use-sync-external-store: 1.4.0(react@19.2.0) viem: 2.47.19(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.1.12)