diff --git a/cspell-config/cspell-misc.txt b/cspell-config/cspell-misc.txt index 7c0ce86c0..8756eb771 100644 --- a/cspell-config/cspell-misc.txt +++ b/cspell-config/cspell-misc.txt @@ -14,6 +14,7 @@ ethereum sepolia foundryup unpermitted +dapps // examples/bank-demo ctap diff --git a/examples/demo-app/components/DeploymentResultCard.vue b/examples/demo-app/components/DeploymentResultCard.vue new file mode 100644 index 000000000..0a0bfd708 --- /dev/null +++ b/examples/demo-app/components/DeploymentResultCard.vue @@ -0,0 +1,41 @@ + + + diff --git a/examples/demo-app/components/FindAddressesByPasskey.vue b/examples/demo-app/components/FindAddressesByPasskey.vue new file mode 100644 index 000000000..9c4299381 --- /dev/null +++ b/examples/demo-app/components/FindAddressesByPasskey.vue @@ -0,0 +1,148 @@ + + + diff --git a/examples/demo-app/components/TypedDataErc7739.vue b/examples/demo-app/components/TypedDataErc7739.vue new file mode 100644 index 000000000..dd7c09b74 --- /dev/null +++ b/examples/demo-app/components/TypedDataErc7739.vue @@ -0,0 +1,374 @@ + + + diff --git a/examples/demo-app/foundry.toml b/examples/demo-app/foundry.toml new file mode 100644 index 000000000..36915eb0b --- /dev/null +++ b/examples/demo-app/foundry.toml @@ -0,0 +1,4 @@ +[profile.default] +src = "smart-contracts" +libs = ["node_modules"] +remappings = ["@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/"] diff --git a/examples/demo-app/package.json b/examples/demo-app/package.json index ac44d89f6..11ef574a7 100644 --- a/examples/demo-app/package.json +++ b/examples/demo-app/package.json @@ -10,7 +10,6 @@ "dependencies": { "@matterlabs/zksync-contracts": "^0.6.1", "@nuxtjs/google-fonts": "^3.2.0", - "@openzeppelin/contracts": "^5.4.0", "@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/server": "^13.1.1", "@wagmi/core": "^2.13.3", @@ -25,6 +24,7 @@ "devDependencies": { "@nuxt/eslint": "^0.5.7", "@nuxtjs/tailwindcss": "^6.12.0", + "@openzeppelin/contracts": "^5.4.0", "@playwright/test": "^1.47.2", "@types/node": "^22.7.5", "vite-plugin-wasm": "^3.5.0" diff --git a/examples/demo-app/pages/web-sdk-test.vue b/examples/demo-app/pages/web-sdk-test.vue index 53f39f12d..7a3bac8b1 100644 --- a/examples/demo-app/pages/web-sdk-test.vue +++ b/examples/demo-app/pages/web-sdk-test.vue @@ -58,36 +58,7 @@ -
-

- Account Deployed Successfully! -

