feat(web): MFA and passkey sign-in on the login page
This commit is contained in:
+182
-3
@@ -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<string, string> = {
|
||||
oidc_unavailable: "Single sign-on is not available on this instance's plan.",
|
||||
@@ -23,8 +25,27 @@ const ERROR_MESSAGES: Record<string, string> = {
|
||||
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<Step>("credentials");
|
||||
const [factorMethods, setFactorMethods] = useState<string[]>([]);
|
||||
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.
|
||||
</p>
|
||||
</Card>
|
||||
) : step === "enrol" ? (
|
||||
<Card>
|
||||
<MfaEnrolWizard mode="ticket" onComplete={handleEnrolComplete} />
|
||||
</Card>
|
||||
) : step === "factor" ? (
|
||||
<Card>
|
||||
<form onSubmit={handleFactorSubmit} className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">{useRecovery ? "Enter one of your recovery codes." : "Enter the 6-digit code from your authenticator app."}</p>
|
||||
<div>
|
||||
<label htmlFor="factor-code" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
{useRecovery ? "Recovery code" : "Verification code"}
|
||||
</label>
|
||||
<input
|
||||
id="factor-code"
|
||||
type="text"
|
||||
inputMode={useRecovery ? "text" : "numeric"}
|
||||
autoComplete="one-time-code"
|
||||
required
|
||||
autoFocus
|
||||
value={factorCode}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{factorError && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{factorError}</div>}
|
||||
|
||||
<Button type="submit" variant="primary" loading={factorBusy} className="w-full justify-center">
|
||||
Verify
|
||||
</Button>
|
||||
|
||||
{factorMethods.includes("webauthn") && (
|
||||
<Button type="button" variant="secondary" loading={factorBusy} className="w-full justify-center" onClick={handleFactorPasskey}>
|
||||
Use passkey
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{factorMethods.includes("recovery") && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-center text-sm text-text-secondary underline-offset-2 hover:underline"
|
||||
onClick={() => {
|
||||
setUseRecovery((v) => !v);
|
||||
setFactorCode("");
|
||||
setFactorError("");
|
||||
}}
|
||||
>
|
||||
{useRecovery ? "Use a verification code instead" : "Use a recovery code instead"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-center text-sm text-text-tertiary underline-offset-2 hover:underline"
|
||||
onClick={() => returnToCredentials("")}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</form>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
{ssoError && <div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{ssoError}</div>}
|
||||
{credentialsNotice && <div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{credentialsNotice}</div>}
|
||||
|
||||
{showLocal && (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
@@ -147,11 +320,17 @@ export default function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{(error as Error).message}</div>}
|
||||
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{describeError(error)}</div>}
|
||||
|
||||
<Button type="submit" variant="primary" loading={isPending} className="w-full justify-center">
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
{isPasskeySupported() && localEnabled && (
|
||||
<Button type="button" variant="secondary" loading={passkeyBusy} className="w-full justify-center" onClick={handlePasswordlessPasskey}>
|
||||
Sign in with passkey
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<Step>("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<string[]>([]);
|
||||
const [savedConfirmed, setSavedConfirmed] = useState(false);
|
||||
|
||||
if (!api) {
|
||||
return <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">This wizard was not given its endpoints for session mode.</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">This instance requires a second sign-in factor. Set one up to continue.</p>
|
||||
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</div>}
|
||||
<div className="space-y-2">
|
||||
<Button type="button" variant="primary" className="w-full justify-center" loading={busy} onClick={startTotp}>
|
||||
Use an authenticator app
|
||||
</Button>
|
||||
{isPasskeySupported() && (
|
||||
<Button type="button" variant="secondary" className="w-full justify-center" loading={busy} onClick={startPasskey}>
|
||||
Use a passkey
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{onCancel && (
|
||||
<Button type="button" variant="ghost" className="w-full justify-center" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "totp") {
|
||||
return (
|
||||
<form onSubmit={confirmTotp} className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">Scan this code with your authenticator app, or enter the key manually.</p>
|
||||
{qrDataUrl && <img src={qrDataUrl} alt="Authenticator QR code" className="mx-auto h-40 w-40" />}
|
||||
<p className="break-all rounded-lg border border-border bg-surface-2 px-3 py-2 text-center font-mono text-xs text-text-secondary">{secret}</p>
|
||||
<div>
|
||||
<label htmlFor="totp-code" className="mb-1.5 block text-sm font-medium text-text-secondary">
|
||||
6-digit code
|
||||
</label>
|
||||
<input
|
||||
id="totp-code"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
required
|
||||
maxLength={6}
|
||||
value={totpCode}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</div>}
|
||||
<Button type="submit" variant="primary" loading={busy} className="w-full justify-center">
|
||||
Confirm
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" className="w-full justify-center" onClick={() => setStep("choose")}>
|
||||
Back
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// step === "recovery"
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">Save these recovery codes somewhere safe. Each can be used once if you lose access to your other factor.</p>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border bg-surface-2 p-3 font-mono text-xs text-text-primary">
|
||||
{recoveryCodes.map((code) => (
|
||||
<div key={code}>{code}</div>
|
||||
))}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<input type="checkbox" checked={savedConfirmed} onChange={(e) => setSavedConfirmed(e.target.checked)} className="h-4 w-4 rounded border-border" />
|
||||
I have saved these codes
|
||||
</label>
|
||||
<Button type="button" variant="primary" className="w-full justify-center" disabled={!savedConfirmed} onClick={() => onComplete(recoveryCodes)}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+67
-4
@@ -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<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
|
||||
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<void>("/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<MeResponse> {
|
||||
return authRequest<MeResponse>("/auth/me");
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Generated
+315
-119
@@ -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"
|
||||
}
|
||||
|
||||
+8
-6
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user