fix remote access review blockers

This commit is contained in:
Ashish Huddar 2026-05-20 12:30:51 +05:30
parent 16ab83cfa5
commit d87ff12291
6 changed files with 204 additions and 44 deletions

View File

@ -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",

View File

@ -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()
);

View File

@ -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 {

View File

@ -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<RemoteInfo | null>(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<string | null>(null);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(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"
>
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</div>
<div className="remote-qr-modal__body">
{qrUrl && host ? (
{qrDataUrl && host ? (
<Image
src={qrUrl}
src={qrDataUrl}
alt={`QR code for ${host.url}`}
className="remote-qr-modal__qr"
width={200}
@ -193,6 +221,7 @@ export function RemoteAccessQR() {
<label className="remote-qr-modal__credential-field">
<span>Password</span>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
autoComplete="current-password"

View File

@ -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<string> {
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<void> {
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})`}`,
),
);
});
});
}

View File

@ -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