From d87ff1229131dfb89916c7207c10ef4f5ef670b7 Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Wed, 20 May 2026 12:30:51 +0530 Subject: [PATCH] fix remote access review blockers --- packages/web/package.json | 2 + packages/web/server/remote-auth.ts | 36 +++--- packages/web/server/start-all.ts | 8 +- .../web/src/components/RemoteAccessQR.tsx | 47 ++++++-- packages/web/src/lib/remote-access-manager.ts | 42 +++++-- pnpm-lock.yaml | 113 ++++++++++++++++++ 6 files changed, 204 insertions(+), 44 deletions(-) diff --git a/packages/web/package.json b/packages/web/package.json index 9ac715576..238c9b42b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -60,6 +60,7 @@ "@xterm/xterm": "^6.0.0", "next": "^15.1.0", "next-themes": "^0.4.6", + "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", "server-only": "^0.0.1", @@ -73,6 +74,7 @@ "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.1.0", + "@types/qrcode": "^1.5.6", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/ws": "^8.18.1", diff --git a/packages/web/server/remote-auth.ts b/packages/web/server/remote-auth.ts index 07a105e4a..796553c32 100644 --- a/packages/web/server/remote-auth.ts +++ b/packages/web/server/remote-auth.ts @@ -1,4 +1,4 @@ -import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; +import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { createDefaultGlobalConfig, getGlobalConfigPath, @@ -12,7 +12,6 @@ export type RemoteAuthCredentials = { const TOKEN_VERSION = "v1"; const TOKEN_TTL_MS = 5 * 60 * 1000; -let lastActiveCredentialKey: string | undefined; function base64UrlEncode(input: string | Buffer): string { return Buffer.from(input).toString("base64url"); @@ -44,16 +43,12 @@ export function rotateRemoteWsTokenSecret(): string { return secret; } -function noteActiveCredentials(credentials: RemoteAuthCredentials): void { - const credentialKey = `${credentials.username}\0${credentials.password ?? ""}`; - if (lastActiveCredentialKey === undefined) { - lastActiveCredentialKey = credentialKey; - return; - } - if (lastActiveCredentialKey !== credentialKey) { - rotateRemoteWsTokenSecret(); - lastActiveCredentialKey = credentialKey; - } +function credentialDigest(credentials: RemoteAuthCredentials): string { + return createHash("sha256") + .update(credentials.username) + .update("\0") + .update(credentials.password ?? "") + .digest("base64url"); } export function decodeBasicToken( @@ -83,33 +78,35 @@ export function readConfiguredRemoteAuth(): RemoteAuthCredentials { }; } -export function activeRemoteAuth(initialConfiguredAuth?: RemoteAuthCredentials): RemoteAuthCredentials { +export function activeRemoteAuth( + initialConfiguredAuth?: RemoteAuthCredentials, +): RemoteAuthCredentials { const configured = readConfiguredRemoteAuth(); if ( initialConfiguredAuth && (configured.username !== initialConfiguredAuth.username || configured.password !== initialConfiguredAuth.password) ) { - noteActiveCredentials(configured); return configured; } const active = { - username: process.env.AO_REMOTE_AUTH_USER || initialConfiguredAuth?.username || configured.username, - password: process.env.AO_REMOTE_AUTH_PASSWORD || initialConfiguredAuth?.password || configured.password, + username: + process.env.AO_REMOTE_AUTH_USER || initialConfiguredAuth?.username || configured.username, + password: + process.env.AO_REMOTE_AUTH_PASSWORD || initialConfiguredAuth?.password || configured.password, }; - noteActiveCredentials(active); return active; } export function createRemoteWsToken(credentials: RemoteAuthCredentials): string | undefined { - noteActiveCredentials(credentials); const secret = getTokenSecret(); if (!secret) return undefined; const payload = base64UrlEncode( JSON.stringify({ u: credentials.username, + c: credentialDigest(credentials), exp: Date.now() + TOKEN_TTL_MS, n: randomBytes(12).toString("base64url"), }), @@ -144,9 +141,10 @@ export function verifyRemoteWsToken( if (!rawPayload) return false; try { - const parsed = JSON.parse(rawPayload) as { u?: unknown; exp?: unknown }; + const parsed = JSON.parse(rawPayload) as { u?: unknown; c?: unknown; exp?: unknown }; return ( parsed.u === expectedCredentials.username && + parsed.c === credentialDigest(expectedCredentials) && typeof parsed.exp === "number" && parsed.exp >= Date.now() ); diff --git a/packages/web/server/start-all.ts b/packages/web/server/start-all.ts index bd1637cb3..bc5955f9f 100644 --- a/packages/web/server/start-all.ts +++ b/packages/web/server/start-all.ts @@ -5,7 +5,6 @@ */ import { type ChildProcess } from "node:child_process"; -import { randomBytes } from "node:crypto"; import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -53,10 +52,11 @@ type RemoteAccessConfig = { const globalConfig = loadGlobalConfig(getGlobalConfigPath()) ?? createDefaultGlobalConfig(); const remoteAccessConfig: RemoteAccessConfig = - globalConfig.remoteAccess && typeof globalConfig.remoteAccess === "object" ? globalConfig.remoteAccess : {}; + globalConfig.remoteAccess && typeof globalConfig.remoteAccess === "object" + ? globalConfig.remoteAccess + : {}; process.env["AO_REMOTE_AUTH_USER"] ||= remoteAccessConfig.username?.trim() || "ao"; -process.env["AO_REMOTE_AUTH_PASSWORD"] ||= - remoteAccessConfig.password?.trim() || randomBytes(18).toString("base64url"); +process.env["AO_REMOTE_AUTH_PASSWORD"] ||= remoteAccessConfig.password?.trim() || ""; ensureRemoteWsTokenSecret(); function log(label: string, msg: string): void { diff --git a/packages/web/src/components/RemoteAccessQR.tsx b/packages/web/src/components/RemoteAccessQR.tsx index e2353594c..c0cb6e26c 100644 --- a/packages/web/src/components/RemoteAccessQR.tsx +++ b/packages/web/src/components/RemoteAccessQR.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from "react"; import Image from "next/image"; +import QRCode from "qrcode"; interface RemoteHost { url: string; @@ -18,10 +19,6 @@ interface RemoteInfo { hosts: RemoteHost[]; } -function qrImageUrl(data: string, size = 200): string { - return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(data)}&margin=10`; -} - export function RemoteAccessQR() { const [info, setInfo] = useState(null); const [open, setOpen] = useState(false); @@ -31,6 +28,7 @@ export function RemoteAccessQR() { const [username, setUsername] = useState("ao"); const [password, setPassword] = useState(""); const [error, setError] = useState(null); + const [qrDataUrl, setQrDataUrl] = useState(null); useEffect(() => { let cancelled = false; @@ -48,7 +46,9 @@ export function RemoteAccessQR() { } }) .catch(() => {}); - return () => { cancelled = true; }; + return () => { + cancelled = true; + }; }, []); const toggle = useCallback(() => setOpen((v) => !v), []); @@ -98,7 +98,28 @@ export function RemoteAccessQR() { const hosts = info?.hosts ?? []; const host = hosts[selectedHost] ?? hosts[0]; const hasMultiple = hosts.length > 1; - const qrUrl = host ? qrImageUrl(host.url) : null; + + useEffect(() => { + let cancelled = false; + setQrDataUrl(null); + if (!host?.url) return; + + QRCode.toDataURL(host.url, { + width: 200, + margin: 2, + errorCorrectionLevel: "M", + }) + .then((dataUrl) => { + if (!cancelled) setQrDataUrl(dataUrl); + }) + .catch(() => { + if (!cancelled) setQrDataUrl(null); + }); + + return () => { + cancelled = true; + }; + }, [host?.url]); return ( <> @@ -145,16 +166,23 @@ export function RemoteAccessQR() { onClick={toggle} aria-label="Close" > - +
- {qrUrl && host ? ( + {qrDataUrl && host ? ( {`QR Password setPassword(event.target.value)} autoComplete="current-password" diff --git a/packages/web/src/lib/remote-access-manager.ts b/packages/web/src/lib/remote-access-manager.ts index 051842985..8a793205e 100644 --- a/packages/web/src/lib/remote-access-manager.ts +++ b/packages/web/src/lib/remote-access-manager.ts @@ -25,7 +25,6 @@ import { spawnManagedDaemonChild, type GlobalConfig, } from "@aoagents/ao-core"; -import { rotateRemoteWsTokenSecret } from "../../server/remote-auth"; interface RemoteHost { url: string; @@ -62,14 +61,14 @@ function configuredUsername(config = loadGlobalConfigOrDefault()): string { } function configuredPassword(config = loadGlobalConfigOrDefault()): string | undefined { - const password = remoteAccessConfig(config).password?.trim() || process.env["AO_REMOTE_AUTH_PASSWORD"]; + const password = + remoteAccessConfig(config).password?.trim() || process.env["AO_REMOTE_AUTH_PASSWORD"]; return password && password.length > 0 ? password : undefined; } function applyRemoteCredentials(username: string, password: string): void { process.env["AO_REMOTE_AUTH_USER"] = username; process.env["AO_REMOTE_AUTH_PASSWORD"] = password; - rotateRemoteWsTokenSecret(); } function generatePassword(): string { @@ -81,7 +80,12 @@ function tryCloudflareUrl(line: string): string | null { } function getCloudflaredCachePath(): string { - return resolve(homedir(), ".agent-orchestrator", "bin", isWindows() ? "cloudflared.exe" : "cloudflared"); + return resolve( + homedir(), + ".agent-orchestrator", + "bin", + isWindows() ? "cloudflared.exe" : "cloudflared", + ); } function getCloudflaredDownload(): { url: string; archive: boolean } { @@ -109,15 +113,18 @@ function getCloudflaredDownload(): { url: string; archive: boolean } { } async function fetchExpectedCloudflaredChecksum(assetName: string): Promise { - const response = await fetch("https://api.github.com/repos/cloudflare/cloudflared/releases/latest", { - headers: { Accept: "application/vnd.github+json" }, - }); + const response = await fetch( + "https://api.github.com/repos/cloudflare/cloudflared/releases/latest", + { + headers: { Accept: "application/vnd.github+json" }, + }, + ); if (!response.ok) throw new Error(`Failed to fetch cloudflared checksums (${response.status})`); const release = (await response.json()) as { body?: unknown }; const body = typeof release.body === "string" ? release.body : ""; const escapedAssetName = assetName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const match = new RegExp(`${escapedAssetName}:\\s*([a-fA-F0-9]{64})`).exec(body); + const match = new RegExp(`^${escapedAssetName}:\\s*([a-fA-F0-9]{64})`, "m").exec(body); if (!match?.[1]) throw new Error(`Missing SHA-256 checksum for ${assetName}`); return match[1].toLowerCase(); } @@ -149,7 +156,8 @@ async function downloadCloudflaredBinary(targetPath: string): Promise { const { execFileSync } = await import("node:child_process"); execFileSync("tar", ["-xzf", archivePath, "-C", tempDir]); const extractedPath = resolve(tempDir, "cloudflared"); - if (!existsSync(extractedPath)) throw new Error("cloudflared archive did not contain a binary"); + if (!existsSync(extractedPath)) + throw new Error("cloudflared archive did not contain a binary"); chmodSync(extractedPath, 0o755); renameSync(extractedPath, targetPath); return; @@ -214,7 +222,9 @@ export function saveRemoteAccessCredentials(input: { return getRemoteAccessInfo(); } -async function startCloudflareTunnel(port: string): Promise<{ publicUrl: string; child: ChildProcess }> { +async function startCloudflareTunnel( + port: string, +): Promise<{ publicUrl: string; child: ChildProcess }> { const cloudflared = await resolveCloudflaredBinary(); const child = spawnManagedDaemonChild( "remote-tunnel", @@ -230,7 +240,11 @@ async function startCloudflareTunnel(port: string): Promise<{ publicUrl: string; if (settled) return; settled = true; child.kill(); - reject(new Error(`Timed out waiting for cloudflared.${recentOutput ? ` Last output: ${recentOutput.trim()}` : ""}`)); + reject( + new Error( + `Timed out waiting for cloudflared.${recentOutput ? ` Last output: ${recentOutput.trim()}` : ""}`, + ), + ); }, 30_000); function settle(publicUrl: string) { @@ -259,7 +273,11 @@ async function startCloudflareTunnel(port: string): Promise<{ publicUrl: string; if (settled) return; settled = true; clearTimeout(timeout); - reject(new Error(`cloudflared exited before creating a tunnel${code === null ? "" : ` (${code})`}`)); + reject( + new Error( + `cloudflared exited before creating a tunnel${code === null ? "" : ` (${code})`}`, + ), + ); }); }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aeaeca8a..a7c37b518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -727,6 +727,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: ^19.0.0 version: 19.2.5 @@ -756,6 +759,9 @@ importers: '@testing-library/react': specifier: ^16.1.0 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: ^19.0.0 version: 19.2.14 @@ -1967,6 +1973,9 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2267,6 +2276,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} @@ -2311,6 +2324,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2372,6 +2388,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -2406,6 +2426,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3340,6 +3363,10 @@ packages: engines: {node: '>=18'} hasBin: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -3386,6 +3413,11 @@ packages: pusher-js@8.5.0: resolution: {integrity: sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -3428,6 +3460,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -3495,6 +3530,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3958,6 +3996,9 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3977,6 +4018,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4019,6 +4064,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4038,10 +4086,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -5069,6 +5125,10 @@ snapshots: dependencies: undici-types: 7.19.2 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.6.0 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -5448,6 +5508,8 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + camelcase@5.3.1: {} + caniuse-lite@1.0.30001787: {} chai@5.3.3: @@ -5484,6 +5546,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -5541,6 +5609,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js@10.6.0: {} decompress-response@6.0.0: @@ -5563,6 +5633,8 @@ snapshots: detect-libc@2.1.2: {} + dijkstrajs@1.0.3: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -6515,6 +6587,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pngjs@5.0.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -6569,6 +6643,12 @@ snapshots: dependencies: tweetnacl: 1.0.3 + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -6613,6 +6693,8 @@ snapshots: require-directory@2.1.1: {} + require-main-filename@2.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -6696,6 +6778,8 @@ snapshots: server-only@0.0.1: {} + set-blocking@2.0.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -7152,6 +7236,8 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -7167,6 +7253,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -7190,6 +7282,8 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -7200,8 +7294,27 @@ snapshots: yaml@2.8.3: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1