From 49fc3d302dd12fa00ae972810990ffb69a317946 Mon Sep 17 00:00:00 2001 From: Esther Adaeze Eze <102748488+esthereze@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:58:08 +0000 Subject: [PATCH] feat(web): add CommunityFactory authorization preflight to community creation Read the factory owner during creation readiness, compare it with the connected wallet (only when the wallet network matches the application network), and surface ready, disconnected, network-unknown, wrong-network, unauthorized, and read-failed states before any simulation or wallet approval. Unauthorized wallets can no longer reach the deploy approval action, and a factory owner read failure is retryable rather than reported as unauthorized. Also repair pre-existing breakage on main that blocked CI: sync the stale root lockfile (missing @testing-library/user-event broke `npm ci`), and fix committed type/lint errors in contracts.ts, useNetworkGuard.ts, useCommunityDeployment.ts, useTransactionLifecycle.ts, and the proposals pages. Closes #257 --- apps/web/e2e/fixtures.ts | 3 + .../(app)/communities/[id]/proposals/page.tsx | 2 +- apps/web/src/app/(app)/proposals/page.tsx | 6 +- .../CommunityDeploymentPanel.test.tsx | 96 +++++++ .../components/CommunityDeploymentPanel.tsx | 133 ++++++++++ apps/web/src/hooks/useNetworkGuard.ts | 18 +- apps/web/src/hooks/useTransactionLifecycle.ts | 6 +- apps/web/src/lib/community/deployment.ts | 30 +++ .../useCommunityDeployment.ts | 6 +- apps/web/src/lib/contracts.ts | 8 +- package-lock.json | 240 ++++-------------- 11 files changed, 343 insertions(+), 205 deletions(-) diff --git a/apps/web/e2e/fixtures.ts b/apps/web/e2e/fixtures.ts index 88b2b53..be17e3e 100644 --- a/apps/web/e2e/fixtures.ts +++ b/apps/web/e2e/fixtures.ts @@ -135,6 +135,9 @@ export async function installCreationFixtures( async verifyRegistry() { return "verified"; }, + async readFactoryOwner() { + return wallet; + }, }, }; }, diff --git a/apps/web/src/app/(app)/communities/[id]/proposals/page.tsx b/apps/web/src/app/(app)/communities/[id]/proposals/page.tsx index d9efea5..c200808 100644 --- a/apps/web/src/app/(app)/communities/[id]/proposals/page.tsx +++ b/apps/web/src/app/(app)/communities/[id]/proposals/page.tsx @@ -45,7 +45,7 @@ function ScopedProposalHistory({ community }: { community: CommunityView }) { try { const client = createReadOnlyGovernorClient(governorContract); const transaction = await client.proposal_state({ - proposal_id: Uint8Array.from(Buffer.from(proposalId, "hex")), + proposal_id: Buffer.from(proposalId, "hex"), }); setStates((current) => ({ ...current, diff --git a/apps/web/src/app/(app)/proposals/page.tsx b/apps/web/src/app/(app)/proposals/page.tsx index 3ace97a..65fefc6 100644 --- a/apps/web/src/app/(app)/proposals/page.tsx +++ b/apps/web/src/app/(app)/proposals/page.tsx @@ -146,7 +146,8 @@ export default function ProposalsPage() { ); useEffect(() => { - void loadStates(); + const timeout = window.setTimeout(() => void loadStates(), 0); + return () => window.clearTimeout(timeout); }, [loadStates]); const availableStates = useMemo( @@ -173,7 +174,8 @@ export default function ProposalsPage() { useEffect(() => { if (stateFilter !== ALL_FILTER && !availableStates.includes(stateFilter)) { - setStateFilter(ALL_FILTER); + const timeout = window.setTimeout(() => setStateFilter(ALL_FILTER), 0); + return () => window.clearTimeout(timeout); } }, [availableStates, stateFilter]); diff --git a/apps/web/src/components/CommunityDeploymentPanel.test.tsx b/apps/web/src/components/CommunityDeploymentPanel.test.tsx index 8b9165c..87758ea 100644 --- a/apps/web/src/components/CommunityDeploymentPanel.test.tsx +++ b/apps/web/src/components/CommunityDeploymentPanel.test.tsx @@ -73,9 +73,24 @@ function adapter() { }), transactionStatus: vi.fn().mockResolvedValue("success"), verifyRegistry: vi.fn().mockResolvedValue("verified"), + readFactoryOwner: vi.fn().mockResolvedValue(address), }; } +const simulateButtons = () => + screen.queryAllByRole("button", { name: "Simulate deployment" }); +const approveButton = () => + screen.queryByRole("button", { name: "Approve and deploy" }); + +/** The owner preflight resolves asynchronously to "ready" before any action. */ +async function awaitReady() { + await waitFor(async () => { + expect( + simulateButtons().some((button) => !(button as HTMLButtonElement).disabled), + ).toBe(true); + }); +} + describe("CommunityDeploymentPanel", () => { beforeEach(() => { sessionStorage.clear(); @@ -92,6 +107,7 @@ describe("CommunityDeploymentPanel", () => { mocks.getE2EBridge.mockReturnValue({ deployment }); render(); + await awaitReady(); fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" })); expect(await screen.findByText(/12345678 stroops/)).toHaveTextContent( "1.2345678 XLM", @@ -134,6 +150,7 @@ describe("CommunityDeploymentPanel", () => { mocks.getE2EBridge.mockReturnValue({ deployment }); render(); + await awaitReady(); fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" })); await screen.findByText(/12345678 stroops/); fireEvent.click(screen.getByRole("button", { name: "Approve and deploy" })); @@ -195,4 +212,83 @@ describe("CommunityDeploymentPanel", () => { ).toBeEnabled(); expect(deployment.signAndSubmit).not.toHaveBeenCalled(); }); + + it("blocks the deploy approval action for a non-owner wallet", async () => { + const deployment = adapter(); + const other = `G${"B".repeat(55)}`; + mocks.useWallet.mockReturnValue({ + address: other, + signTransaction: vi.fn(), + walletNetwork: "testnet", + walletNetworkPassphrase: "Test SDF Network ; September 2015", + }); + mocks.getE2EBridge.mockReturnValue({ deployment }); + render(); + + await screen.findByText(/Only the CommunityFactory owner can create communities/); + expect( + screen.getByRole("button", { name: "Simulate deployment" }), + ).toBeDisabled(); + fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" })); + expect(deployment.simulate).not.toHaveBeenCalled(); + expect(approveButton()).not.toBeInTheDocument(); + }); + + it("reports a disconnected wallet as disconnected and holds actions", async () => { + mocks.useWallet.mockReturnValue({ + address: null, + signTransaction: vi.fn(), + walletNetwork: null, + walletNetworkPassphrase: null, + }); + render(); + + expect( + await screen.findByText(/Connect your wallet to check whether this account can create a community/), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Simulate deployment" }), + ).toBeDisabled(); + }); + + it("is network-aware and never reports a mismatched wallet as unauthorized", async () => { + const deployment = adapter(); + mocks.useWallet.mockReturnValue({ + address, + signTransaction: vi.fn(), + walletNetwork: "mainnet", + walletNetworkPassphrase: "Public Global Stellar Network ; September 2015", + }); + mocks.getE2EBridge.mockReturnValue({ deployment }); + render(); + + expect(await screen.findByText(/Expected testnet/)).toHaveTextContent( + "Detected mainnet", + ); + expect(deployment.readFactoryOwner).not.toHaveBeenCalled(); + expect( + screen.queryByText(/cannot create/i), + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/only the communityfactory owner/i), + ).not.toBeInTheDocument(); + }); + + it("treats a factory owner read failure as retryable, not unauthorized", async () => { + const deployment = adapter(); + deployment.readFactoryOwner.mockRejectedValueOnce(new Error("RPC unavailable")); + mocks.getE2EBridge.mockReturnValue({ deployment }); + render(); + + const retry = await screen.findByRole("button", { name: "Retry owner check" }); + expect( + screen.queryByText(/only the communityfactory owner|cannot create/i), + ).not.toBeInTheDocument(); + + deployment.readFactoryOwner.mockResolvedValueOnce(address); + fireEvent.click(retry); + await awaitReady(); + expect(deployment.readFactoryOwner).toHaveBeenCalledTimes(2); + expect(screen.queryByText("Retry owner check")).not.toBeInTheDocument(); + }); }); diff --git a/apps/web/src/components/CommunityDeploymentPanel.tsx b/apps/web/src/components/CommunityDeploymentPanel.tsx index d13857a..b821224 100644 --- a/apps/web/src/components/CommunityDeploymentPanel.tsx +++ b/apps/web/src/components/CommunityDeploymentPanel.tsx @@ -35,6 +35,36 @@ type Props = { confirmed: boolean; }; +export type FactoryAuthorizationStatus = + | "checking" + | "ready" + | "disconnected" + | "network-unknown" + | "wrong-network" + | "unauthorized" + | "read-failed"; + +function authorizationMessage(status: FactoryAuthorizationStatus): string | null { + switch (status) { + case "ready": + return null; + case "checking": + return "Checking CommunityFactory owner authorization."; + case "disconnected": + return "Connect your wallet to check whether this account can create a community."; + case "network-unknown": + return "Reading the wallet network. Deploy stays locked until it is confirmed."; + case "wrong-network": + return "Your wallet is on a different Stellar network. Switch it to the configured network to check creation rights."; + case "unauthorized": + return "Only the CommunityFactory owner can create communities during this pilot. This wallet cannot deploy one."; + case "read-failed": + return "Could not read the CommunityFactory owner. Retry to re-check before simulating."; + default: + return null; + } +} + function friendlyError(error: unknown): string { const message = error instanceof Error ? error.message : String(error); if (/reject|declin|denied/i.test(message)) { @@ -74,6 +104,13 @@ export function CommunityDeploymentPanel({ const [knownTransactionStatus, setKnownTransactionStatus] = useState(null); const [busy, setBusy] = useState(false); + const [ownerRead, setOwnerRead] = useState< + | { status: "ok"; owner: string } + | { status: "error" } + | null + >(null); + const [authorizationCheck, setAuthorizationCheck] = useState(0); + const preflightInFlight = useRef(false); const inFlight = useRef(false); const previousInput = useRef(""); @@ -123,6 +160,65 @@ export function CommunityDeploymentPanel({ } }, [inputSignature, networkMismatch, transactionHash]); + /** + * Authorization preflight: resolve the authorization state before any + * simulation or signature. The deterministic states (disconnected, + * network-unknown, wrong-network, unconfigured factory) are derived during + * render. Only when the wallet and the network are in a comparable state do + * we read the factory owner from chain and compare it with the connected + * address. The read is network-aware - it only runs when the wallet network + * matches the application network, so a wrong-network wallet is reported as + * such rather than as unauthorized. A failed read surfaces a retryable + * "read-failed" state, never an authorization verdict. + */ + const staticAuthorization: FactoryAuthorizationStatus | null = useMemo(() => { + if (transactionHash) return null; + if (!address) return "disconnected"; + if (walletNetworkUnknown) return "network-unknown"; + if (networkMismatch) return "wrong-network"; + if (!factoryId) return "read-failed"; + return null; + }, [address, factoryId, networkMismatch, transactionHash, walletNetworkUnknown]); + + const authorization: FactoryAuthorizationStatus = useMemo(() => { + if (staticAuthorization !== null) return staticAuthorization; + if (ownerRead === null) return "checking"; + if (ownerRead.status === "ok") { + return ownerRead.owner === address ? "ready" : "unauthorized"; + } + return "read-failed"; + }, [address, ownerRead, staticAuthorization]); + + useEffect(() => { + if (transactionHash || staticAuthorization !== null) return; + let cancelled = false; + if (preflightInFlight.current) return; + preflightInFlight.current = true; + void adapter + .readFactoryOwner(factoryId, address ?? "") + .then((owner) => { + if (cancelled) return; + setOwnerRead({ status: "ok", owner }); + }) + .catch(() => { + if (cancelled) return; + setOwnerRead({ status: "error" }); + }) + .finally(() => { + preflightInFlight.current = false; + }); + return () => { + cancelled = true; + }; + }, [ + adapter, + address, + authorizationCheck, + factoryId, + staticAuthorization, + transactionHash, + ]); + const verifyExpectedRecord = useCallback( async (expected: CommunityDeploymentRecovery["expectedRecord"]) => { setRegistryState("checking"); @@ -219,6 +315,7 @@ export function CommunityDeploymentPanel({ !factoryId || networkMismatch || walletNetworkUnknown || + authorization !== "ready" || transactionHash ) { return; @@ -259,6 +356,7 @@ export function CommunityDeploymentPanel({ !confirmed || networkMismatch || walletNetworkUnknown || + authorization !== "ready" || transactionHash ) { return; @@ -343,6 +441,39 @@ export function CommunityDeploymentPanel({ )} + {(authorization === "disconnected" || + authorization === "network-unknown" || + authorization === "unauthorized" || + authorization === "read-failed") && ( + + {authorizationMessage(authorization)} + {authorization === "read-failed" && ( + + )} + + )} + + {authorization === "checking" && ( + + {authorizationMessage("checking")} + + )} + {simulation && !transactionHash && (
@@ -395,6 +526,7 @@ export function CommunityDeploymentPanel({ !factoryId || networkMismatch || walletNetworkUnknown || + authorization !== "ready" || stage === "simulating" } className="min-h-11 rounded-lg border border-indigo-500 px-4 py-2 text-sm font-medium text-indigo-200 hover:bg-indigo-950/50 disabled:cursor-not-allowed disabled:opacity-50" @@ -410,6 +542,7 @@ export function CommunityDeploymentPanel({ !confirmed || networkMismatch || walletNetworkUnknown || + authorization !== "ready" || stage === "awaiting_approval" } className="min-h-11 rounded-lg bg-indigo-500 px-5 py-2 text-sm font-medium text-white hover:bg-indigo-400 disabled:cursor-not-allowed disabled:opacity-50" diff --git a/apps/web/src/hooks/useNetworkGuard.ts b/apps/web/src/hooks/useNetworkGuard.ts index cc54cb9..e090032 100644 --- a/apps/web/src/hooks/useNetworkGuard.ts +++ b/apps/web/src/hooks/useNetworkGuard.ts @@ -2,7 +2,11 @@ import { useMemo } from "react"; import { useWallet } from "@/context/WalletProvider"; -import { compareNetworks, type NetworkComparison } from "@/lib/network"; +import { + compareNetworks, + describeNetwork, + type NetworkComparison, +} from "@/lib/network"; import { activeNetwork } from "@/lib/stellar"; /** @@ -11,9 +15,11 @@ import { activeNetwork } from "@/lib/stellar"; * network directly, so mismatch handling stays in one place. */ export function useNetworkGuard(): NetworkComparison { - const { walletNetwork } = useWallet(); - return useMemo( - () => compareNetworks(activeNetwork, walletNetwork), - [walletNetwork], - ); + const { walletNetwork, walletNetworkPassphrase } = useWallet(); + return useMemo(() => { + const detected = walletNetworkPassphrase + ? describeNetwork(walletNetworkPassphrase, walletNetwork ?? undefined) + : null; + return compareNetworks(activeNetwork, detected); + }, [walletNetwork, walletNetworkPassphrase]); } diff --git a/apps/web/src/hooks/useTransactionLifecycle.ts b/apps/web/src/hooks/useTransactionLifecycle.ts index 3468292..76334e5 100644 --- a/apps/web/src/hooks/useTransactionLifecycle.ts +++ b/apps/web/src/hooks/useTransactionLifecycle.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { mapTransactionError } from "@/lib/transactionErrors"; /** @@ -73,7 +73,9 @@ export function useTransactionLifecycle(options?: UseTransactionLifecycleOptions }); const inFlightRef = useRef(false); const onConfirmedRef = useRef(options?.onConfirmed); - onConfirmedRef.current = options?.onConfirmed; + useEffect(() => { + onConfirmedRef.current = options?.onConfirmed; + }, [options?.onConfirmed]); const reset = useCallback(() => { if (inFlightRef.current) return; diff --git a/apps/web/src/lib/community/deployment.ts b/apps/web/src/lib/community/deployment.ts index 4730221..75f8fda 100644 --- a/apps/web/src/lib/community/deployment.ts +++ b/apps/web/src/lib/community/deployment.ts @@ -74,6 +74,13 @@ export type CommunityDeploymentAdapter = { verifyRegistry( expected: CommunityRegistryRecord, ): Promise<"verified" | "missing" | "mismatch" | "rpc-error">; + /** + * Reads the CommunityFactory owner so the UI can gate deployment before any + * simulation or signature. Resolves to the owner address string on success; + * throws on a transient read failure so callers can retry rather than treat + * the result as an authorization verdict. + */ + readFactoryOwner(factoryId: string, publicKey: string): Promise; }; function bytesToHex(bytes: Uint8Array): string { @@ -349,6 +356,29 @@ export const defaultCommunityDeploymentAdapter: CommunityDeploymentAdapter = { return "rpc-error"; } }, + + /** + * Reads `owner()` off the factory as a read-only simulation. Any throw is a + * transient read failure that the UI reports as retryable, never as + * "unauthorized". + */ + async readFactoryOwner(factoryId, publicKey) { + const transaction = await AssembledTransaction.build({ + contractId: factoryId, + method: "owner", + args: [], + networkPassphrase: config.networkPassphrase, + rpcUrl: config.rpcUrl, + publicKey, + timeoutInSeconds: COMMUNITY_DEPLOYMENT_TIMEOUT_SECONDS, + parseResultXdr: (value) => scValToNative(value) as string, + }); + const owner = transaction.result; + if (typeof owner !== "string" || owner.trim() === "") { + throw new Error("The CommunityFactory owner read did not return an address."); + } + return owner; + }, }; export function metadataHashBytes(invocation: CommunityFactoryInvocation) { diff --git a/apps/web/src/lib/communityFactory/useCommunityDeployment.ts b/apps/web/src/lib/communityFactory/useCommunityDeployment.ts index 5e9772d..5a185e3 100644 --- a/apps/web/src/lib/communityFactory/useCommunityDeployment.ts +++ b/apps/web/src/lib/communityFactory/useCommunityDeployment.ts @@ -27,7 +27,7 @@ const stageLabels: Record = { }; export function useCommunityDeployment() { - const { address, networkPassphrase, signTransaction } = useWallet(); + const { address, walletNetworkPassphrase, signTransaction } = useWallet(); const activeSubmissionRef = useRef(false); const [stage, setStage] = useState("idle"); const [error, setError] = useState(null); @@ -55,7 +55,7 @@ export function useCommunityDeployment() { const nextOutcome = await deployCommunityFromWizard(state, { address, expectedNetworkPassphrase: config.networkPassphrase, - walletNetworkPassphrase: networkPassphrase, + walletNetworkPassphrase, createClient: () => createCommunityFactoryClient({ publicKey: address ?? "", @@ -78,7 +78,7 @@ export function useCommunityDeployment() { activeSubmissionRef.current = false; } }, - [address, isSubmitting, networkPassphrase, signTransaction], + [address, isSubmitting, walletNetworkPassphrase, signTransaction], ); return { diff --git a/apps/web/src/lib/contracts.ts b/apps/web/src/lib/contracts.ts index 0814ee1..0b3ba71 100644 --- a/apps/web/src/lib/contracts.ts +++ b/apps/web/src/lib/contracts.ts @@ -42,7 +42,7 @@ export function createNftClient({ const nft = contractId ?? requireContractIds().nft; return new NftClient({ contractId: nft, - networkPassphrase: config.passphrase, + networkPassphrase: config.networkPassphrase, rpcUrl: config.rpcUrl, publicKey, signTransaction, @@ -59,7 +59,7 @@ export function createGovernorClient({ if (mocked) return mocked as unknown as GovernorClient; return new GovernorClient({ contractId: governor, - networkPassphrase: config.passphrase, + networkPassphrase: config.networkPassphrase, rpcUrl: config.rpcUrl, publicKey, signTransaction, @@ -70,7 +70,7 @@ export function createReadOnlyNftClient(contractId?: string) { const nft = contractId ?? requireContractIds().nft; return new NftClient({ contractId: nft, - networkPassphrase: config.passphrase, + networkPassphrase: config.networkPassphrase, rpcUrl: config.rpcUrl, }); } @@ -81,7 +81,7 @@ export function createReadOnlyGovernorClient(contractId?: string) { if (mocked) return mocked as unknown as GovernorClient; return new GovernorClient({ contractId: governor, - networkPassphrase: config.passphrase, + networkPassphrase: config.networkPassphrase, rpcUrl: config.rpcUrl, }); } diff --git a/package-lock.json b/package-lock.json index 46ae777..aaa7735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@tailwindcss/postcss": "^4", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.5.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -2498,7 +2499,6 @@ "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.62.0" }, @@ -2861,9 +2861,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2881,9 +2878,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2901,9 +2895,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2921,9 +2912,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2941,9 +2929,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2961,9 +2946,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3883,6 +3865,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@trezor/analytics": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.5.0.tgz", @@ -6890,9 +6886,15 @@ "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { "version": "1.12.2", @@ -7025,129 +7027,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/snapshot": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", @@ -13554,35 +13433,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.62.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13689,6 +13539,35 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -14491,19 +14370,6 @@ "node": ">=v12.22.7" } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",