From 26825841fabd684f794df5a9f09a14c809224e20 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 16 Sep 2026 09:22:18 +0000 Subject: [PATCH] feat(web): MFA and passkey sign-in on the login page --- web/app/login/page.tsx | 185 ++++++++++- web/components/mfa/MfaEnrolWizard.tsx | 185 +++++++++++ web/lib/api.ts | 71 ++++- web/lib/webauthn.ts | 68 ++++ web/package-lock.json | 434 +++++++++++++++++++------- web/package.json | 14 +- 6 files changed, 825 insertions(+), 132 deletions(-) create mode 100644 web/components/mfa/MfaEnrolWizard.tsx create mode 100644 web/lib/webauthn.ts diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx index 7b5ef9c..7225060 100644 --- a/web/app/login/page.tsx +++ b/web/app/login/page.tsx @@ -2,11 +2,13 @@ import { useEffect, useState } from "react"; import { useMutation } from "@tanstack/react-query"; -import { auth, type PublicProvider } from "@/lib/api"; +import { auth, ApiError, type PublicProvider } from "@/lib/api"; import { Button, Card } from "@/components/ui"; import { Logo } from "@/components/Logo"; import { NetworkBackground } from "@/components/NetworkBackground"; import { ProviderIcon } from "@/components/settings/ProviderIcon"; +import { MfaEnrolWizard } from "@/components/mfa/MfaEnrolWizard"; +import { isPasskeySupported, toRequestOptions, credentialToJSON } from "@/lib/webauthn"; const ERROR_MESSAGES: Record = { oidc_unavailable: "Single sign-on is not available on this instance's plan.", @@ -23,8 +25,27 @@ const ERROR_MESSAGES: Record = { state_failed: "Could not start sign-in. Please try again.", unknown_host: "This address does not name a known instance.", instance_locked: "Access to this instance is suspended.", + invalid_code: "That code was not correct.", + mfa_ticket_expired: "That sign-in attempt expired. Please sign in again.", }; +/** Turns a thrown error into the page's inline message, handling the rate limit specially. */ +function describeError(err: unknown): string { + if (err instanceof ApiError) { + if (err.status === 429) { + return err.retryAfter ? `Too many attempts. Try again in ${err.retryAfter}s.` : "Too many attempts. Please wait a moment and try again."; + } + if (err.code === "invalid_code" && err.attemptsLeft !== undefined) { + return `That code was not correct. ${err.attemptsLeft} attempt${err.attemptsLeft === 1 ? "" : "s"} left.`; + } + if (err.code && ERROR_MESSAGES[err.code]) return ERROR_MESSAGES[err.code]; + return err.message || "Sign-in failed. Please try again."; + } + return "Sign-in failed. Please try again."; +} + +type Step = "credentials" | "factor" | "enrol"; + export default function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -38,6 +59,15 @@ export default function LoginPage() { // says so instead of drawing a form that cannot sign anyone in. const [locked, setLocked] = useState(false); + const [step, setStep] = useState("credentials"); + const [factorMethods, setFactorMethods] = useState([]); + const [useRecovery, setUseRecovery] = useState(false); + const [factorCode, setFactorCode] = useState(""); + const [factorError, setFactorError] = useState(""); + const [factorBusy, setFactorBusy] = useState(false); + const [credentialsNotice, setCredentialsNotice] = useState(""); + const [passkeyBusy, setPasskeyBusy] = useState(false); + useEffect(() => { const code = new URLSearchParams(window.location.search).get("error"); if (code === "instance_locked") setLocked(true); @@ -80,16 +110,98 @@ export default function LoginPage() { error, } = useMutation({ mutationFn: () => auth.login(email, password), - onSuccess: () => { + onSuccess: (res) => { + if ("mfa_required" in res && res.mfa_required) { + setFactorMethods(res.methods); + setUseRecovery(false); + setFactorCode(""); + setFactorError(""); + setStep("factor"); + return; + } + if ("enrol_required" in res && res.enrol_required) { + setStep("enrol"); + return; + } window.location.href = "/"; }, }); function handleSubmit(e: React.FormEvent) { e.preventDefault(); + setCredentialsNotice(""); signIn(); } + function returnToCredentials(message: string) { + setStep("credentials"); + setCredentialsNotice(message); + } + + async function handleFactorSubmit(e: React.FormEvent) { + e.preventDefault(); + setFactorError(""); + setFactorBusy(true); + try { + if (useRecovery) { + await auth.mfaRecovery(factorCode); + } else { + await auth.mfaTotp(factorCode); + } + window.location.href = "/"; + } catch (err) { + if (err instanceof ApiError && err.code === "mfa_ticket_expired") { + returnToCredentials("That sign-in attempt expired. Please sign in again."); + return; + } + setFactorError(describeError(err)); + } finally { + setFactorBusy(false); + } + } + + async function handleFactorPasskey() { + setFactorError(""); + setFactorBusy(true); + try { + const { publicKey, ceremony_id } = await auth.mfaWebAuthnBegin(); + const cred = (await navigator.credentials.get({ + publicKey: toRequestOptions(publicKey), + })) as PublicKeyCredential; + await auth.mfaWebAuthnFinish(ceremony_id, credentialToJSON(cred)); + window.location.href = "/"; + } catch (err) { + if (err instanceof ApiError && err.code === "mfa_ticket_expired") { + returnToCredentials("That sign-in attempt expired. Please sign in again."); + return; + } + setFactorError(describeError(err)); + } finally { + setFactorBusy(false); + } + } + + async function handlePasswordlessPasskey() { + setCredentialsNotice(""); + setPasskeyBusy(true); + try { + const { publicKey, ceremony_id } = await auth.passkeyLoginBegin(); + const cred = (await navigator.credentials.get({ + publicKey: toRequestOptions(publicKey), + })) as PublicKeyCredential; + await auth.passkeyLoginFinish(ceremony_id, credentialToJSON(cred)); + window.location.href = "/"; + } catch (err) { + setCredentialsNotice(describeError(err)); + } finally { + setPasskeyBusy(false); + } + } + + function handleEnrolComplete() { + window.location.href = "/"; + } + const showLocal = localEnabled || providers.length === 0; const showDivider = showLocal && providers.length > 0; @@ -111,9 +223,70 @@ export default function LoginPage() { Nobody can sign in, and its servers are not being managed, until this is resolved. If you manage this account, check your email from Vantage for details.

+ ) : step === "enrol" ? ( + + + + ) : step === "factor" ? ( + +
+

{useRecovery ? "Enter one of your recovery codes." : "Enter the 6-digit code from your authenticator app."}

+
+ + setFactorCode(e.target.value)} + className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" + /> +
+ + {factorError &&
{factorError}
} + + + + {factorMethods.includes("webauthn") && ( + + )} + + {factorMethods.includes("recovery") && ( + + )} + + +
+
) : ( {ssoError &&
{ssoError}
} + {credentialsNotice &&
{credentialsNotice}
} {showLocal && (
@@ -147,11 +320,17 @@ export default function LoginPage() { /> - {error &&
{(error as Error).message}
} + {error &&
{describeError(error)}
} + + {isPasskeySupported() && localEnabled && ( + + )}
)} diff --git a/web/components/mfa/MfaEnrolWizard.tsx b/web/components/mfa/MfaEnrolWizard.tsx new file mode 100644 index 0000000..3733a2c --- /dev/null +++ b/web/components/mfa/MfaEnrolWizard.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useEffect, useState } from "react"; +import QRCode from "qrcode"; +import { auth } from "@/lib/api"; +import { isPasskeySupported, toCreateOptions, credentialToJSON } from "@/lib/webauthn"; +import { Button } from "@/components/ui"; + +/** + * The calls a wizard step makes to set up a second factor. Defaults to the + * ticket-scoped `/auth/mfa/enrol/*` endpoints (an in-progress, unauthenticated + * sign-in identified by the server's ticket cookie) for `mode="ticket"`. + * + * `mode="session"` is for an already-authenticated user managing their own + * factors (the account security page) and has no safe default: that page's + * endpoints do not exist yet, so its caller must pass `endpoints` explicitly. + */ +export interface MfaEnrolEndpoints { + totpSetup: () => Promise<{ secret: string; otpauth_url: string }>; + totpConfirm: (code: string) => Promise<{ ok: true; recovery_codes?: string[] }>; + passkeyBegin: () => Promise<{ publicKey: any; ceremony_id: string }>; + passkeyFinish: (ceremonyId: string, credential: unknown, name?: string) => Promise<{ ok: true; recovery_codes?: string[] }>; +} + +const TICKET_ENDPOINTS: MfaEnrolEndpoints = { + totpSetup: () => auth.enrolTotpSetup(), + totpConfirm: (code) => auth.enrolTotpConfirm(code), + passkeyBegin: () => auth.enrolPasskeyBegin(), + passkeyFinish: (ceremonyId, credential, name) => auth.enrolPasskeyFinish(ceremonyId, credential, name), +}; + +interface MfaEnrolWizardProps { + mode: "session" | "ticket"; + /** Required for mode="session"; defaults to the ticket-scoped endpoints for mode="ticket". */ + endpoints?: MfaEnrolEndpoints; + onComplete: (recoveryCodes: string[]) => void; + /** Omit to make the wizard mandatory, as in forced enrolment during sign-in. */ + onCancel?: () => void; +} + +type Step = "choose" | "totp" | "passkey" | "recovery"; + +export function MfaEnrolWizard({ mode, endpoints, onComplete, onCancel }: MfaEnrolWizardProps) { + const api = endpoints ?? (mode === "ticket" ? TICKET_ENDPOINTS : undefined); + const [step, setStep] = useState("choose"); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + + // TOTP setup state + const [secret, setSecret] = useState(""); + const [qrDataUrl, setQrDataUrl] = useState(""); + const [totpCode, setTotpCode] = useState(""); + + // Recovery codes state + const [recoveryCodes, setRecoveryCodes] = useState([]); + const [savedConfirmed, setSavedConfirmed] = useState(false); + + if (!api) { + return
This wizard was not given its endpoints for session mode.
; + } + + async function startTotp() { + setError(""); + setBusy(true); + try { + const { secret, otpauth_url } = await api!.totpSetup(); + setSecret(secret); + setQrDataUrl(await QRCode.toDataURL(otpauth_url)); + setStep("totp"); + } catch (e) { + setError((e as Error).message); + } finally { + setBusy(false); + } + } + + async function confirmTotp(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setBusy(true); + try { + const res = await api!.totpConfirm(totpCode); + setRecoveryCodes(res.recovery_codes ?? []); + setStep("recovery"); + } catch (e) { + setError((e as Error).message); + } finally { + setBusy(false); + } + } + + async function startPasskey() { + setError(""); + setBusy(true); + try { + const { publicKey, ceremony_id } = await api!.passkeyBegin(); + const cred = (await navigator.credentials.create({ + publicKey: toCreateOptions(publicKey), + })) as PublicKeyCredential; + const res = await api!.passkeyFinish(ceremony_id, credentialToJSON(cred)); + setRecoveryCodes(res.recovery_codes ?? []); + setStep("recovery"); + } catch (e) { + setError((e as Error).message); + } finally { + setBusy(false); + } + } + + if (step === "choose") { + return ( +
+

This instance requires a second sign-in factor. Set one up to continue.

+ {error &&
{error}
} +
+ + {isPasskeySupported() && ( + + )} +
+ {onCancel && ( + + )} +
+ ); + } + + if (step === "totp") { + return ( +
+

Scan this code with your authenticator app, or enter the key manually.

+ {qrDataUrl && Authenticator QR code} +

{secret}

+
+ + setTotpCode(e.target.value)} + className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" + /> +
+ {error &&
{error}
} + + +
+ ); + } + + // step === "recovery" + return ( +
+