-
-
- User ID: - {{ deploymentResult.userId }} -
-
- Account ID (computed): - {{ deploymentResult.accountId }} -
-
- Account Address: - {{ deploymentResult.address }} -
-
- EOA Signer: - {{ deploymentResult.eoaSigner }} - (Anvil Rich Wallet #1) -
-
- Passkey Enabled: Yes -
-
-
+
- + + + +
-

- Find Addresses by Passkey -

-

- Authenticate with a passkey to find all smart account addresses associated with it. +

+ E2E Bridge: Typed Data Actions

- -
- +
- - -
-
-
-

- Passkey Credential ID: -

- {{ findPasskeyCredentialId }} -
- -
-

- Origin Domain: -

- {{ findPasskeyOriginDomain }} -
- - -
-

- Associated Accounts: -

-
-
    -
  • - {{ index + 1 }}. {{ address }} -
  • -
-
-
-

- No accounts found for this passkey. -

-
-
-
-
- - -
- Scan Error: -

- {{ findPasskeyScanError }} -

-
- -
- Search Error: -

- {{ findAddressesError }} -

-
+ Verify Signature +
+ + +
(null); +// E2E bridge ref to call TypedDataErc7739 actions +const typedDataRef = ref<{ signErc7739?: () => void; verifyErc7739?: () => void } | null>(null); // Address computation parameters const addressParams = ref({ @@ -463,13 +370,7 @@ const passkeyRegistered = ref(false); const passkeyRegisterResult = ref(""); const passkeyRegisterError = ref(""); -// Find addresses by passkey state -const findPasskeyScanned = ref(false); -const findPasskeyCredentialId = ref(""); -const findPasskeyOriginDomain = ref(""); -const findPasskeyScanError = ref(""); -const foundAddresses = ref(null); -const findAddressesError = ref(""); +// Find addresses by passkey state moved to component // Fund smart account parameters const fundParams = ref({ @@ -621,6 +522,8 @@ async function testWebSDK() { } } +/** Typed Data (ERC-7739) is now a separate component */ + /** * Load WebAuthn validator address from contracts.json */ @@ -922,99 +825,7 @@ async function registerPasskey() { } } -/** - * Scan/authenticate with a passkey and automatically find associated accounts - */ -async function scanPasskeyForFindAccounts() { - loading.value = true; - findPasskeyScanError.value = ""; - foundAddresses.value = null; - findAddressesError.value = ""; - findPasskeyScanned.value = false; - - try { - // eslint-disable-next-line no-console - console.log("Requesting WebAuthn authentication to scan passkey..."); - - // Create a challenge for authentication - const challenge = new Uint8Array(32); - crypto.getRandomValues(challenge); - - // Request authentication to get the credential ID - const credential = await navigator.credentials.get({ - publicKey: { - challenge, - timeout: 60000, - rpId: window.location.hostname, - userVerification: "required", - }, - }); - - if (!credential || credential.type !== "public-key") { - throw new Error("Failed to authenticate with passkey"); - } - - const pkCredential = credential as PublicKeyCredential; - - // Extract credential ID - const credentialId = new Uint8Array(pkCredential.rawId); - const credentialIdHex = `0x${Array.from(credentialId).map((b) => b.toString(16).padStart(2, "0")).join("")}`; - - // Set the scanned passkey details - findPasskeyCredentialId.value = credentialIdHex; - findPasskeyOriginDomain.value = window.location.origin; - findPasskeyScanned.value = true; - - // eslint-disable-next-line no-console - console.log("Passkey scanned successfully:"); - // eslint-disable-next-line no-console - console.log(" Credential ID:", credentialIdHex); - // eslint-disable-next-line no-console - console.log(" Origin:", window.location.origin); - - // Automatically find accounts for this passkey - // eslint-disable-next-line no-console - console.log("Finding accounts for passkey..."); - - // Load contracts configuration - const contracts = await loadContracts(); - - // Create public client - const publicClient = await createPublicClient(contracts); - - // eslint-disable-next-line no-console - console.log(" WebAuthn Validator:", contracts.webauthnValidator); - - // Call the findAddressesByPasskey action - const result = await findAddressesByPasskey({ - client: publicClient, - contracts: { - webauthnValidator: contracts.webauthnValidator as Address, - }, - passkey: { - credentialId: credentialIdHex as Hex, - originDomain: window.location.origin, - }, - }); - - foundAddresses.value = result.addresses; - - // eslint-disable-next-line no-console - console.log(` Found ${result.addresses.length} account(s):`, result.addresses); - } catch (err: unknown) { - // eslint-disable-next-line no-console - console.error("Failed to scan passkey or find accounts:", err); - - // Determine if error was during scan or search - if (!findPasskeyScanned.value) { - findPasskeyScanError.value = `Failed to scan passkey: ${err instanceof Error ? err.message : String(err)}`; - } else { - findAddressesError.value = `Failed to find accounts: ${err instanceof Error ? err.message : String(err)}`; - } - } finally { - loading.value = false; - } -} +/** Find addresses by passkey logic moved into FindAddressesByPasskey component */ // Fund the smart account with ETH from EOA wallet async function fundSmartAccount() { @@ -1107,8 +918,8 @@ async function computeAddress() { throw new Error("Please fill in all address parameters correctly"); } - // Use the WASM function to compute the account ID - const accountId = compute_account_id(addressParams.value.userId); + // Compute the account ID using the SDK helper + const accountId = generateAccountId(addressParams.value.userId); // eslint-disable-next-line no-console console.log("Computing address with parameters:"); diff --git a/examples/demo-app/project.json b/examples/demo-app/project.json index 34173d6d4..ab11f2efc 100644 --- a/examples/demo-app/project.json +++ b/examples/demo-app/project.json @@ -76,6 +76,13 @@ "command": "forge build" } }, + "deploy-erc1271-caller": { + "executor": "nx:run-commands", + "options": { + "cwd": "examples/demo-app", + "command": "./scripts/deploy-erc1271-caller.sh" + } + }, "deploy-contracts": { "executor": "nx:run-commands", "options": { @@ -100,6 +107,23 @@ }, "dependsOn": ["build-erc4337-contracts"] }, + "deploy-contracts-erc4337-runtime": { + "executor": "nx:run-commands", + "options": { + "cwd": "examples/demo-app", + "commands": [ + { + "command": "echo '{\"deployedTo\":\"0x0000000000000000000000000000000000000000\"}' > forge-output-paymaster.json", + "prefix": "Stub-Paymaster:" + }, + { + "command": "./scripts/deploy-erc1271-caller.sh", + "prefix": "ERC1271Caller:" + } + ] + }, + "dependsOn": ["deploy-msa-factory"] + }, "build:local": { "executor": "nx:run-commands", "options": { @@ -139,7 +163,7 @@ "cwd": "examples/demo-app", "command": "pnpm exec playwright install chromium" }, - "dependsOn": ["deploy-contracts-erc4337"] + "dependsOn": ["deploy-contracts-erc4337-runtime"] }, "e2e": { "executor": "nx:run-commands", diff --git a/examples/demo-app/remappings.txt b/examples/demo-app/remappings.txt new file mode 100644 index 000000000..0c071e486 --- /dev/null +++ b/examples/demo-app/remappings.txt @@ -0,0 +1 @@ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ diff --git a/examples/demo-app/scripts/deploy-erc1271-caller.sh b/examples/demo-app/scripts/deploy-erc1271-caller.sh new file mode 100755 index 000000000..a4ecef4c9 --- /dev/null +++ b/examples/demo-app/scripts/deploy-erc1271-caller.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy the ERC1271Caller helper for the demo app and emit forge-output-erc1271.json + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +DEMO_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +# (Optional) Workspace root; uncomment if needed later +# WORKSPACE_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd) + +# Defaults for local Anvil +RPC_URL=${RPC_URL:-http://localhost:8545} +PRIVATE_KEY=${PRIVATE_KEY:-0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6} # anvil acct #9 + +CONTRACT_PATH="$DEMO_DIR/smart-contracts/ERC1271Caller.sol" +CONTRACT_NAME="ERC1271Caller" + +echo "🚀 Deploying $CONTRACT_NAME to $RPC_URL" + +if [[ ! -f "$CONTRACT_PATH" ]]; then + echo "❌ Contract not found at $CONTRACT_PATH" >&2 + exit 1 +fi + +# Change to demo-app dir so forge can find remappings.txt and node_modules +cd "$DEMO_DIR" + +# Build contract first to ensure it's compiled +forge build "smart-contracts/ERC1271Caller.sol" >/dev/null 2>&1 + +# Deploy and capture the address from forge output (EVM) +DEPLOY_OUTPUT=$(forge create "smart-contracts/ERC1271Caller.sol:$CONTRACT_NAME" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + 2>&1) + +# Extract deployed address from output (format: "Deployed to: 0x...") +DEPLOYED_TO=$(echo "$DEPLOY_OUTPUT" | grep -i "Deployed to:" | awk '{print $3}') + +if [[ -z "${DEPLOYED_TO:-}" || ! "$DEPLOYED_TO" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + echo "❌ Failed to parse deployed address from forge output" >&2 + echo "$DEPLOY_OUTPUT" + exit 1 +fi + +echo "✅ Deployed at: $DEPLOYED_TO" + +# Write the output JSON used by the demo component +OUTPUT_FILE="$DEMO_DIR/forge-output-erc1271.json" +echo "{\"deployedTo\":\"$DEPLOYED_TO\"}" > "$OUTPUT_FILE" +echo "💾 Wrote $OUTPUT_FILE" + +exit 0 diff --git a/examples/demo-app/smart-contracts/ERC1271Caller.sol b/examples/demo-app/smart-contracts/ERC1271Caller.sol index 93a36ee3b..719936e84 100644 --- a/examples/demo-app/smart-contracts/ERC1271Caller.sol +++ b/examples/demo-app/smart-contracts/ERC1271Caller.sol @@ -34,4 +34,80 @@ contract ERC1271Caller is EIP712 { bytes4 magic = IERC1271(signer).isValidSignature(digest, signature); return magic == IERC1271.isValidSignature.selector; } + + /** + * Validates a precomputed digest against an ERC-1271 signer. + * This is useful for schemes like ERC-7739 where the digest is computed off-chain. + */ + function validateDigest(bytes32 digest, address signer, bytes calldata signature) external view returns (bool) { + require(signer != address(0), "Invalid signer address"); + bytes4 magic = IERC1271(signer).isValidSignature(digest, signature); + return magic == IERC1271.isValidSignature.selector; + } + + // --- DEBUG helpers --------------------------------------------------- + /// @notice Compute the EIP-712 struct hash for the provided TestStruct + function computeStructHash(TestStruct calldata testStruct) external pure returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("TestStruct(string message,uint256 value)"), + keccak256(bytes(testStruct.message)), + testStruct.value + ) + ); + } + + /// @notice Compute the TypedDataSign typed-data wrapper typehash, wrapper struct hash and final hash from Basic.t.sol + /// @dev This mirrors the assembly algorithm in Solady/Basic.t.sol for debugging parity + function computeTypedDataSignAndFinalHash( + TestStruct calldata testStruct, + string calldata contentsDescription, + string calldata verifierName, + string calldata verifierVersion, + uint256 verifierChainId, + address verifierContract, + bytes32 verifierSalt + ) external view returns (bytes32 typedDataSignTypehash, bytes32 wrapperStructHash, bytes32 finalHash) { + bytes32 structHash = keccak256( + abi.encode( + keccak256("TestStruct(string message,uint256 value)"), + keccak256(bytes(testStruct.message)), + testStruct.value + ) + ); + + // Build the TypedDataSign type string, appending the `contentsDescription` as in Basic.t.sol + bytes memory prefix = abi.encodePacked( + "TypedDataSign(", + "TestStruct contents,", + "string name,", + "string version,", + "uint256 chainId,", + "address verifyingContract,", + "bytes32 salt)", + contentsDescription + ); + + typedDataSignTypehash = keccak256(prefix); + + wrapperStructHash = keccak256( + abi.encode( + typedDataSignTypehash, + structHash, + keccak256(bytes(verifierName)), + keccak256(bytes(verifierVersion)), + verifierChainId, + uint256(uint160(verifierContract)), + verifierSalt + ) + ); + + finalHash = keccak256(abi.encodePacked(hex"1901", _domainSeparatorV4(), wrapperStructHash)); + } + + /// @notice Return this contract's EIP-712 domain separator (app domain) + function appDomainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); + } } diff --git a/examples/demo-app/tests/web-sdk-test.spec.ts b/examples/demo-app/tests/web-sdk-test.spec.ts index 763a5cdfd..8e6c2e36b 100644 --- a/examples/demo-app/tests/web-sdk-test.spec.ts +++ b/examples/demo-app/tests/web-sdk-test.spec.ts @@ -262,7 +262,7 @@ test("Find addresses by passkey credential ID", async ({ page }) => { // Get the deployed account address const deployedAddressElement = page.getByTestId("deployed-account-address"); - await expect(deployedAddressElement).toBeVisible(); + await expect(deployedAddressElement).toBeVisible({ timeout: 10000 }); const deployedAddress = await deployedAddressElement.textContent(); console.log(`Deployed account address: ${deployedAddress}`); @@ -639,3 +639,42 @@ test("Deploy account with session validator pre-installed", async ({ page }) => console.log("✅ Deploy with session validator test completed!"); }); + +test("Sign and verify ERC-7739 typed data via ERC-1271", async ({ page }) => { + // Use Anvil account #10 for this test to avoid nonce conflicts + await page.goto("/web-sdk-test?fundingAccount=10"); + await expect(page.getByText("ZKSync SSO Web SDK Test")).toBeVisible(); + + // Wait for SDK to load + await expect(page.getByText("SDK Loaded:")).toBeVisible(); + await expect(page.getByText("Yes")).toBeVisible({ timeout: 10000 }); + + // Ensure a smart account is deployed so we have an address to verify against + await page.getByRole("button", { name: "Deploy Account" }).click(); + await expect(page.getByText("Account Deployed Successfully!")).toBeVisible({ timeout: 30000 }); + + // Wait for typed-data section to be visible + const typedDataSection = page.getByTestId("typed-data-section"); + await expect(typedDataSection).toBeVisible({ timeout: 10000 }); + + // Sign the ERC-7739 typed data (no connection needed with new implementation) + await expect(typedDataSection).toBeVisible({ timeout: 15000 }); + // Ensure ERC1271 caller address is present before interacting (ensures readiness) + const callerAddr = typedDataSection.getByTestId("erc1271-caller-address"); + await expect(callerAddr).toBeVisible({ timeout: 30000 }); + // Use E2E bridge to trigger sign to avoid internal gating + const signBridge = page.getByTestId("typeddata-sign-bridge"); + await expect(signBridge).toBeVisible({ timeout: 20000 }); + await signBridge.click(); + await expect(typedDataSection.getByTestId("typeddata-signature")).toBeVisible({ timeout: 20000 }); + + // Verify on-chain via ERC-1271 helper contract + const verifyBridge = page.getByTestId("typeddata-verify-bridge"); + await expect(verifyBridge).toBeVisible({ timeout: 20000 }); + await verifyBridge.click(); + const result = typedDataSection.getByTestId("typeddata-verify-result"); + await expect(result).toBeVisible({ timeout: 30000 }); + await expect(result).toContainText("Valid"); + + console.log("✓ ERC-7739 typed-data signature verified on-chain via ERC-1271"); +}); diff --git a/examples/demo-app/utils/erc7739.ts b/examples/demo-app/utils/erc7739.ts new file mode 100644 index 000000000..74c09aff2 --- /dev/null +++ b/examples/demo-app/utils/erc7739.ts @@ -0,0 +1,120 @@ +import { hashTypedData, wrapTypedDataSignature } from "viem/experimental/erc7739"; +import { pad, concatHex } from "viem"; +import type { TypedData, TypedDataDomain, Hex } from "viem"; + +export interface Erc7739TypedData { + domain: TypedDataDomain; + types: TypedData; + primaryType: string; + message: Record; +} + +export interface SignTypedDataErc7739Args { + typedData: Erc7739TypedData; + smartAccountAddress: Hex; + signer: (hash: Hex) => Promise; + chainId?: number; + validatorAddress?: Hex; +} + +export interface HashTypedDataErc7739Args { + typedData: Erc7739TypedData; + smartAccountAddress: Hex; + chainId?: number; +} + +/** + * Creates the verifier domain for ERC-7739 typed data wrapping. + * This domain is used to wrap application-specific typed data with account verification metadata. + * + * @param smartAccountAddress - The smart account address that will verify the signature + * @param chainId - The chain ID for the verifier domain (defaults to 1337 for local Anvil) + * @returns The TypedDataDomain for the verifier (smart account) with all required fields + */ +function createVerifierDomain(smartAccountAddress: Hex, chainId = 1337) { + return { + chainId, + name: "zksync-sso-1271" as const, + version: "1.0.0" as const, + verifyingContract: smartAccountAddress, + salt: pad("0x", { size: 32 }), + } as const; +} + +/** + * Signs EIP-712 typed data using ERC-7739 nested typed data wrapping. + * + * This function implements the ERC-7739 standard for smart account typed data signatures: + * 1. Wraps the application's typed data in a verifier domain (smart account context) + * 2. Computes the ERC-7739 hash (nested EIP-712 hash) + * 3. Signs the hash with the provided signer + * 4. Optionally prepends a validator address (required for ModularSmartAccount) + * 5. Wraps the signature with ERC-7739 metadata (domain, contents, description) + * + * The resulting signature can be verified on-chain via ERC-1271's isValidSignature. + * + * @param typedData - The application's EIP-712 typed data to sign + * @param smartAccountAddress - The smart account that will verify this signature + * @param signer - Async function that signs a hash and returns a signature (e.g., EOA.sign) + * @param chainId - Chain ID for the verifier domain (defaults to 1337) + * @param validatorAddress - Optional validator module address to prepend before wrapping (required for modular accounts) + * @returns The ERC-7739 wrapped signature with validator prefix and metadata + */ +export async function signTypedDataErc7739({ + typedData, + smartAccountAddress, + signer, + chainId = 1337, + validatorAddress, +}: SignTypedDataErc7739Args): Promise { + const verifierDomain = createVerifierDomain(smartAccountAddress, chainId); + + const erc7739Data = { + ...typedData, + verifierDomain, + }; + const hash = hashTypedData(erc7739Data); + const signature = await signer(hash); + + const signatureToWrap = validatorAddress + ? concatHex([validatorAddress, signature]) + : signature; + + return wrapTypedDataSignature({ + domain: typedData.domain, + types: typedData.types, + primaryType: typedData.primaryType, + message: typedData.message, + signature: signatureToWrap, + }); +} + +/** + * Computes the ERC-7739 hash for typed data without signing. + * + * This function is useful for: + * - Debugging signature verification issues by comparing JS and on-chain hashes + * - Precomputing the digest that will be signed + * - Testing hash computation without requiring a signer + * + * The hash is computed by wrapping the application's typed data in a verifier domain + * (smart account context) and computing the nested EIP-712 hash per ERC-7739. + * + * @param typedData - The application's EIP-712 typed data + * @param smartAccountAddress - The smart account address used in the verifier domain + * @param chainId - Chain ID for the verifier domain (defaults to 1337) + * @returns The ERC-7739 hash (nested EIP-712 hash) that would be signed + */ +export function hashTypedDataErc7739({ + typedData, + smartAccountAddress, + chainId = 1337, +}: HashTypedDataErc7739Args): Hex { + const verifierDomain = createVerifierDomain(smartAccountAddress, chainId); + + const erc7739Data = { + ...typedData, + verifierDomain, + }; + return hashTypedData(erc7739Data); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9618a0029..7a022b5d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,9 +159,6 @@ importers: '@nuxtjs/google-fonts': specifier: ^3.2.0 version: 3.2.0(magicast@0.3.5)(rollup@4.30.1) - '@openzeppelin/contracts': - specifier: ^5.4.0 - version: 5.4.0 '@simplewebauthn/browser': specifier: ^13.1.0 version: 13.1.0 @@ -199,6 +196,9 @@ importers: '@nuxtjs/tailwindcss': specifier: ^6.12.0 version: 6.12.2(magicast@0.3.5)(rollup@4.30.1)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@24.3.3)(typescript@5.6.2)) + '@openzeppelin/contracts': + specifier: ^5.4.0 + version: 5.4.0 '@playwright/test': specifier: ^1.47.2 version: 1.48.1 @@ -19969,6 +19969,14 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.8(vite@5.4.10(@types/node@22.8.0)(sass@1.80.4)(terser@5.36.0))': + dependencies: + '@vitest/spy': 2.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.10(@types/node@22.8.0)(sass@1.80.4)(terser@5.36.0) + '@vitest/mocker@2.1.8(vite@5.4.10(@types/node@24.3.3)(sass@1.80.4)(terser@5.36.0))': dependencies: '@vitest/spy': 2.1.8 @@ -26922,7 +26930,7 @@ snapshots: vue: 3.5.21(typescript@5.6.2) vue-bundle-renderer: 2.1.1 vue-devtools-stub: 0.1.0 - vue-router: 4.4.5(vue@3.5.21(typescript@5.6.2)) + vue-router: 4.4.5(vue@3.5.13(typescript@5.6.2)) optionalDependencies: '@parcel/watcher': 2.4.1 '@types/node': 22.8.0 @@ -29703,7 +29711,7 @@ snapshots: unplugin: 1.14.1 yaml: 2.6.0 optionalDependencies: - vue-router: 4.4.5(vue@3.5.21(typescript@5.6.2)) + vue-router: 4.4.5(vue@3.5.13(typescript@5.6.2)) transitivePeerDependencies: - rollup - vue @@ -30292,7 +30300,7 @@ snapshots: vitest@2.1.8(@types/node@22.8.0)(sass@1.80.4)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.10(@types/node@24.3.3)(sass@1.80.4)(terser@5.36.0)) + '@vitest/mocker': 2.1.8(vite@5.4.10(@types/node@22.8.0)(sass@1.80.4)(terser@5.36.0)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -30409,6 +30417,11 @@ snapshots: dependencies: vue: 3.5.24(typescript@5.6.2) + vue-router@4.4.5(vue@3.5.13(typescript@5.6.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.6.2) + vue-router@4.4.5(vue@3.5.21(typescript@5.6.2)): dependencies: '@vue/devtools-api': 6.6.4