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)