Save these recovery codes somewhere safe. Each can be used once if you lose access to your other factor.

+
+ {recoveryCodes.map((code) => ( +
{code}
+ ))} +
+ + +
+ ); +} diff --git a/web/lib/api.ts b/web/lib/api.ts index 9d4033d..fcd94a0 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -628,10 +628,13 @@ export interface AuthProviderUpdate { order?: number; } -class ApiError extends Error { +export class ApiError extends Error { constructor( public status: number, message: string, + public code?: string, + public retryAfter?: number, + public attemptsLeft?: number, ) { super(message); this.name = "ApiError"; @@ -675,11 +678,16 @@ async function authRequest(path: string, options?: RequestInit): Promise { if (!res.ok) { let message = `HTTP ${res.status}`; + let code: string | undefined; + let attemptsLeft: number | undefined; try { const body = await res.json(); if (body?.error) message = body.error; + if (body?.code) code = body.code; + if (typeof body?.attempts_left === "number") attemptsLeft = body.attempts_left; } catch {} - throw new ApiError(res.status, message); + const retryAfter = res.status === 429 ? Number(res.headers.get("Retry-After")) : undefined; + throw new ApiError(res.status, message, code, Number.isFinite(retryAfter) ? retryAfter : undefined, attemptsLeft); } if (res.status === 204) { @@ -701,8 +709,8 @@ export const auth = { }); }, - login(email: string, password: string): Promise<{ ok: boolean }> { - return authRequest<{ ok: boolean }>("/auth/login", { + login(email: string, password: string): Promise<{ ok?: true } | { mfa_required: true; methods: string[] } | { enrol_required: true }> { + return authRequest("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); @@ -712,6 +720,61 @@ export const auth = { return authRequest("/auth/logout", { method: "POST" }); }, + // --- second factor (an in-progress login, identified by the server-side ticket cookie) --- + + mfaTotp(code: string): Promise<{ ok: true }> { + return authRequest("/auth/mfa/totp", { method: "POST", body: JSON.stringify({ code }) }); + }, + + mfaRecovery(code: string): Promise<{ ok: true }> { + return authRequest("/auth/mfa/recovery", { method: "POST", body: JSON.stringify({ code }) }); + }, + + mfaWebAuthnBegin(): Promise<{ publicKey: any; ceremony_id: string }> { + return authRequest("/auth/mfa/webauthn/begin", { method: "POST" }); + }, + + mfaWebAuthnFinish(ceremonyId: string, credential: unknown): Promise<{ ok: true }> { + return authRequest("/auth/mfa/webauthn/finish", { + method: "POST", + body: JSON.stringify({ ceremony_id: ceremonyId, credential }), + }); + }, + + // --- passwordless passkey sign-in --- + + passkeyLoginBegin(): Promise<{ publicKey: any; ceremony_id: string }> { + return authRequest("/auth/passkey/begin", { method: "POST" }); + }, + + passkeyLoginFinish(ceremonyId: string, credential: unknown): Promise<{ ok: true }> { + return authRequest("/auth/passkey/finish", { + method: "POST", + body: JSON.stringify({ ceremony_id: ceremonyId, credential }), + }); + }, + + // --- forced enrolment (a fresh account that has not set up a second factor yet) --- + + enrolTotpSetup(): Promise<{ secret: string; otpauth_url: string }> { + return authRequest("/auth/mfa/enrol/totp/setup", { method: "POST" }); + }, + + enrolTotpConfirm(code: string): Promise<{ ok: true; recovery_codes?: string[] }> { + return authRequest("/auth/mfa/enrol/totp/confirm", { method: "POST", body: JSON.stringify({ code }) }); + }, + + enrolPasskeyBegin(): Promise<{ publicKey: any; ceremony_id: string }> { + return authRequest("/auth/mfa/enrol/passkey/begin", { method: "POST" }); + }, + + enrolPasskeyFinish(ceremonyId: string, credential: unknown, name?: string): Promise<{ ok: true; recovery_codes?: string[] }> { + return authRequest("/auth/mfa/enrol/passkey/finish", { + method: "POST", + body: JSON.stringify({ ceremony_id: ceremonyId, credential, name }), + }); + }, + me(): Promise { return authRequest("/auth/me"); }, diff --git a/web/lib/webauthn.ts b/web/lib/webauthn.ts new file mode 100644 index 0000000..35a2721 --- /dev/null +++ b/web/lib/webauthn.ts @@ -0,0 +1,68 @@ +// Browser-side WebAuthn helpers. The server sends and expects base64url; +// the browser's credential APIs need ArrayBuffers. + +function b64urlToBuffer(value: string): ArrayBuffer { + const padded = value.replace(/-/g, "+").replace(/_/g, "/"); + const binary = atob(padded.padEnd(padded.length + ((4 - (padded.length % 4)) % 4), "=")); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; +} + +function bufferToB64url(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let binary = ""; + for (const b of bytes) binary += String.fromCharCode(b); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +export function isPasskeySupported(): boolean { + return typeof window !== "undefined" && !!window.PublicKeyCredential; +} + +export function toCreateOptions(options: any): PublicKeyCredentialCreationOptions { + return { + ...options, + challenge: b64urlToBuffer(options.challenge), + user: { ...options.user, id: b64urlToBuffer(options.user.id) }, + excludeCredentials: (options.excludeCredentials ?? []).map((c: any) => ({ + ...c, + id: b64urlToBuffer(c.id), + })), + }; +} + +export function toRequestOptions(options: any): PublicKeyCredentialRequestOptions { + return { + ...options, + challenge: b64urlToBuffer(options.challenge), + allowCredentials: (options.allowCredentials ?? []).map((c: any) => ({ + ...c, + id: b64urlToBuffer(c.id), + })), + }; +} + +// credentialToJSON produces the shape go-webauthn's parsers read. +export function credentialToJSON(cred: PublicKeyCredential): unknown { + const response = cred.response as AuthenticatorAttestationResponse & AuthenticatorAssertionResponse; + const json: any = { + id: cred.id, + rawId: bufferToB64url(cred.rawId), + type: cred.type, + clientExtensionResults: cred.getClientExtensionResults(), + response: { clientDataJSON: bufferToB64url(response.clientDataJSON) }, + }; + if (response.attestationObject) { + json.response.attestationObject = bufferToB64url(response.attestationObject); + if (typeof response.getTransports === "function") { + json.response.transports = response.getTransports(); + } + } + if (response.authenticatorData) { + json.response.authenticatorData = bufferToB64url(response.authenticatorData); + json.response.signature = bufferToB64url(response.signature); + json.response.userHandle = response.userHandle ? bufferToB64url(response.userHandle) : null; + } + return json; +} diff --git a/web/package-lock.json b/web/package-lock.json index d75634a..e2bf91a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,12 +11,14 @@ "@tanstack/react-query": "^5.51.1", "clsx": "^2.1.1", "next": "16.2.9", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^2.4.0" }, "devDependencies": { "@types/node": "^20.14.11", + "@types/qrcode": "^1.5.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.19", @@ -71,6 +73,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -280,32 +283,10 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "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==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, @@ -616,9 +597,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -635,9 +613,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -654,9 +629,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -673,9 +645,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -692,9 +661,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -711,9 +677,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -730,9 +693,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -749,9 +709,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -768,9 +725,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -793,9 +747,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -818,9 +769,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -843,9 +791,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -868,9 +813,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -893,9 +835,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -918,9 +857,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -943,9 +879,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1161,9 +1094,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1180,9 +1110,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1199,9 +1126,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1218,9 +1142,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1401,12 +1322,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1467,6 +1399,7 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -1823,9 +1756,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1840,9 +1770,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1857,9 +1784,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1874,9 +1798,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1891,9 +1812,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1908,9 +1826,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1925,9 +1840,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1942,9 +1854,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1959,9 +1868,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1976,9 +1882,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2018,6 +1921,18 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -2029,6 +1944,17 @@ "tslib": "^2.4.0" } }, + "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", @@ -2077,6 +2003,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2111,11 +2038,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2498,6 +2433,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2572,6 +2508,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2663,6 +2608,17 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2676,7 +2632,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2689,7 +2644,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -2830,6 +2784,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2890,6 +2853,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -3145,6 +3114,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3330,6 +3300,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3779,6 +3750,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4281,6 +4261,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4550,6 +4539,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5193,6 +5183,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5210,7 +5209,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5272,6 +5270,15 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5302,6 +5309,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -5499,6 +5507,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5525,6 +5550,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5537,6 +5563,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5619,6 +5646,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5772,6 +5814,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6008,6 +6056,26 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6122,6 +6190,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6381,6 +6461,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6550,6 +6631,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6777,6 +6859,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", @@ -6809,6 +6897,26 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6816,6 +6924,93 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6835,6 +7030,7 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index edf0d63..3dcd0d0 100644 --- a/web/package.json +++ b/web/package.json @@ -9,22 +9,24 @@ "lint": "next lint" }, "dependencies": { - "next": "16.2.9", - "react": "^18.3.1", - "react-dom": "^18.3.1", "@tanstack/react-query": "^5.51.1", "clsx": "^2.1.1", + "next": "16.2.9", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", "tailwind-merge": "^2.4.0" }, "devDependencies": { "@types/node": "^20.14.11", + "@types/qrcode": "^1.5.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.19", + "eslint": "^9.0.0", + "eslint-config-next": "16.2.9", "postcss": "^8.4.39", "tailwindcss": "^3.4.6", - "typescript": "^5.5.3", - "eslint": "^9.0.0", - "eslint-config-next": "16.2.9" + "typescript": "^5.5.3" } }